算法--三种方法求连续子数组的最大和
題目描述:
輸入一個(gè)整形數(shù)組,數(shù)組里有正數(shù)也有負(fù)數(shù)。 數(shù)組中連續(xù)的一個(gè)或多個(gè)整數(shù)組成一個(gè)子數(shù)組,每個(gè)子數(shù)組都有一個(gè)和。 求所有子數(shù)組的和的最大值。要求時(shí)間復(fù)雜度為O(n)。例如輸入的數(shù)組為1, -2, 3, 10, -4, 7, 2, -5,和最大的子數(shù)組為3, 10, -4, 7, 2, 因此輸出為該子數(shù)組的和18。下面按照時(shí)間復(fù)雜度逐步優(yōu)化的順序依次給出這三種算法。
暴力求解法
該方法的思想非常簡(jiǎn)單,先找出從第1個(gè)元素開始的最大子數(shù)組,而后再?gòu)牡?個(gè)元素開始找出從第2個(gè)元素開始的最大子數(shù)組,依次類推,比較得出最大的子數(shù)組。實(shí)現(xiàn)代碼如下:
/* 常規(guī)方法,時(shí)間復(fù)雜度O(n*n) 先從第一個(gè)元素開始向后累加, 每次累加后與之前的和比較,保留最大值, 再?gòu)牡诙€(gè)元素開始向后累加,以此類推。 */ int MaxSubSum1(int *arr,int len) {int i,j;int MaxSum = 0;//每次開始累加的起始位置的循環(huán)for(i=0;i<len;i++){int CurSum = 0;//向后累加的循環(huán)for(j=i;j<len;j++){CurSum += arr[j];if(CurSum > MaxSum)MaxSum = CurSum;}}return MaxSum; }很明顯地可以看出,該方法的時(shí)間復(fù)雜度為O(n*n)。
分治求解法
解釋一
考慮將數(shù)組從中間分為兩個(gè)子數(shù)組,則最大子數(shù)組必然出現(xiàn)在以下三種情況之一:
1、完全位于左邊的數(shù)組中。2、完全位于右邊的數(shù)組中。3、跨越中點(diǎn),包含左右數(shù)組中靠近中點(diǎn)的部分。遞歸將左右子數(shù)組再分別分成兩個(gè)數(shù)組,直到子數(shù)組中只含有一個(gè)元素,退出每層遞歸前,返回上面三種情況中的最大值。實(shí)現(xiàn)代碼如下: /* 求三個(gè)數(shù)中的最大值 */ int Max3(int a,int b,int c) {int Max = a;if(b > Max)Max = b;if(c > Max)Max = c;return Max; }/* 次優(yōu)算法,采用分治策略 */ int MaxSubSum2(int *arr,int left,int right) {int MaxLeftSum,MaxRightSum; //左右邊的最大和int MaxLeftBorderSum,MaxRightBorderSum; //含中間邊界的左右部分最大和int LeftBorderSum,RightBorderSum; //含中間邊界的左右部分當(dāng)前和int i,center;//遞歸到最后的基本情況if(left == right)if(arr[left]>0)return arr[left];elsereturn 0;//求含中間邊界的左右部分的最大值center = (left + right)/2;MaxLeftBorderSum = 0;LeftBorderSum = 0;for(i=center;i>=left;i--){LeftBorderSum += arr[i];if(LeftBorderSum > MaxLeftBorderSum)MaxLeftBorderSum = LeftBorderSum;}MaxRightBorderSum = 0;RightBorderSum = 0;for(i=center+1;i<=right;i++){RightBorderSum += arr[i];if(RightBorderSum > MaxRightBorderSum)MaxRightBorderSum = RightBorderSum;}//遞歸求左右部分最大值MaxLeftSum = MaxSubSum2(arr,left,center);MaxRightSum = MaxSubSum2(arr,center+1,right);//返回三者中的最大值return Max3(MaxLeftSum,MaxRightSum,MaxLeftBorderSum+MaxRightBorderSum); }/* 將分支策略實(shí)現(xiàn)的算法封裝起來 */ int MaxSubSum2_1(int *arr,int len) {return MaxSubSum2(arr,0,len-1); }設(shè)該算法的時(shí)間復(fù)雜度為T(n),則:
T(n)= 2T(n/2)+ O(n),且T(1)= 1。
逐步遞推得到時(shí)間復(fù)雜度T(n)= O(nlogn)。
另一種通俗解釋二
這里再介紹一種更高效的算法,時(shí)間復(fù)雜度為O(nlogn)。這是個(gè)分治的思想,解決復(fù)雜問題我們經(jīng)常使用的一種思維方法——分而治之。
而對(duì)于此題,我們把數(shù)組A[1..n]分成兩個(gè)相等大小的塊:
前兩種情況的求法和整體的求法是一樣的,因此遞歸求得。
第三種,我們可以采取的方法也比較簡(jiǎn)單,沿著第n/2向左搜索,直到左邊界,找到最大的和maxleft,以及沿著第n/2+1向右搜索找到最大和maxright,那么總的最大和就是maxleft+maxright。而數(shù)組A的最大子數(shù)組和就是這三種情況中最大的一個(gè)。
偽代碼
int maxSubArray(int *A,int l,int r) {if l<r do mid = (l+r)/2;ml = maxSubArray(A,l,mid); //分治 mr = maxSubArray(A,mid+1,r);for i=mid downto l do search maxleft; for i=mid+1 to r do search maxright; return max(ml,mr,maxleft+maxright); //歸并 then //遞歸出口 return A[l]; }線性時(shí)間算法
該算法在每次元素累加和小于0時(shí),從下一個(gè)元素重新開始累加。實(shí)現(xiàn)代碼如下:
/* 最優(yōu)方法,時(shí)間復(fù)雜度O(n) 和最大的子序列的第一個(gè)元素肯定是正數(shù) 因?yàn)樵赜姓胸?fù),因此子序列的最大和一定大于0 */ int MaxSubSum3(int *arr,int len) {int i;int MaxSum = 0;int CurSum = 0;for(i=0;i<len;i++){CurSum += arr[i];if(CurSum > MaxSum)MaxSum = CurSum;//如果累加和出現(xiàn)小于0的情況,//則和最大的子序列肯定不可能包含前面的元素,//這時(shí)將累加和置0,從下個(gè)元素重新開始累加if(CurSum < 0)CurSum = 0;}return MaxSum; }顯然,該算法的時(shí)間復(fù)雜度O(n)。該算法理解起來應(yīng)該不難,但是要想出來可就不容易了。另外,該算法的一個(gè)附帶的有點(diǎn)是:它只對(duì)數(shù)據(jù)進(jìn)行一次掃描,一旦元素被讀入并被處理,它就不再需要被記憶。因此,如果數(shù)組在磁盤或磁帶上,他就可以被順序讀入,在主存中不必存儲(chǔ)數(shù)組的任何部分。不僅如此,在任意時(shí)刻,該算法都能對(duì)它已經(jīng)讀入的數(shù)據(jù)給出最大子數(shù)組(另外兩種算法不具有這種特性)。具有這種特性的算法叫做聯(lián)機(jī)算法。
掃描法
掃描算法實(shí)際上就是上面的線性時(shí)間算法,這里是轉(zhuǎn)載的自另一個(gè)作者,思路邏輯可能更加嚴(yán)謹(jǐn)一些,分析也更加精確。
當(dāng)我們加上一個(gè)正數(shù)時(shí),和會(huì)增加;當(dāng)我們加上一個(gè)負(fù)數(shù)時(shí),和會(huì)減少。如果當(dāng)前得到的和是個(gè)負(fù)數(shù),那么這個(gè)和在接下來的累加中應(yīng)該拋棄并重新清零,不然的話這個(gè)負(fù)數(shù)將會(huì)減少接下來的和。實(shí)現(xiàn):
(后加注:這里提到的掃描法存在一個(gè)問題就是如果最大字段和小于0則算法沒法給出正確答案。其實(shí)這個(gè)問題用動(dòng)態(tài)規(guī)劃就好,這里的掃描法其實(shí)真的不是個(gè)好方法,只是因?yàn)楹苡忻赃€是粘出來了)
//copyright@ July 2010/10/18 //updated,2011.05.25. #include <iostream.h> int maxSum(int* a, int n) { int sum=0; //其實(shí)要處理全是負(fù)數(shù)的情況,很簡(jiǎn)單,如稍后下面第3點(diǎn)所見,直接把這句改成:"int sum=a[0]"即可 //也可以不改,當(dāng)全是負(fù)數(shù)的情況,直接返回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}; //測(cè)試全是負(fù)數(shù)的用例 cout<<maxSum(a,8)<<endl; return 0; } /*------------------------------------- 解釋下: 例如輸入的數(shù)組為1, -2, 3, 10, -4, 7, 2, -5, 那么最大的子數(shù)組為3, 10, -4, 7, 2, 因此輸出為該子數(shù)組的和18。 所有的東西都在以下倆行, 即: b : 0 1 -1 3 13 9 16 18 13 sum: 0 1 1 3 13 13 16 18 18 其實(shí)算法很簡(jiǎn)單,當(dāng)前面的幾個(gè)數(shù),加起來后,b<0后, 把b重新賦值,置為下一個(gè)元素,b=a[i]。 當(dāng)b>sum,則更新sum=b; 若b<sum,則sum保持原值,不更新。。July、10/31。(后加注:前面的代碼是粘貼別人的,下面的證明是我自己鼓搗的。之后由于看到了動(dòng)態(tài)規(guī)劃法,覺得這個(gè)證明實(shí)在是沒什么必要看了。在后來練習(xí)了很多數(shù)學(xué)證明發(fā)現(xiàn),證明是越精煉越好,不過這種敢于窮舉情況的思路還是很好的,很多時(shí)候難的證明題只要多窮舉幾種情況再加以精煉往往就能做出,大不了多舉幾個(gè)情況問題也能解決)
據(jù)說這道題是《編程珠機(jī)》里面的題目,叫做掃描法,速度最快,掃描一次就求出結(jié)果,復(fù)雜度是O(n)。書中說,這個(gè)算法是一個(gè)統(tǒng)計(jì)學(xué)家提出的。
這個(gè)算法如此精煉簡(jiǎn)單,而且復(fù)雜度只有線性。但是我想,能想出來卻非常困難,而且證明也不簡(jiǎn)單。在這里,我斗膽寫出自己證明的想法:
關(guān)于這道題的證明,我的思路是去證明這樣的掃描法包含了所有n^2種情況,即所有未顯示列出的子數(shù)組都可以在本題的掃描過程中被拋棄。
1 首先,假設(shè)算法掃描到某個(gè)地方時(shí),始終未出現(xiàn)加和小于等于0的情況。
我們可以把所有子數(shù)組(實(shí)際上為當(dāng)前掃描過的元素所組成的子數(shù)組)列為三種:
1.1 以開頭元素為開頭,結(jié)尾為任一的子數(shù)組
1.2 以結(jié)尾元素為結(jié)尾,開頭為任一的子數(shù)組
1.3 開頭和結(jié)尾都不等于當(dāng)前開頭結(jié)尾的所有子數(shù)組
1.1由于遍歷過程中已經(jīng)掃描,所以算法已經(jīng)考慮了。1.2確實(shí)沒考慮,但我們隨便找到1.2中的某一個(gè)數(shù)組,可知,從開頭元素到這個(gè)1.2中的數(shù)組的加和大于0(因?yàn)槿绻∮?就說明掃描過程中遇到小于0的情況,不包括在大前提1之內(nèi)),那么這個(gè)和一定小于從開頭到這個(gè)1.2數(shù)組結(jié)尾的和。故此種情況可舍棄
1.3 可以以1.2同樣的方法證明,因?yàn)槲覀兊慕Y(jié)尾已經(jīng)列舉了所有的情況,那么每一種情況和1.2是相同的,故也可以舍棄。
2 如果當(dāng)前加和出現(xiàn)小于等于0的情況,且是第一次出現(xiàn),可知前面所有的情況加和都不為0
一個(gè)很直觀的結(jié)論是,如果子段和小于0,我們可以拋棄,但問題是是不是他的所有以此子段結(jié)尾為結(jié)尾而開頭任意的子段也需要拋棄呢?
答案是肯定的。因?yàn)橐源俗佣伍_頭為開頭而結(jié)尾任意的子段加和都大于0(情況2的前提),所以這些子段的和是小于當(dāng)前子段的,也就是小于0的,對(duì)于后面也是需要拋棄的。也就是說,所有以之前的所有元素為開頭而以當(dāng)前結(jié)尾之后元素為結(jié)尾的數(shù)組都可以拋棄了。
而對(duì)于后面拋棄后的數(shù)組,則可以同樣遞歸地用1 2兩個(gè)大情況進(jìn)行分析,于是得證。
這個(gè)算法的證明有些復(fù)雜,現(xiàn)在感覺應(yīng)該不會(huì)錯(cuò),至少思路是對(duì)的,誰(shuí)幫著在表達(dá)上優(yōu)化下吧。:-)
動(dòng)態(tài)規(guī)劃
設(shè)sum[i]為以第i個(gè)元素結(jié)尾且和最大的連續(xù)子數(shù)組。假設(shè)對(duì)于元素i,所有以它前面的元素結(jié)尾的子數(shù)組的長(zhǎng)度都已經(jīng)求得,那么以第i個(gè)元素結(jié)尾且和最大的連續(xù)子數(shù)組實(shí)際上,要么是以第i-1個(gè)元素結(jié)尾且和最大的連續(xù)子數(shù)組加上這個(gè)元素,要么是只包含第i個(gè)元素,即sum[i] = max(sum[i-1] + a[i], a[i])。可以通過判斷sum[i-1] + a[i]是否大于a[i]來做選擇,而這實(shí)際上等價(jià)于判斷sum[i-1]是否大于0。由于每次運(yùn)算只需要前一次的結(jié)果,因此并不需要像普通的動(dòng)態(tài)規(guī)劃那樣保留之前所有的計(jì)算結(jié)果,只需要保留上一次的即可,因此算法的時(shí)間和空間復(fù)雜度都很小。
偽代碼
result = a[1] sum = a[1]for i: 2 to LENGTH[a]if sum > 0sum += a[i]elsesum = a[i]if sum > resultresult = sumreturn result參考文章地址1:http://www.cnblogs.com/waytofall/archive/2012/04/10/2439820.html
參考文章地址2:http://www.cnblogs.com/xkfz007/archive/2012/05/17/2506299.html
總結(jié)
以上是生活随笔為你收集整理的算法--三种方法求连续子数组的最大和的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: JQuery中checkbox勾选/取消
- 下一篇: 如何设置input实现同时选中多个文件并