1. 程式人生 > >動態規劃dp經典題目:最大連續子序列和

動態規劃dp經典題目:最大連續子序列和

最大連續子序列和問題

        給定k個整數的序列{N1,N2,...,Nk },其任意連續子序列可表示為{ Ni, Ni+1, ..., Nj },其中 1 <= i <= j <= k。最大連續子序列是所有連續子序中元素和最大的一個,例如給定序列{ -2, 11, -4, 13, -5, -2 },其最大連續子序列為{11,-4,13},最大連續子序列和即為20。

注:為方便起見,如果所有整數均為負數,則最大子序列和為0。

解決這樣一個問題是一個很有趣的過程,我們可以嘗試著從複雜度比較高的演算法一步一步地推出複雜度較低的演算法。

演算法一:

       時間複雜度:O(N^3)

       其程式碼:

  1. int MaxSubSequence(constint A[], int N){  
  2.     int ThisSum,MaxSum,i,j,k;  
  3.     MaxSum = 0;  
  4.     for(i=0;i<N;i++)  
  5.     {  
  6.         for(j=i;j<N;j++)  
  7.         {  
  8.             ThisSum = 0;  
  9.             for(k=i;k<=j;k++)  
  10.             {  
  11.                 ThisSum += A[k];  
  12.             }  
  13.             if(ThisSum > MaxSum)  
  14.                 MaxSum = ThisSum;  
  15.         }  
  16.     }  
  17.     return MaxSum;  
  18. }   
        對於此種演算法,其主要方法是窮舉法,即求出該序列所有子序列的序列和,然後取最大值即可。

演算法二:

       時間複雜度:O(N^2)

       其程式碼:

  1. int MaxSubSequence(constint A[], int N){  
  2.     int ThisSum,MaxSum,i,j;  
  3.     MaxSum = 0;  
  4.     for(i=0;i<N;i++)  
  5.     {  
  6.         ThisSum = 0;  
  7.         for(j=i;j<N;j++)  
  8.         {  
  9.             ThisSum += A[j];  
  10.             if(ThisSum > MaxSum)  
  11.                 MaxSum = ThisSum;  
  12.         }  
  13.     }  
  14.     return MaxSum;  
  15. }  
對於這種方法,歸根究底還是屬於窮舉法,其間接地求出了所有的連續子序列的和,然後取最大值即可。

       那麼,這裡,我們需要對比一下前面兩種演算法,為什麼同樣都是窮舉法,但演算法一的時間複雜度遠高於演算法二的時間複雜度?

       演算法二相較於演算法一,其優化主要體現在減少了很多重複的操作。

       對於A-B-C-D這樣一個序列,

       演算法一在計算連續子序列和的時候,其過程為:

       A-B、A-C、A-D、B-C、B-D、C-D

       而對於演算法二,其過程為:

       A-B、A-C、A-D、B-C、B-D、C-D

       其過程貌似是一樣的,但是演算法一的複雜就在於沒有充分利用前面已經求出的子序列和的值。

       舉個例子,演算法一在求A-D連續子序列和的值時,其過程為A-D = A-B + B-C + C-D;

       而對於演算法二,A-D連續子序列和的求值過程為A-D = A-C+C-D;

       這樣,演算法二充分利用了前面的計算值,這樣就大大減少了計運算元序列和的步驟。

演算法三:遞迴法(分治法)

       時間複雜度:O(NlogN)

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

       其實現程式碼為:

  1. int MaxSubSequence(constint A[],int N)  
  2. {  
  3.     return MaxSubSum(A,0,N-1);  
  4. }  
  5. staticint MaxSubSum(constint A[], int Left, int Right)  
  6. {  
  7.     int MaxLeftSum,MaxRightSum;  
  8.     int MaxLeftBorderSum,MaxRightBorderSum;  
  9.     int LeftBorderSum,RightBorderSum;  
  10.     int Center,i;  
  11.     if(Left == Right)  
  12.     {  
  13.         if(A[Left] > 0)  
  14.             return A[Left];  
  15.         else
  16.             return 0;  
  17.     }  
  18.     Center = (Left + Right)/2;  
  19.     MaxLeftSum = MaxSubSequence(A,Left,Center);  
  20.     MaxRightSum = MaxSubSequence(A,Center+1,Right);  
  21.     MaxLeftBorderSum = 0;  
  22.     LeftBorderSum = 0;  
  23.     for(i = Center;i >= Left;i--)  
  24.     {  
  25.         LeftBorderSum += A[i];  
  26.         if(LeftBorderSum > MaxLeftBorderSum)  
  27.             MaxLeftBorderSum = LeftBorderSum;  
  28.     }  
  29.     MaxRightBorderSum = 0;  
  30.     RightBorderSum = 0;  
  31.     for(i = Center+1;i <= Right;i++)  
  32.     {  
  33.         RightBorderSum += A[i];  
  34.         if(RightBorderSum > MaxRightBorderSum)  
  35.             MaxRightBorderSum = RightBorderSum;  
  36.     }     
  37.     return Max(MaxLeftSum,MaxRightSum,MaxLeftBorderSum + MaxRightBorderSum);  
  38. }   
  39. int Max(int a, int b, int c)  
  40. {  
  41.     if(a>b&&a>c)  
  42.         return a;  
  43.     elseif(b>a&&b>c)  
  44.         return b;  
  45.     else
  46.         return c;   
  47. }  
現在對上面的程式碼進行相關說明:

        Center變數所確定的值將處理序列分割為兩部分,一部分為Center前半部,一部分為Center+1後半部。

       在上文,我們提到,最大連續子序列的出現位置有三種情況。

       對於前兩種情況,我們根據遞迴特性,可以得到:

  1. MaxLeftSum = MaxSubSequence(A,Left,Center);  
  2. MaxRightSum = MaxSubSequence(A,Center+1,Right);  
而對於第三種情況,我們需要先求出前半部包含最後一個元素的最大子序列:
  1. MaxLeftBorderSum = 0;  
  2. LeftBorderSum = 0;  
  3. for(i = Center;i >= Left;i--)  
  4. {  
  5.     LeftBorderSum += A[i];  
  6.     if(LeftBorderSum > MaxLeftBorderSum)  
  7.         MaxLeftBorderSum = LeftBorderSum;  
  8. }  
然後,再求出後半部包含第一個元素的最大子序列:
  1. MaxRightBorderSum = 0;  
  2. RightBorderSum = 0;  
  3. for(i = Center+1;i <= Right;i++)  
  4. {  
  5.     RightBorderSum += A[i];  
  6.     if(RightBorderSum > MaxRightBorderSum)  
  7.         MaxRightBorderSum = RightBorderSum;  
  8. }     
最後,我們只需比較這三種情況所求出的最大連續子序列和,取最大的一個,即可得到需要求解的答案。
  1. return Max(MaxLeftSum,MaxRightSum,MaxLeftBorderSum + MaxRightBorderSum);  
     我們在介紹這個演算法的開始,就已經提到了其時間複雜度,現在做一個推導:

     令T(N)是求解大小為N的最大連續子序列和問題所花費的時間。

     當N==1時,T(1) = 1;

     當N>1時,T(N) = T(N/2) + O(N);

     有數學推導公式,我們可以得到:

     T(N) = NlogN + N =O(NlogN)。

演算法四:動態規劃法

       時間複雜度:O(N)

       終於到了動態規劃的部分了,這麼一步一步走來,感受到了演算法的無窮魅力。那麼如何用動態規劃來處理這個問題?

       首先,我們重溫將一個問題用動態規劃方法處理的準則:

       “最優子結構”、“子問題重疊”、“邊界”和“子問題獨立”。

       在本問題中,我們可以將子序列與其子子序列進行問題分割。

       最後得到的狀態轉移方程為:            

       MaxSum[i] = Max{ MaxSum[i-1] + A[i], A[i]};

       在這裡,我們不必設定陣列MaxSum[]。

程式碼實現:

  1. int MaxSubSequence(constint A[], int N)  
  2. {  
  3.     int ThisSum,MaxSum,j;  
  4.     ThisSum = MaxSum =0;  
  5.     for(j = 0;j < N;j++)  
  6.     {  
  7.         ThisSum += A[j];  
  8.         if(ThisSum > MaxSum)  
  9.             MaxSum = ThisSum;  
  10.         elseif(ThisSum < 0)  
  11.             ThisSum = 0;   
  12.     }  
  13.     return MaxSum;   
  14. }   
        在本程式碼實現中,ThisSum持續更新,同時整個過程,只對資料進行了一次掃描,一旦A[i]被讀入處理,它就不再需要被記憶。(聯機演算法)

小結:

       整個過程是一個思想的選擇問題,從最初的窮舉法,到分治法,再到動態規劃法。演算法設計思想的靈活選擇是處理一個實際問題的關鍵。

部落格已搬:洪學林部落格