整理自 http://blog.csdn.net/v_JULY_v/article/details/6444021
求子數組的最大和
題目描述:
輸入一個整形數組,數組里有正數也有負數。
數組中連續的一個或多個整數組成一個子數組,每個子數組都有一個和。
求所有子數組的和的最大值。要求時間復雜度為O(n)。
例如輸入的數組為1, -2, 3, 10, -4, 7, 2, -5,和最大的子數組為3, 10, -4, 7, 2,
因此輸出為該子數組的和18。
思路
一. 動態規划
設sum[i]為以第i個元素結尾且和最大的連續子數組。假設對於元素i,所有以它前面的元素結尾的子數組的長度都已經求得,那么以第i個元素結尾且和最大的連續子數組實際上,要么是以第i-1個元素結尾且和最大的連續子數組加上這個元素,要么是只包含第i個元素,即sum[i] = max(sum[i-1] + a[i], a[i])。可以通過判斷sum[i-1] + a[i]是否大於a[i]來做選擇,而這實際上等價於判斷sum[i-1]是否大於0。由於每次運算只需要前一次的結果,因此並不需要像普通的動態規划那樣保留之前所有的計算結果,只需要保留上一次的即可,因此算法的時間和空間復雜度都很小。
偽代碼如下
result = a[1] sum = a[1] for i: 2 to LENGTH[a] if sum > 0 sum += a[i] else sum = a[i] if sum > result result = sum return result
二. 掃描法
(后加注:這里提到的掃描法存在一個問題就是如果最大字段和小於0則算法沒法給出正確答案。其實這個問題用動態規划就好,這里的掃描法其實真的不是個好方法,只是因為很有名所以還是粘出來了)
當我們加上一個正數時,和會增加;當我們加上一個負數時,和會減少。如果當前得到的和是個負數,那么這個和在接下來的累加中應該拋棄並重新清零,不然的話這個負數將會減少接下來的和。實現:
//copyright@ July 2010/10/18 //updated,2011.05.25. #include <iostream.h> int maxSum(int* a, int n) { int sum=0; //其實要處理全是負數的情況,很簡單,如稍后下面第3點所見,直接把這句改成:"int sum=a[0]"即可 //也可以不改,當全是負數的情況,直接返回0,也不見得不行。 int b=0; for(int i=0; i<n; i++) { if(b<0) //... b=a[i]; else b+=a[i]; if(sum<b) sum=b; } return sum; } int main() { int a[10]={1, -2, 3, 10, -4, 7, 2, -5}; //int a[]={-1,-2,-3,-4}; //測試全是負數的用例 cout<<maxSum(a,8)<<endl; return 0; } /*------------------------------------- 解釋下: 例如輸入的數組為1, -2, 3, 10, -4, 7, 2, -5, 那么最大的子數組為3, 10, -4, 7, 2, 因此輸出為該子數組的和18。 所有的東西都在以下倆行, 即: b : 0 1 -1 3 13 9 16 18 13 sum: 0 1 1 3 13 13 16 18 18 其實算法很簡單,當前面的幾個數,加起來后,b<0后, 把b重新賦值,置為下一個元素,b=a[i]。 當b>sum,則更新sum=b; 若b<sum,則sum保持原值,不更新。。July、10/31。
(后加注:前面的代碼是粘貼別人的,下面的證明是我自己鼓搗的。之后由於看到了動態規划法,覺得這個證明實在是沒什么必要看了。在后來練習了很多數學證明發現,證明是越精煉越好,不過這種敢於窮舉情況的思路還是很好的,很多時候難的證明題只要多窮舉幾種情況再加以精煉往往就能做出,大不了多舉幾個情況問題也能解決)
據說這道題是《編程珠機》里面的題目,叫做掃描法,速度最快,掃描一次就求出結果,復雜度是O(n)。書中說,這個算法是一個統計學家提出的。
這個算法如此精煉簡單,而且復雜度只有線性。但是我想,能想出來卻非常困難,而且證明也不簡單。在這里,我斗膽寫出自己證明的想法:
關於這道題的證明,我的思路是去證明這樣的掃描法包含了所有n^2種情況,即所有未顯示列出的子數組都可以在本題的掃描過程中被拋棄。
1 首先,假設算法掃描到某個地方時,始終未出現加和小於等於0的情況。
我們可以把所有子數組(實際上為當前掃描過的元素所組成的子數組)列為三種:
1.1 以開頭元素為開頭,結尾為任一的子數組
1.2 以結尾元素為結尾,開頭為任一的子數組
1.3 開頭和結尾都不等於當前開頭結尾的所有子數組
1.1由於遍歷過程中已經掃描,所以算法已經考慮了。1.2確實沒考慮,但我們隨便找到1.2中的某一個數組,可知,從開頭元素到這個1.2中的數組的加和大於0(因為如果小於0就說明掃描過程中遇到小於0的情況,不包括在大前提1之內),那么這個和一定小於從開頭到這個1.2數組結尾的和。故此種情況可舍棄
1.3 可以以1.2同樣的方法證明,因為我們的結尾已經列舉了所有的情況,那么每一種情況和1.2是相同的,故也可以舍棄。
2 如果當前加和出現小於等於0的情況,且是第一次出現,可知前面所有的情況加和都不為0
一個很直觀的結論是,如果子段和小於0,我們可以拋棄,但問題是是不是他的所有以此子段結尾為結尾而開頭任意的子段也需要拋棄呢?
答案是肯定的。因為以此子段開頭為開頭而結尾任意的子段加和都大於0(情況2的前提),所以這些子段的和是小於當前子段的,也就是小於0的,對於后面也是需要拋棄的。也就是說,所有以之前的所有元素為開頭而以當前結尾之后元素為結尾的數組都可以拋棄了。
而對於后面拋棄后的數組,則可以同樣遞歸地用1 2兩個大情況進行分析,於是得證。
這個算法的證明有些復雜,現在感覺應該不會錯,至少思路是對的,誰幫着在表達上優化下吧。:-)
三. 分治,合並的時候窮舉
(這個也是直接從原帖抄的,思路應該不難理解。鑒於復雜度高又遞歸,實際解決問題時不建議采用)
//Algorithm 3:時間效率為O(n*log n) //算法3的主要思想:采用二分策略,將序列分成左右兩份。 //那么最長子序列有三種可能出現的情況,即 //【1】只出現在左部分. //【2】只出現在右部分。 //【3】出現在中間,同時涉及到左右兩部分。 //分情況討論之。 static int MaxSubSum(const int A[],int Left,int Right) { int MaxLeftSum,MaxRightSum; //左、右部分最大連續子序列值。對應情況【1】、【2】 int MaxLeftBorderSum,MaxRightBorderSum; //從中間分別到左右兩側的最大連續子序列值,對應case【3】。 int LeftBorderSum,RightBorderSum; int Center,i; if(Left == Right)Base Case if(A[Left]>0) return A[Left]; else return 0; Center=(Left+Right)/2; MaxLeftSum=MaxSubSum(A,Left,Center); MaxRightSum=MaxSubSum(A,Center+1,Right); MaxLeftBorderSum=0; LeftBorderSum=0; for(i=Center;i>=Left;i--) { LeftBorderSum+=A[i]; if(LeftBorderSum>MaxLeftBorderSum) MaxLeftBorderSum=LeftBorderSum; } MaxRightBorderSum=0; RightBorderSum=0; for(i=Center+1;i<=Right;i++) { RightBorderSum+=A[i]; if(RightBorderSum>MaxRightBorderSum) MaxRightBorderSum=RightBorderSum; } int max1=MaxLeftSum>MaxRightSum?MaxLeftSum:MaxRightSum; int max2=MaxLeftBorderSum+MaxRightBorderSum; return max1>max2?max1:max2; }