1. 程式人生 > >最大連續子序列和:遞迴和動態規劃

最大連續子序列和:遞迴和動態規劃

問題描述:

給定一個整數序列,a0, a1, a2, …… , an(項可以為負數),求其中最大的子序列和。如果所有整數都是負數,那麼最大子序列和為0;

方法一:

用了三層迴圈,因為要找到這個子序列,肯定是需要起點和終點的,所以第一層迴圈確定起點,第二層迴圈確定終點,第三層迴圈在起點和終點之間遍歷。時間複雜度:O(N^3)

package secondCHA;

public class maxSUM1 {

	static int MaxSubSequence( int A[]){  
	    int ThisSum,MaxSum,i,j,k;  
	    int N=A.length;
	    MaxSum = 0;  
	    for(i=0;i<N;i++)  
	    {  
	        for(j=i;j<N;j++)  
	        {  
	            ThisSum = 0;  
	            for(k=i;k<=j;k++)  
	            {  
	                ThisSum += A[k];  
	            }  
	            if(ThisSum > MaxSum)  
	                MaxSum = ThisSum;  
	        }  
	    }  
	    return MaxSum;  
	}   
	public static void main(String[] args) {
		// TODO Auto-generated method stub
         int[] a={4,-3,5,-2,-1,2,6,-2};
         System.out.println(MaxSubSequence(a));
	}

}

方法二:

只用了兩層迴圈,相當於是第一層用來找到每個可能的子序列的起點,而第二個迴圈直接從第一層確定的起點往陣列結尾遍歷,在遍歷的過程中同時也計算序列值。前兩種方法都是窮舉法,都是把所有可能的子序列都找出來,然後計算值再和最大值比較。

package secondCHA;

public class maxSUM2 {
	static int MaxSubSequence( int A[]){  
		int N=A.length;
	    int ThisSum,MaxSum,i,j;  
	    MaxSum = 0;  
	    for(i=0;i<N;i++)  
	    {  
	        ThisSum = 0;  
	        for(j=i;j<N;j++)  
	        {  
	            ThisSum += A[j];  
	            if(ThisSum > MaxSum)  
	                MaxSum = ThisSum;  
	        }  
	    }  
	    return MaxSum;  
	}  
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		  int[] a={4,-3,5,-2,-1,2,6,-2};
	      System.out.println(MaxSubSequence(a));
	}

}

方法三:

用到了遞迴,用到了這樣一種思想,當然要我肯定想不到,純屬借鑑了:

對於一數字序列,其最大連續子序列和對應的子序列可能出現在三個地方。或是整個出現在輸入資料的前半部(左),或是整個出現在輸入資料的後半部(右),或是跨越輸入資料的中部從而佔據左右兩半部分。前兩種情況可以通過遞迴求解,第三種情況可以通過求出前半部分的最大和(包含前半部分的最後一個元素)以及後半部分的最大和(包含後半部分的第一個元素)而得到,然後將這兩個和加在一起即可。

主要從遞迴的角度分析,把問題分為了小問題,處理的過程都一樣,嗯,所以就用遞迴了。然後遞迴重要在於理解兩步,過程和終點。

首先分析終點,子問題一步步被劃分,最終,明顯看出,原始序列最終會被分為一個長度為1的子序列

(一個大於1的數一直除,肯定會到1的),把長度為1的子序列當成原問題,那麼根據題意,如果是正數,就要,如果是負數,不如不要,就為0。所以,遞迴的終點設定得正確。

然後分析過程,分析遞迴的過程,在debug的過程中,你能發現,遞迴最開始是一直拆分子問題,直到到達遞迴終點再一層一層倒著返回呼叫結果。可以把遞迴過程看成一個二叉樹,所以遞迴終點就是此遞迴過程二叉樹的最後一層,而在以下程式中, MaxLeftSum = MaxSubSum(A,Left,Center); 這句就是左孩子,MaxRightSum = MaxSubSum(A,Center+1,Right);就是右孩子。而左孩子語句先執行,所以過程最開始一定是往左下方向直到第一個終點,觀察程式中的陣列,第一個終點之前前肯定是傳遞了一個{4,-3}的陣列進去,再一分為二,然後傳遞{4}進去返回4,傳遞-3進去,返回0,再把他們左右序列最大值(因為兩序列各只有一個,且這裡必須包含左序列最右和右序列最左,所以這裡就是這倆序列自身)加起來,為1。分析了這個問題的規模最小的問題,發現了這個遞迴的過程確實沒有毛病。

理解了終點和過程,那麼這個遞迴你也就理解了。

但是首先你得先想到這種思想,你才有前提寫出這遞迴程式碼。

package secondCHA;

public class maxSUM3 {
   
	static int MaxSubSum( int A[], int Left, int Right)  
	{  
	    int MaxLeftSum,MaxRightSum;  
	    int MaxLeftBorderSum,MaxRightBorderSum;  
	    int LeftBorderSum,RightBorderSum;  
	    int Center,i;  
	      
	    if(Left == Right)  
	    {  
	        if(A[Left] > 0)  
	            return A[Left];  
	        else  
	            return 0;  
	    }  
	      
	    Center = (Left + Right)/2;  
	    MaxLeftSum = MaxSubSum(A,Left,Center);  
	    MaxRightSum = MaxSubSum(A,Center+1,Right);  
	      
	    MaxLeftBorderSum = 0;  
	    LeftBorderSum = 0;  
	    for(i = Center;i >= Left;i--)  
	    {  
	        LeftBorderSum += A[i];  
	        if(LeftBorderSum > MaxLeftBorderSum)  
	            MaxLeftBorderSum = LeftBorderSum;  
	    }  
	      
	    MaxRightBorderSum = 0;  
	    RightBorderSum = 0;  
	    for(i = Center+1;i <= Right;i++)  
	    {  
	        RightBorderSum += A[i];  
	        if(RightBorderSum > MaxRightBorderSum)  
	            MaxRightBorderSum = RightBorderSum;  
	    }     
	      
	    return Max(MaxLeftSum,MaxRightSum,MaxLeftBorderSum + MaxRightBorderSum);  
	}   
	  
	static int Max(int a, int b, int c)  
	{  
	    if(a>b&&a>c)  
	        return a;  
	    else if(b>a&&b>c)  
	        return b;  
	    else  
	        return c;   
	}  
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		 int[] a={4,-3,5,-2,-1,2,6,-2};
		 int left=0;
		 int right=a.length-1;
		 System.out.println(MaxSubSum(a,left,right));
	}

}

方法四:

這種方法應該也算動態規劃吧,畢竟把遞迴變成了非遞迴(但我這麼理解不對,非遞迴不等於動態規劃)。演算法書說這是聯機演算法,特點是僅需要常量空間和線性時間執行。

重點還是在於理解算

1.只有一個for迴圈,迴圈裡的j變數理解成序列的起點,但是這個起點有時會跳過若干數,噹噹前計算的序列a[i]到a[j]的和ThisSum一旦為負時,則ThisSum歸零,因為for迴圈的迴圈變數加1,則從a[j+1]開始加和,即起點跳到了負數和的子序列的下一個數字。

2.理解這樣的事實,一個子序列必然是以正數開頭的,因為如果以負數開頭,那麼去掉這個子序列,那得到一個更優解。

3.還有這麼個事實,一個子序列,如果一旦他的前若干個數字組成的新的個數更少的子序列的和為負數,那麼去掉這個子序列,便能得到了一個更優解。

4.MaxSum若遇到更大的和則更新,若遇到更小的和,則不更新(沒有動作)。ThisSum則充分利用了第二個事實,當某個時刻子序列為負數,則歸零即相當於去掉了這子序列所有數字,從下一個數字從新開始加和。

5.其實第二個事實,不是那麼好理解,關鍵在於,去掉是當一旦出現了和為負數(之前的和都是>=0的,但是加了下一個數後和就為負數了)的子序列,就去掉這個子序列,而且這時,子序列最後一個數肯定為負數,因為是沒加最後一個數時和還為正數,但一加上了就變成負數,所以這最後一個數肯定是負數。假設這個負數為a[i],現在歸零了,從a[i+1]開始加和了,思考以a[i]為結果的剛才去掉的這個序列(這裡指去掉的子序列的任意數到這個序列的最後一個數,的這些所有可能的子序列,因為是要求連續,所以是任意到結尾的某個序列考慮是否該加回去)是不是不該去掉呢,為了證明,我們可以從去掉的序列的最後一個a[i](第一種可能是隻有最後那個數)開始加回去,你會加上a[i],因為a[i]是負數,所以加了和反而變小了,不該加。如果加上a[i-1]和a[i]呢,a[i-1]和a[i]的和肯定也是負數,這個負數可能比a[i]小,也可能大但肯定是個負數(舉例:1,2,-5和2,-1,-5),前者是大一點的負數,後者是小一點的負數,但是必然的,加回去的序列和肯定還是負數,共同點是-5是造成序列和變成負數的那個第一個數,同理可得從任意處開始到結尾的序列的和必須也是負數,即是所有這些可能的序列都不應該加回去。——所以,一旦序列和為負數,那麼這個序列就應該去掉了。(不會出現這種情況,比如去掉這個子序列2,1,-5,1(和為-1),因為當i移動到-5時,就已經去掉了,也證明了去掉的子序列的最後一個數是負數)

package secondCHA;

public class maxSUM4 {
	static int MaxSubSequence( int A[])  
	{  
		int N=A.length;
	    int ThisSum,MaxSum,j;  
	    ThisSum = MaxSum =0;  
	    for(j = 0;j < N;j++)  
	    {  
	        ThisSum += A[j];  
	          
	        if(ThisSum > MaxSum)  
	            MaxSum = ThisSum;  
	        else if(ThisSum < 0)  
	            ThisSum = 0;   
	    }  
	    return MaxSum;   
	}   
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		  int[] a={4,-3,5,-2,-1,2,6,-2};
	      System.out.println(MaxSubSequence(a));
	}

}