1. 程式人生 > >演算法--三種方法求連續子陣列的最大和

演算法--三種方法求連續子陣列的最大和

  這是一道考的爛的不能再爛的題目,但是依然有很多公司樂於將這樣的題目作為筆試或面試題,足見其經典。

題目描述:

輸入一個整形陣列,數組裡有正數也有負數。
陣列中連續的一個或多個整陣列成一個子陣列,每個子陣列都有一個和。
求所有子陣列的和的最大值。要求時間複雜度為O(n)。

例如輸入的陣列為1, -2, 3, 10, -4, 7, 2, -5,和最大的子陣列為3, 10, -4, 7, 2,
因此輸出為該子陣列的和18。

下面按照時間複雜度逐步優化的順序依次給出這三種演算法。

暴力求解法

該方法的思想非常簡單,先找出從第1個元素開始的最大子陣列,而後再從第2個元素開始找出從第2個元素開始的最大子陣列,依次類推,比較得出最大的子陣列。實現程式碼如下:

/*
常規方法,時間複雜度O(n*n)
先從第一個元素開始向後累加,
每次累加後與之前的和比較,保留最大值,
再從第二個元素開始向後累加,以此類推。
*/
int MaxSubSum1(int *arr,int len)
{
    int i,j;
    int MaxSum = 0;
    //每次開始累加的起始位置的迴圈
    for(i=0;i<len;i++)
    {
        int CurSum = 0;
        //向後累加的迴圈
        for(j=i;j<len;j++)
        {
            CurSum += arr[j];
            if
(CurSum > MaxSum) MaxSum = CurSum; } } return MaxSum; }

很明顯地可以看出,該方法的時間複雜度為O(n*n)。

分治求解法

解釋一

考慮將陣列從中間分為兩個子陣列,則最大子陣列必然出現在以下三種情況之一:

1、完全位於左邊的陣列中。

2、完全位於右邊的陣列中。

3、跨越中點,包含左右陣列中靠近中點的部分。

遞迴將左右子陣列再分別分成兩個陣列,直到子陣列中只含有一個元素,退出每層遞迴前,返回上面三種情況中的最大值。實現程式碼如下:
/*
求三個數中的最大值
*/
int Max3(int a,int b,int c) { int Max = a; if(b > Max) Max = b; if(c > Max) Max = c; return Max; } /* 次優演算法,採用分治策略 */ int MaxSubSum2(int *arr,int left,int right) { int MaxLeftSum,MaxRightSum; //左右邊的最大和 int MaxLeftBorderSum,MaxRightBorderSum; //含中間邊界的左右部分最大和 int LeftBorderSum,RightBorderSum; //含中間邊界的左右部分當前和 int i,center; //遞迴到最後的基本情況 if(left == right) if(arr[left]>0) return arr[left]; else return 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); } /* 將分支策略實現的演算法封裝起來 */ int MaxSubSum2_1(int *arr,int len) { return MaxSubSum2(arr,0,len-1); }

設該演算法的時間複雜度為T(n),則:

T(n)= 2T(n/2)+ O(n),且T(1)= 1。

逐步遞推得到時間複雜度T(n)= O(nlogn)。

另一種通俗解釋二

這裡再介紹一種更高效的演算法,時間複雜度為O(nlogn)。這是個分治的思想,解決複雜問題我們經常使用的一種思維方法——分而治之。
而對於此題,我們把陣列A[1..n]分成兩個相等大小的塊:

A[1..n/2]和A[n/2+1..n],最大的子陣列只可能出現在三種情況:

A[1..n]的最大子陣列和A[1..n/2]最大子陣列相同;
A[1..n]的最大子陣列和A[n/2+1..n]最大子陣列相同;
A[1..n]的最大子陣列跨過A[1..n/2]和A[n/2+1..n]

前兩種情況的求法和整體的求法是一樣的,因此遞迴求得。

第三種,我們可以採取的方法也比較簡單,沿著第n/2向左搜尋,直到左邊界,找到最大的和maxleft,以及沿著第n/2+1向右搜尋找到最大和maxright,那麼總的最大和就是maxleft+maxright。而陣列A的最大子陣列和就是這三種情況中最大的一個。

虛擬碼

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]; 
}

線性時間演算法

該演算法在每次元素累加和小於0時,從下一個元素重新開始累加。實現程式碼如下:

/*
最優方法,時間複雜度O(n)
和最大的子序列的第一個元素肯定是正數
因為元素有正有負,因此子序列的最大和一定大於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;
        //如果累加和出現小於0的情況,
        //則和最大的子序列肯定不可能包含前面的元素,
        //這時將累加和置0,從下個元素重新開始累加
        if(CurSum < 0)
            CurSum = 0;
    }
    return MaxSum;
}

顯然,該演算法的時間複雜度O(n)。該演算法理解起來應該不難,但是要想出來可就不容易了。另外,該演算法的一個附帶的有點是:它只對資料進行一次掃描,一旦元素被讀入並被處理,它就不再需要被記憶。因此,如果陣列在磁碟或磁帶上,他就可以被順序讀入,在主存中不必儲存陣列的任何部分。不僅如此,在任意時刻,該演算法都能對它已經讀入的資料給出最大子陣列(另外兩種演算法不具有這種特性)。具有這種特性的演算法叫做聯機演算法。

掃描法

掃描演算法實際上就是上面的線性時間演算法,這裡是轉載的自另一個作者,思路邏輯可能更加嚴謹一些,分析也更加精確。

當我們加上一個正數時,和會增加;當我們加上一個負數時,和會減少。如果當前得到的和是個負數,那麼這個和在接下來的累加中應該拋棄並重新清零,不然的話這個負數將會減少接下來的和。實現:

(後加注:這裡提到的掃描法存在一個問題就是如果最大欄位和小於0則演算法沒法給出正確答案。其實這個問題用動態規劃就好,這裡的掃描法其實真的不是個好方法,只是因為很有名所以還是粘出來了)

//[email protected] 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兩個大情況進行分析,於是得證。

這個演算法的證明有些複雜,現在感覺應該不會錯,至少思路是對的,誰幫著在表達上優化下吧。:-)

動態規劃

設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

相關推薦

演算法--方法連續陣列

這是一道考的爛的不能再爛的題目,但是依然有很多公司樂於將這樣的題目作為筆試或面試題,足見其經典。 題目描述: 輸入一個整形陣列,數組裡有正數也有負數。 陣列中連續的一個或多個整陣列成一個子陣列,每個子陣列都有一個和。 求所有子陣列的和的最大值。要求

演算法拾遺】方法連續陣列

    這是一道考的爛的不能再爛的題目,但是依然有很多公司樂於將這樣的題目作為筆試或面試題,足見其經典。    問題是這樣的:一個整數陣列中的元素有正有負,在該陣列中找出一個連續子陣列,要求該連續子陣列

動態規劃--目標值問題、找零錢問題以及連續陣列 --java

1、動態規劃一般可分為線性動規,區域動規,樹形動規,揹包動規四類。 舉例: 線性動規:攔截導彈,合唱隊形,挖地雷,建學校,劍客決鬥等; 區域動規:石子合併, 加分二叉樹,統計單詞個數,炮兵佈陣等; 樹形動規:貪吃的九頭龍,二分查詢樹,聚會的歡樂,數字三角形等;

python實現連續陣列

問題描述:例如:[6,-3,-2,7,-15,1,2,2]求連續子陣列中的最大和,此陣列中最大和為8,從arr[0]到arr[3]。其餘位置都比這個要小。最大連續子陣列的特點:(1)第一個不為負數(2)如果前面數的累加加上當前數小於當前數,說明這次累加對總體的結果是無效的;如

某個數組裡連續陣列的幾個演算法

注意:這裡的陣列元素,有可能全為負。這樣,所謂的: int find_max_array(const vector<int> &a) { int max_sum = 0; int this_sum = 0; fo

連續陣列O(n)兩解法:雙指標 動態規劃

題目描述 HZ偶爾會拿些專業問題來忽悠那些非計算機專業的同學。今天測試組開完會後,他又發話了:在古老的一維模式識別中,常常需要計算連續子向量的最大和,當向量全為正數的時候,問題很好解決。但是,如果向量中包含負數,是否應該包含某個負數,並期望旁邊的正數會彌補它呢?例如:{6,-3,-2,7

常見演算法 - 連續陣列

public class Solution { public int FindGreatestSumOfSubArray(int[] array) { if(array.length == 0){ return 0; }

動態規劃演算法連續陣列,O(N)時間複雜度O(1)空間複雜度) 【更新於:2018-05-13】

這個題目最早在13年阿里筆試出現,直到前兩天面試另一家電商又出現,哎,欠的都是要還的。 這個問題的思路如下:一維陣列的下標連續的元素構成連續子陣列,求所有連續子陣列中和最大的那個子陣列。 解析:2018-11-08 1 首先這個問題要轉化為Q(n)的問題,對於Q(n)的

劍指offer:(31)時間效率 :連續陣列

package jianzhioffer; public class Solution31 { //動態規劃:就是將中間值儲存下來 public static int FindGreatestSumOfSubArray(int[] array) { if (array == null

【劍指offer】連續陣列

題目:在古老的一維模式識別中,常常需要計算連續子向量的最大和,當向量全為正數的時候,問題很好解決。但是,如果向量中包含負數,是否應該包含某個負數,並期望旁邊的正數會彌補它呢?例如:{6,-3,-2,7,-15,1,2,2},連續子向量的最大和為8(從第0個開始,到第3個為止)。給一個數組,返回它的最大連續子序

牛客網 《劍指Offer》程式設計 30.連續陣列

題目描述 HZ偶爾會拿些專業問題來忽悠那些非計算機專業的同學。今天測試組開完會後,他又發話了:在古老的一維模式識別中,常常需要計算連續子向量的最大和,當向量全為正數的時候,問題很好解決。但是,如果向量中包含負數,是否應該包含某個負數,並期望旁邊的正數會彌補它呢?例如:{6,

陣列連續陣列乘積

題目:給定一個數組,要求其連續子陣列的最大和。如陣列為{6,-3,-2,7,-15,1,2,2},連續子陣列的最大和為8(從第0個開始,到第3個為止) 解法1:首先最容易想到的便是利用列舉的方法,枚舉出所有可能大小的連續子陣列的和,然後選出其中最大的一個。即從連續子陣列的大

連續陣列

題目描述: 輸入一個整形陣列,數組裡有正數也有負數。 陣列中連續的一個或多個整陣列成一個子陣列,每個子陣列都有一個和。 求所有子陣列的和的最大值。要求時間複雜度為O(n)。 例如輸入的陣列為1, -2, 3, 10, -4, 7, 2, -5,和最大的子陣列為3, 10,

連續陣列問題

1. 問題描述 輸入一個整形陣列,求陣列中連續的子陣列使其和最大。比如,陣列x 應該返回 x[2..6]的和187. 2. 問題解決 我們很自然地能想到窮舉的辦法,窮舉所有的子陣列的之和,找出最大值。 窮舉法 i, j的for迴圈表示x[i..j],k的for

姿勢拿下連續序列問題,附虛擬碼(以HDU 1003 1231為例)

問題描述:       連續子序列最大和,其實就是求一個序列中連續的子序列中元素和最大的那個。       比如例如給定序列:            { -2, 11, -4, 13, -5, -2 }         其最大連續子序列為{ 11, -4, 13 },最大和

演算法陣列(一)】陣列的解決方法詳解

題目: 輸入一個整形陣列,數組裡有正數也有負數。 陣列中連續的一個或多個整陣列成一個子陣列,每個子陣列都有一個和。 求所有子陣列的和的最大值。 例如輸入的陣列為1, -2, 3, 10, -4, 7, 2, -5,和最大的子陣列為3, 10, -4, 7, 2, 因此

從暴力求解到動態規劃—— 7 方法求解連續陣列問題(python實現)

問題描述 已知一個數組 a[n],裡面存放著浮點數,可能是正數、負數或0。求它的所有連續子陣列中的最大和。 連續子陣列:指的是陣列的一個連續切片,即可以表示為 a[i:j],0≤i≤j<n。 連續子陣列的和:比如連續子陣列為 a[i:j] ,則和為

連續序列問題的四經典解答

問題描述    給定(可能是負的)整數序列A1, A2,...,AN, 尋找(並標識)使Sum(Ak)(k >=i, k <= j)的值最大的序列。如果所有的整數都是負的,那麼連續子序列的最大和是那個最大的負數項。最好給出給出最大和連續子序列!! 1 暴力破

迴圈陣列陣列

一。實驗要求 1.輸入一個整型陣列,數組裡有正數也有負數,陣列中一個或多個整陣列成一個整陣列,每個子陣列都有一個和。 2.陣列可以首位相連,允許A【i-1】,....,A[n-2],A[0]........A[j-1]和最大 3.返回最大子陣列的位置,求最大子陣列的和. 二、實驗思路 迴圈陣列,也就

連續序列問題---python實現

連續子序列最大和問題—python實現 問題:連續子序列最大和 給定一個數字序列[A1A2A3…An],求i,j(1<=i<=j<=n)使得Ai…Aj和最大, 輸出這個最大和(連續大子序列最大和) 例如: 輸入: L=[-2 ,6,