POJ 2479 最大子段和

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


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章