最大子段和問題分析和總結


最大子段和問題(Maximum Interval Sum) 經典的動態規划問題,幾乎所有的算法教材都會提到.本文將分析最大子段和問題的幾種不同效率的解法,以及最大子段和問題的擴展和運用.

一.問題描述

給定長度為n的整數序列,a[1...n], 求[1,n]某個子區間[i , j]使得a[i]+…+a[j]和最大.或者求出最大的這個和.例如(-2,11,-4,13,-5,2)的最大子段和為20,所求子區間為[2,4].

二. 問題分析

1.窮舉法

窮舉應當是每個人都要學會的一種方式,這里實際上是要窮舉所有的[1,n]之間的區間,所以我們用兩重循環,可以很輕易地做到遍歷所有子區間,一個表示起始位置,一個表示終點位置.代碼如下:

?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
int start = 0; //起始位置
int end = 0;  //結束位置
int max = 0;
for ( int i = 1; i <= n; ++i)
{
     for ( int j = i; j <= n;++j)
     {
         int sum = 0;
         for ( int k = i; k <=j; ++k)
             sum += a[k];
         if (sum > max)
         {
            start = i;
            end = j;
            max = sum;
         }
     }
}

這個算法是幾乎所有人都能想到的,它所需要的計算時間是O(n^3).當然,這個代碼還可以做點優化,實際上我們並不需要每次都重新從起始位置求和加到終點位置.可以充分利用之前的計算結果.

或者我們換一種窮舉思路,對於起點 i,我們遍歷所有長度為1,2,…,n-i+1的子區間和,以求得和最大的一個.這樣也遍歷了所有的起點的不同長度的子區間,同時,對於相同起點的不同長度的子區間,可以利用前面的計算結果來計算后面的.

比如,i為起點長度為2的子區間和就等於長度為1的子區間的和+a[i+1]即可,這樣就省掉了一個循環,計算時間復雜度減少到了O(n^2).代碼如下:

?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
int start = 0; //起始位置
int end = 0; //結束位置
int max = 0;
for ( int i = 1; i <= n; ++i)
{
     int sum = 0;
     for ( int j = i; j <= n;++j)
     {
         sum += a[j];
         if (sum > max)
         {
            start = i;
            end = j;
            max = sum;
         }
     }
}

2.分治法

求子區間及最大和,從結構上是非常適合分治法的,因為所有子區間[start, end]只可能有以下三種可能性:

  • 在[1, n/2]這個區域內
  • 在[n/2+1, n]這個區域內
  • 起點位於[1,n/2],終點位於[n/2+1,n]內

以上三種情形的最大者,即為所求. 前兩種情形符合子問題遞歸特性,所以遞歸可以求出. 對於第三種情形,則需要單獨處理. 第三種情形必然包括了n/2和n/2+1兩個位置,這樣就可以利用第二種窮舉的思路求出:

  • 以n/2為終點,往左移動擴張,求出和最大的一個left_max
  • 以n/2+1為起點,往右移動擴張,求出和最大的一個right_max
  • left_max+right_max是第三種情況可能的最大值

參考代碼如下:

?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
int maxInterval( int *a, int left, int right)
  {
     if (right==left)
       return a[left]>0?a[left]:0;
 
     int center = (left+right)/2;
     //左邊區間的最大子段和
     int leftMaxInterval = maxInterval(a,left,center);
     //右邊區間的最大子段和
     int rightMaxInterval= maxInterval(a,center+1,right);
 
     //以下求端點分別位於不同部分的最大子段和
 
     //center開始向左移動
     int sum = 0;
     int left_max = 0;
     for ( int i = center; i >= left; –i)
     {
        sum += a[i];
        if (sum > left_max)
           left_max = sum;
     }
     //center+1開始向右移動
     sum = 0;
     int right_max = 0;
     for ( int i = center+1; i <= right; ++i)
     {
        sum += a[i];
        if (sum > right_max)
          right_max = sum;
     }
     int ret = left_max+right_max;
     if (ret < leftMaxInterval)
         ret = leftMaxInterval;
     if (ret < rightMaxInterval)
         ret = rightMaxInterval;
     return ret;
  }

分治法的難點在於第三種情形的理解,這里應該抓住第三種情形的特點,也就是中間有兩個定點,然后分別往兩個方向擴張,以遍歷所有屬於第三種情形的子區間,求的最大的一個,如果要求得具體的區間,稍微對上述代碼做點修改即可. 分治法的計算時間復雜度為O(nlogn).

3.動態規划法

動態規划的基本原理這里不再贅述,主要討論這個問題的建模過程和子問題結構.時刻記住一個前提,這里是連續的區間

  • 令b[j]表示以位置 j 為終點的所有子區間中和最大的一個
  • 子問題:如j為終點的最大子區間包含了位置j-1,則以j-1為終點的最大子區間必然包括在其中
  • 如果b[j-1] >0, 那么顯然b[j] = b[j-1] + a[j],用之前最大的一個加上a[j]即可,因為a[j]必須包含
  • 如果b[j-1]<=0,那么b[j] = a[j] ,因為既然最大,前面的負數必然不能使你更大

對於這種子問題結構和最優化問題的證明,可以參考算法導論上的“剪切法”,即如果不包括子問題的最優解,把你假設的解粘帖上去,會得出子問題的最優化矛盾.證明如下

  • 令a[x,y]表示a[x]+…+a[y] , y>=x
  • 假設以j為終點的最大子區間 [s, j] 包含了j-1這個位置,以j-1為終點的最大子區間[ r, j-1]並不包含其中
  • 即假設[r,j-1]不是[s,j]的子區間
  • 存在s使得a[s, j-1]+a[j]為以j為終點的最大子段和,這里的 r != s 
  • 由於[r, j -1]是最優解, 所以a[s,j-1]<a[r, j-1],所以a[s,j-1]+a[j]<a[r, j-1]+a[j]
  • 與[s,j]為最優解矛盾.

參考代碼如下:

?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
int max = 0;
int b[n+1];
int start = 0;
int end = 0;
memset (b,0,n+1);
for ( int i = 1; i <= n; ++i)
  {
    if (b[i-1]>0)
    {
      b[i] = b[i-1]+a[i];
    } else {
      b[i] = a[i];
    }
    if (b[i]>max)
      max = b[i];
  }

動態規划法的計算時間復雜度為O(n),是最優的解,這里推薦練習一下UVA507來加深理解. 我以前的題解:

http://www.stackpop.org/blog/html/y2007/371_uva_507.html


 

我們總結一下二維最大子段和問題,以及最大m段和問題.

二維最大子段和問題

二維最大子段和問題又稱為最大子矩陣問題,給定一個m行n列的整數矩陣A,試求A的一子矩陣,使其各元素之和最大

問題分析

子矩陣的概念這里不再贅述,不了解的可以去復習一下線性代數.如下圖所示的

首先明確一件事情,可以通過知道對角線上的兩個元素來確定一個子矩陣,在二維最大子段和問題中,我們要求的是這樣一個子矩陣,如圖中紅框所示,其中 0<= i <= j <=n-1 , 0 <= p <= q <= n-1。因而容易得到一個O(n^4)的枚舉算法.

動態規划法

動態規划法其實就是把二維最大子段和轉化為一維最大子段和問題.
轉化方法:

  • 我們把這個矩陣划分成n個“條”,條的長度為1到m,通過兩個for遍歷所有長度的條
  • 然后,若干個連續的條,就是一個子矩陣了,這樣問題就輕易地轉化為一維最大子段和問題了
  • 通過求所有這種條,起點為i,長度為1到m-i+1的“條”的最大子段和,就可以求出整個矩陣的最大子矩陣了
  • 具體枚舉長條的時候,同一起點的長度,由於“條”的不同長度間可以利用之前的結果
  • 比如令b[k][i][j]表示第k個長“條”區間從i到j的和,那么b[k][i][j+1] = b[k][i][j]+a[j][k]
  • 當然,實際編程的時候,由於之前的結果求完一維最大子段和后,便不需要保存,所以只需要一維數組b即可

參考代碼如下:

?
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
//標准的一維最大子段和
int maxSubInterval( int *data, int n)
{
     int max = 0;
     int b = 0;
     for ( int i = 0; i != n; ++i)
     {
        if (b > 0)
        {
            b = b+data[i];
        } else {
            b = data[i];
        }
        if (b>max)
           max = b;
     }
     return max;
}
 
int maxSubMatrix( int (*a)[10], int m, int n)
{
     int max = 0;
     //b[k]記錄第k個“條”的和
     int *b = new int [n+1];
     for ( int i = 0; i != m; ++i)
     {
         //“條”的和先置為0
         for ( int k = 0; k != n; ++k)
             b[k] = 0;
         //起點為i,長度為j-i+1的條
         //相同起點,長度為k的“條”的和,等於長度為k-1的“條”的和加上當前元素a[j][k]
         for ( int j = i; j != m; ++j)
         {
             for ( int k = 0; k != n; ++k)
                 b[k] += a[j][k];
             int sum = maxSubInterval(b,n);
             if (sum > max)
                 max = sum;
         }
     }
         free (b);
     return max;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM