最大子段和問題(Maximum Interval Sum)
(有時也稱LIS)
經典的動態規劃問題,幾乎所有的演算法教材都會提到.本文將分析最大子段和問題的幾種不同效率的解法,以及最大子段和問題的擴充和運用. 一.問題描述
給定長度為n的整數序列,a[1...n], 求[1,n]某個子區間[i , j]使得a[i]+…+a[j]和最大.或者求出最大的這個和.例如(-2,11,-4,13,-5,2)的最大子段和為20,所求子區間為[2,4].
二. 問題分析 1.窮舉法
窮舉應當是每個人都要學會的一種方式,這裡實際上是要窮舉所有的[1,n]之間的區間,所以我們用兩重迴圈,可以很輕易地做到遍曆所有子區間,一個表示起始位置,一個表示終點位置.代碼如下:
int start = 0;//起始位置int end = 0; //結束位置int max = 0;for(int i = 1; i <= n; ++i){ for(int j = i; j <= n;++j) { int sum = 0; for(int k = i; k <=j; ++k) sum += a[k]; if(sum > max) { start = i; end = j; max = sum; } }}
這個演算法是幾乎所有人都能想到的,它所需要的計算時間是O(n^3).當然,這個代碼還可以做點最佳化,實際上我們並不需要每次都重新從起始位置求和加到終點位置.可以充分利用之前的計算結果.
或者我們換一種窮舉思路,對於起點 i,我們遍曆所有長度為1,2,…,n-i+1的子區間和,以求得和最大的一個.這樣也遍曆了所有的起點的不同長度的子區間,同時,對於相同起點的不同長度的子區間,可以利用前面的計算結果來計算後面的.
比如,i為起點長度為2的子區間和就等於長度為1的子區間的和+a[i+1]即可,這樣就省掉了一個迴圈,計算時間複雜度減少到了O(n^2).代碼如下:
int start = 0;//起始位置int end = 0;//結束位置int max = 0;for(int i = 1; i <= n; ++i){ int sum = 0; for(int j = i; j <= n;++j) { sum += a[j]; if(sum > max) { start = i; end = j; max = sum; } }}
2.分治法
求子區間及最大和,從結構上是非常適合分治法的,因為所有子區間[start, end]只可能有以下三種可能性: 在[1, n/2]這個地區內 在[n/2+1, n]這個地區內 起點位於[1,n/2],終點位於[n/2+1,n]內
以上三種情形的最大者,即為所求. 前兩種情形符合子問題遞迴特性,所以遞迴可以求出. 對於第三種情形,則需要單獨處理. 第三種情形必然包括了n/2和n/2+1兩個位置,這樣就可以利用第二種窮舉的思路求出: 以n/2為終點,往左移動擴張,求出和最大的一個left_max 以n/2+1為起點,往右移動擴張,求出和最大的一個right_max left_max+right_max是第三種情況可能的最大值
樣本:
int maxInterval(int *a, int left, int right) { if(right==left) return a[left]>0?a[left]:0; int center = (left+right)/2; //左邊區間的最大子段和 int leftMaxInterval = maxInterval(a,left,center); //右邊區間的最大子段和 int rightMaxInterval= maxInterval(a,center+1,right); //以下求端點分別位於不同部分的最大子段和 //center開始向左移動 int sum = 0; int left_max = 0; for(int i = center; i >= left; –i) { sum += a[i]; if(sum > left_max) left_max = sum; } //center+1開始向右移動 sum = 0; int right_max = 0; for(int i = center+1; i <= right; ++i) { sum += a[i]; if(sum > right_max) right_max = sum; } int ret = left_max+right_max; if(ret < leftMaxInterval) ret = leftMaxInterval; if(ret < rightMaxInterval) ret = rightMaxInterval; return ret; }
分治法的痛點在於第三種情形的理解,這裡應該抓住第三種情形的特點,也就是中間有兩個定點,然後分別往兩個方向擴張,以遍曆所有屬於第三種情形的子區間,求的最大的一個,如果要求得具體的區間,稍微對上述代碼做點修改即可. 分治法的計算時間複雜度為O(nlogn). 3.動態規劃法
動態規劃的基本原理這裡不再贅述,主要討論這個問題的建模過程和子問題結構.時刻記住一個前提,這裡是連續的區間 令b[j]表示以位置 j 為終點的所有子區間中和最大的一個 子問題:如j為終點的最大子區間包含了位置j-1,則以j-1為終點的最大子區間必然包括在其中 如果b[j-1] >0, 那麼顯然b[j] = b[j-1] + a[j],用之前最大的一個加上a[j]即可,因為a[j]必須包含 如果b[j-1]<=0,那麼b[j] = a[j] ,因為既然最大,前面的負數必然不能使你更大
對於這種子問題結構和最佳化問題的證明,可以參考演算法導論上的“剪下法”,即如果不包括子問題的最優解,把你假設的解粘帖上去,會得出子問題的最佳化矛盾.證明如下: 令a[x,y]表示a[x]+…+a[y] , y>=x 假設以j為終點的最大子區間 [s, j] 包含了j-1這個位置,以j-1為終點的最大子區間[ r, j-1]並不包含其中 即假設[r,j-1]不是[s,j]的子區間 存在s使得a[s, j-1]+a[j]為以j為終點的最大子段和,這裡的 r != s 由於[r, j -1]是最優解, 所以a[s,j-1]<a[r, j-1],所以a[s,j-1]+a[j]<a[r, j-1]+a[j] 與[s,j]為最優解矛盾.
執行個體:
int max = 0;int b[n+1];int start = 0;int end = 0;memset(b,0,n+1);for(int i = 1; i <= n; ++i) { if(b[i-1]>0) { b[i] = b[i-1]+a[i]; }else{ b[i] = a[i]; } if(b[i]>max) max = b[i]; }
動態規劃法的計算時間複雜度為O(n),是最優的解。做幾道題加深理解
最直白的LIS題:http://acm.hdu.edu.cn/showproblem.php?pid=1087
最大子段和升級版,最大M段和:http://acm.hdu.edu.cn/showproblem.php?pid=1024