POJ 2479 最大子段和
POJ 2479嚴格來說不是單純的最大子段和,它是一個雙向的最大子段和,爲了弄清雙向的最大子段和就必須弄清楚單向的最大子序和。
單向最大子段和問題如下:
在序列A[1..n](任意一個Ai都是整數,有正有負)中找出一個子序列A[p..q]使得M=Ap+Ap+1+…+Aq-1+Aq最大
例如如下的序列:
1 7 -8 -4 5 10 -9 -5
最大的一段是從{5,10}和爲15
爲使M最大,則A[p]和A[q]必須爲正數,(在q-p>=1情況下)
如果A[p]爲負數,那麼爲使M最大,可以捨棄A[p],只取A[p+1..q]的序列(一個數加上一個負數會變小,要使和最大不如不要這個負數)A[q]必須爲正也是同樣的道理。
再推廣結論,子序列A[p..q]中A[p..k]的子子序列和也必須爲正(p<=k<=q),因爲A[p..k]的和可以看成一個數,它是在最優子序列的最左邊的,若它是負數,那麼A[k+1..q]的序列和將更大,同樣地,A[k+1,.q]必須爲正。
於是就有了這樣的想法:
最優子序列A[p..q]一定是在A[1..n]中的,它的左邊A[1..p-1]一定是負值或不存在,它的右邊A[q+1..n]也一定是負值或不存在。但滿足左邊子子序列是負的,右邊子子序列也是負的的子序列不一定是最優的,我可以比較所有這些序列的和,取其中的最大值。
於是設一個變量i來從左向右遍歷序列,並分別求出A[1..i]中最大子序列的和sum,用max來記錄子序列的最大值。
開始在i累加的過程中,使sum累加A[i]
那麼就有了兩種情況:
1.累加過程中sum<0 例如 A[1]爲負值(此時sum=A[1]),那麼A[1]不可能是最優子序列的左邊部分,如果它是最優子序列的左邊部分,那麼它一定大於零,這裏產生了矛盾,此時就需要重置sum爲0,從A[2]再開始累加,若A[2]也小於零,再重置爲0,從A[3]開始累加,以此類推。
2.累加過程中sum>0 例如A[1]爲正的(此時sum=A[1]),那麼A[1]就很有可能是最優子序列的左邊部分,將sum與max比較,維護max的值。若A[2]也是正的(sum=A[1]+A[2])也需要維護max,及時更新max的值。
在sum>0的情況中又會有兩種情況出現:
要加的下一個值爲負值
a.加上這個負數後sum仍然爲正值,此時sum不會被置零,max值不會更新。(此時sum較之前變小了,max保存的是前面計算的子序列的和)
b.加上這個負數後sum爲負值,此時就要置零sum,重新來計算下一個可能的最優子序列,此時max值保存的是左邊的可能最優子序列的值。
以此進行下去,直到遍歷完整個序列。
sum的值維護了子序列的左邊界,max的值維護了子學列的右邊界,都來保證子序列的和最大。
上述分析可寫成下面的僞代碼:
sum = A[1] //初始化
max = -∞
for i <- 2 to n //2到n遍歷,初始化時已經算上A[1]了
if sum < 0 //sum小於零
sum = A[i] //重置sum
else //sum大於零
sum += A[i] //累加sum
if sum > max //維護max值
max = sum
簡化的C代碼:
sum = 0;
max = -∞;
for(i=1;i<=n;i++) {
sum += A[i];
if(sum<0)
sum = 0;
if(sum>max)
max = sum;
}
細節的地方:
有些地方往往沒有說序列有正有負,可能全是負數,此時就會出現一個麻煩:上面的代碼執行完後,最大爲0.
有可能序列的第一個值就是負值,這時執行代碼時,sum不是被處理成了0就是被處理成了第二值,導致max值在i=1的時候不準確,這一點在需要打表的地方尤爲重要,然而POJ 2479正是在這裏爲難大家。
那麼應該怎麼改呢?
既然只是第一個值的問題,那麼就可以對第一個值進行特殊處理,令max=A[1],就能解決這個問題
於是改過的代碼就是:
sum = 0;
max = A[1];
for(i=1;i<=n;i++) {
sum += A[i];
if(sum>max) //一定要先比較
max = sum;
if(sum<0)
sum = 0;
}
來看一看上下兩段代碼處理這兩個序列的不同結果:
1.
-3 4 6 1 -3
未注意細節的 | 注意了細節的 | ||
sum | max | sum | max |
-3->0 | -∞ | -3->0 | -3 |
4 | 0->4 | 4 | 0->4 |
10 | 4->10 | 10 | 4->10 |
11 | 10->11 | 11 | 10->11 |
9 | 11 | 9 | 11 |
對結果無太大影響,最終都是11
2.
-2 -3 -5 -1 -4
未注意細節的 | 注意了細節的 | ||
sum | max | sum | max |
-2->0 | 0 | -2->0 | -2 |
-3->0 | 0 | -3->0 | -2 |
-5->0 | 0 | -5->0 | -2 |
-1->0 | 0 | -1->0 | -1 |
-4->0 | 0 | -4->0 | -1 |
如果碰上了全負的序列,未注意細節的算法就是一場災難。
POJ 2479中就需要注意這樣的細節,雙向的最大子序和問題關鍵在於分割,可將序列從A[k]處分開成A[1..k],A[k+1..n](1<k<n)的兩個子序列,在分別對A[1.k],A[k+1..n]求最大子序和,再把兩者相加和總的最大值比較。
在實際的過程中,先會遍歷k從1到n-1,求出A[1.k]的最大子序和,打一個表比方說叫left[k],再遍歷k從n到2,求出A[k..n]的最大子序和,再打一個表比方叫right[k],再遍歷k從1到n-1,求出最大的left[k]+right[k+1]
轉移方程爲:
res = max{left[k]+right[k+1],1<=k<=n-1}
AC代碼:
#include <stdio.h>
int data[50005];//數據
int left[50005];//left[i]表示從1到i的最大子序和值
int right[50005];//right[i]表示從i到n的最大子序和值
int main()
{
int T,n,i,j,sum,max=-(1<<30);
scanf("%d",&T);
for(i=0;i<T;i++) {//對每一組數據
scanf("%d",&n);
for(j=1;j<=n;j++) {//讀入數據
scanf("%d",&data[j]);
}
//-----------計算左邊---------------
sum = data[1];
max = data[1];
left[1] = data[1];
for(j=2;j<=n;j++) {
if(sum < 0)
sum = data[j];
else
sum += data[j];
if(sum > max) {
max = sum;
}
left[j] = max;
}
//--------------------------------
//-----------計算右邊--------------
max = data[n];
sum = data[n];
right[n] = data[n];
for(j=n-1;j>=1;j--) {
if(sum < 0)
sum = data[j];
else
sum += data[j];
if(sum > max) {
max = sum;
}
right[j] = max;
}
//-------------------------------
max = -(1<<30);
for(j=1;j<=n-1;j++) {//尋找最大值
if(left[j]+right[j+1]>max)
max = left[j]+right[j+1];
}
printf("%d\n",max);
}
return 0;
}