1. 問題描述
輸入一個整形數組,求數組中連續的子數組使其和最大。比如,數組x
應該返回 x[2..6]的和187.
2. 問題解決
我們很自然地能想到窮舉的辦法,窮舉所有的子數組的之和,找出最大值。
窮舉法
i, j的for循環表示x[i..j],k的for循環用來計算x[i..j]之和。
maxsofar = 0
for i = [0, n)
for j = [i, n)
sum = 0
for k = [i, j]
sum += x[k]
/* sum is sum of x[i..j] */
maxsofar = max(maxsofar, sum)
有三層循環,窮舉法的時間復雜度為\(O(n^3)\)
對窮舉法的改進1
我們注意到x[i..j]之和 = x[i..j-1]之和 + x[j]
,因此在j的for循環中,可直接求出sum。
maxsofar = 0
for i = [0, n)
sum = 0
for j = [i, n)
sum += x[j]
/* sum is sum of x[i..j] */
maxsofar = max(maxsofar, sum)
顯然,改進之后的時間復雜度變為\(O(n^2)\)。
對窮舉法的改進2
在計算fibonacci數時,應該還有印象:用一個累加數組(cumulative array)記錄前面n-1次之和,計算當前時只需加上n即可。同樣地,我們用累加數組cumarr記錄:cumarr[i] = x[0] + . . . +x[i]
,那么x [i.. j]之和 = cumarr[j] -cumarr[i - 1]
。
cumarr[-1] = 0
for i = [0, n)
cumarr[i] = cumarr[i-1] + x[i]
maxsofar = 0
for i = [0, n)
for j = [i, n)
sum = cumarr[j] - cumarr[i-1]
/* sum is sum of x[i..j] */
maxsofar = max(maxsofar, sum)
時間復雜度依然為\(O(n^2)\)。
分治法
所謂分治法,是指將一個問題分解為兩個子問題,然后分而解決之。具體步驟如下:
-
先將數組分為兩個等長的子數組a, b;
-
分別求出兩個數組a,b的連續子數組之和;
-
還有一種情況(容易忽略):有可能最大和的子數組跨越兩個數組;
-
最后比較\(m_a\), \(m_b\), \(m_c\),取最大即可。
在計算\(m_c\)時,注意:\(m_c\)必定包含總區間的中間元素,因此求\(m_c\)等價於從中間元素開始往左累加的最大值 + 從中間元素開始往右累加的最大值
。
float maxsum3(l, u)
if (l > u) /* zero elements */
return 0
if (l == u) /* one element */
return max(0, x[l])
m = (l + u) / 2
/* find max crossing to left */
lmax = sum = 0
for (i = m; i >= l; i--)
sum += x[i]
lmax = max(lmax, sum)
/* find max crossing to right */
rmax = sum = 0
for i = (m, u]
sum += x[i]
rmax = max(rmax, sum)
return max(lmax+rmax,
maxsum3(l, m),
maxsum3(m+1, u));
容易證明,時間復雜度為\(O(n*log \ n)\)。
動態規划
Kadane算法又被稱為掃描法,為動態規划(dynamic programming)的一個典型應用。我們用DP來解決最大子數組和問題:對於數組\(a\),用\(c_i\)標記子數組\(a[0..i]\)的最大和,那么則有
子數組最大和即為\(\max c_i\)。Kadane算法比上面DP更進一步,不需要用一個數組來記錄中間子數組和。通過觀察容易得到:若\(c_{i-1} \leq 0\),則\(c_i = a_i\)。用\(e\)表示以當前為結束的子數組的最大和,以替代數組\(c\);那么
Python實現如下:
def max_subarray(A):
max_ending_here = max_so_far = A[0]
for x in A[1:]:
max_ending_here = max(x, max_ending_here + x)
max_so_far = max(max_so_far, max_ending_here)
return max_so_far
max_ending_here
對應於標記\(e\),max_so_far
記錄已掃描到的子數組的最大和。Kadane算法只掃描了一遍數組,因此時間復雜度為\(O(n)\).
3. 參考資料
[1] Jon Bentley, Programming Pearls.
[2] GeeksforGeeks, Largest Sum Contiguous Subarray.