最大子段和問題(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來加深理解. 我以前的題解:
二維最大子段和問題
二維最大子段和問題又稱為最大子矩陣問題,給定一個m行n列的整數矩陣A,試求A的一子矩陣,使其各元素之和最大
問題分析
子矩陣的概念這里不再贅述,不了解的可以去復習一下線性代數.如下圖所示的
首先明確一件事情,可以通過知道對角線上的兩個元素來確定一個子矩陣,在二維最大子段和問題中,我們要求的是這樣一個子矩陣,如圖中紅框所示,其中 0<= i <= j <=n-1 , 0 <= p <= q <= n-1。因而容易得到一個O(n^4)的枚舉算法.
動態規划法
動態規划法其實就是把二維最大子段和轉化為一維最大子段和問題.
轉化方法:
參考代碼如下:
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;
|