[算法]連續子數組最大和


[問題描述]

給定一個數組,求其中一段連續的子數組,使其和最大。

 

[算法及其實現]

據說這是一個面試的經典問題,照例咱們先來分析分析。如果所有數都是非負的,當然整個數組的和就是最大和;所以當數組中存在負數時,這個問題才有意思。(本題考慮整型數,且一切運算結果不超過int的情況)

一切先從暴力開始思考。一個很朴素的思想是,枚舉子數組的起始位置、終止位置,再進行加和。所以這是個O(n^3)的算法,實現如下(c++代碼):

 1 #include <iostream>
 2 #include <cstdio>
 3 using namespace std;
 4 #define MaxN 120
 5 void Read(int *a, int &n)
 6 {
 7     int i;
 8     printf("請輸入一個整數n,表示數組的元素個數。\n");
 9     scanf("%d", &n);
10     printf("請輸入n個整數,用空白符隔開。\n");
11     for(i = 1; i <= n; i++)
12         scanf("%d", &a[i]);
13 }
14 int Get_Max_Sum(int *a, int n) //求a[1...n]的連續子數組最大和 
15 {
16     int i, j, k;
17     int Ret, Sum;
18     Ret = a[1];
19     for(i = 1; i <= n; i++)
20     {
21         for(j = i; j <= n; j++)
22         {
23             Sum = 0;
24             for(k = i; k <= j; k++)
25                 Sum +=a[k];
26             Ret = max(Ret, Sum);
27         }
28     }
29     return Ret;
30 }
31 int main()
32 {
33     int a[MaxN], n;
34     int Ans;
35     Read(a, n);
36     Ans = Get_Max_Sum(a, n);
37     printf("連續子數組最大和 = %d\n", Ans);
38     return 0;
39 }
View Code

 

當然我們是不滿意這樣的復雜度的,所以思考優化。很容易能想到,當已知起始位置和終止位置之后,在進行加和計算是愚蠢而耗時的(因為重復計算太多),能不能把這個操作優化一下呢?答案是肯定的。有一個非常常用的技術——維護前綴和。對於原數組a[]來說,我們維護一個新的數組sum[],其中sum[i]=sigma{a[k]}_{k=1...i},那么a[i]+a[i+1]+……+a[j]=sum[j]-sum[i-1],這樣一個O(n)的操作就變成O(1)的了。整體的復雜度降為O(n^2)。優化后的代碼如下(c++代碼):

 1 #include <iostream>
 2 #include <cstdio>
 3 using namespace std;
 4 #define MaxN 120
 5 void Read(int *a, int &n)
 6 {
 7     int i;
 8     printf("請輸入一個整數n,表示數組的元素個數。\n");
 9     scanf("%d", &n);
10     printf("請輸入n個整數,用空白符隔開。\n");
11     for(i = 1; i <= n; i++)
12         scanf("%d", &a[i]);
13 }
14 int Get_Max_Sum(int *a, int n) //求a[1...n]的連續子數組最大和 
15 {
16     int i, j;
17     int Ret;
18     a[0] = 0;
19     for(i = 2; i <= n; i++)
20         a[i] += a[i - 1]; //這里a[]就變為了維護前綴和的sum[]
21     Ret = a[1];
22     for(i = 1; i <= n; i++)
23         for(j = i; j <= n; j++)
24             Ret = max(Ret, a[j] - a[i - 1]);
25     return Ret;
26 }
27 int main()
28 {
29     int a[MaxN], n;
30     int Ans;
31     Read(a, n);
32     Ans = Get_Max_Sum(a, n);
33     printf("連續子數組最大和 = %d\n", Ans);
34     return 0;
35 }
View Code

 

但從實際考慮,O(n^2)仍然是一個很高的復雜度,我們當然希望復雜度能夠更低。從復雜度上來看,比O(n^2)低的一般來說有兩個量級,O(n lg n)和O(n)。先思考O(n lg n)的算法,出現對數,基本上跟二分會扯上關系,那么這個問題能否用分治來實現呢?非常幸運,是可以的。

從答案出發思考,最后得到的使和最大的一段連續的子數組,會出現在什么地方呢?將原數組從中間一分為二mid=(left+right)/2,要么答案出現在左側,要么答案出現在右側,要么答案橫跨中間位置。前兩種情況將問題轉化為規模更小、本質沒變的子問題了,分治的雛形大致明了了;對於后一種情況,我們很容易證明,最大和=mid向左延伸的最大值+(mid+1)向右延伸的最大值,復雜度為O(n)。所以整體復雜度為O(n lg n)。實現如下(c++代碼):

 1 #include <iostream>
 2 #include <cstdio>
 3 using namespace std;
 4 #define MaxN 120
 5 void Read(int *a, int &n)
 6 {
 7     int i;
 8     printf("請輸入一個整數n,表示數組的元素個數。\n");
 9     scanf("%d", &n);
10     printf("請輸入n個整數,用空白符隔開。\n");
11     for(i = 1; i <= n; i++)
12         scanf("%d", &a[i]);
13 }
14 int Get_Max_Sum(int *a, int L, int R)
15 {
16     int Ret, tmp, mid = L + (R - L) / 2;
17     int Max1, Max2;
18     int i;
19     /*單個點的情況,直接返回*/
20     if(L == R)
21     {
22         Ret = a[L];
23         return Ret;
24     }
25     /*從左側得到答案*/
26     Ret = Get_Max_Sum(a, L, mid);
27     /*從右側得到答案*/
28     tmp = Get_Max_Sum(a, mid + 1, R);
29     Ret = max(Ret, tmp);
30     /*橫跨mid的情況,實際上是指至少包括a[mid]和a[mid+1]*/
31     //左側
32     tmp = 0;
33     Max1 = a[mid];
34     for(i = mid; i >= L; i--)
35     {
36         tmp += a[i];
37         Max1 = max(Max1, tmp);
38     }
39     //右側
40     tmp = 0;
41     Max2 = a[mid + 1];
42     for(i = mid + 1; i <= R; i++)
43     {
44         tmp += a[i];
45         Max2 = max(Max2, tmp);
46     }
47     tmp = Max1 + Max2;
48     Ret = max(Ret, tmp);
49     return Ret;
50 }
51 int main()
52 {
53     int a[MaxN], n;
54     int Ans;
55     Read(a, n);
56     Ans = Get_Max_Sum(a, 1, n);
57     printf("連續子數組最大和 = %d\n", Ans);
58     return 0;
59 }
View Code

 

到現在為止,這樣的復雜度已經足夠讓人感到滿意了。(如果說遞歸的實現讓人擔心速度或者爆棧,可以嘗試手寫棧等,這都是實現技術上的問題,這里筆者認為沒有必要糾結)

但是人都是貪心的,之前我們說了,還有一種更低的復雜度的量級O(n),如果能達到這個級別那多好啊。確實,真的有O(n)的算法。(zyy賣了好久的關子w(゚Д゚)w)

在上一種算法中,有一種情況是當前區間的答案來自於橫跨中間位置mid的子數組,其答案由mid向左延伸最大值和(mid+1)向右延伸最大值組成。由此得到啟發,我們可以從某一個點向左延伸,當然一定會有一個最大值(相當於固定了子數組的末端位置)。乍一看還是一個平方復雜度的算法,實則不然。考慮當前按位置i,從位置i向前延伸得到的最大和保存在sum[i]中,而對於sum[i]的取值,要么將當前元素a[i]跟之前的加在一起為sum[i]=sum[i-1]+a[i],要么從i位置重新開始一個新的子串,即sum[i]=a[i]。sum[i]取何值與sum[i-1]有關,若sum[i-1]<0,顯然應該丟掉前面的東西重新開始,否則選后者。所以最終答案是max{sum[1...n]}。

具體實現時,我們發現sum[]的元素不會重復使用,故只用一個變量就可以了,另外用一個變量來記錄出現過的最大值即可。實現如下(c++代碼):

 1 #include <iostream>
 2 #include <cstdio>
 3 using namespace std;
 4 #define MaxN 120
 5 void Read(int *a, int &n)
 6 {
 7     int i;
 8     printf("請輸入一個整數n,表示數組的元素個數。\n");
 9     scanf("%d", &n);
10     printf("請輸入n個整數,用空白符隔開。\n");
11     for(i = 1; i <= n; i++)
12         scanf("%d", &a[i]);
13 }
14 int Get_Max_Sum(int *a, int L, int R)
15 {
16     int Sum, Ret, i;
17     Ret = Sum = a[L];
18     for(i = L + 1; i <= R; i++)
19     {
20         if(Sum > 0)
21             Sum += a[i];
22         else
23             Sum = a[i];
24         Ret = max(Ret, Sum);
25     }
26     return Ret;
27 }
28 int main()
29 {
30     int a[MaxN], n;
31     int Ans;
32     Read(a, n);
33     Ans = Get_Max_Sum(a, 1, n); //求數組a[1...n]的子數組最大和
34     printf("%d\n", Ans);
35     return 0;
36 }
View Code

 

這個算法確實應該算作DP,給出狀態轉移方程:

sum[0] = 0

sum[i] = max{sum[i - 1], sum[i - 1] + a[i]}

Ans = max{sum[i]}_{i=1...n}

 


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
  © 2014-2022 ITdaan.com 联系我们: