1. 程式人生 > >求某個數組裡連續子陣列最大和的幾個演算法

求某個數組裡連續子陣列最大和的幾個演算法

注意:這裡的陣列元素,有可能全為負。這樣,所謂的:

int find_max_array(const vector<int> &a)  
{  
    int max_sum = 0;  
    int this_sum = 0;  
    for (int i = 0; i < a.size(); ++i)  
    {  
        this_sum += a[i];  
        if (this_sum > max_sum)  
            max_sum = this_sum;  
        else if (this_sum < 0)  
            this_sum = 0;  
    }  
    return max_sum;  
}  
這種方法就行不通了。下面是幾種無論正負的通用方法。

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(n3)

對窮舉法的改進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

(n2)

對窮舉法的改進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(n2)

分治法

所謂分治法,是指將一個問題分解為兩個子問題,然後分而解決之。具體步驟如下:

  • 先將陣列分為兩個等長的子陣列a, b;

  • 分別求出兩個陣列a,b的連續子陣列之和;

  • 還有一種情況(容易忽略):有可能最大和的子陣列跨越兩個陣列;

  • 最後比較mambmc,取最大即可。

在計算mc時,注意:mc必定包含總區間的中間元素,因此求mc等價於從中間元素開始往左累加的最大值 + 從中間元素開始往右累加的最大值

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(nlogn)

動態規劃

Kadane演算法又被稱為掃描法,為動態規劃(dynamic programming)的一個典型應用。我們用DP來解決最大子陣列和問題:對於陣列a,用ci標記子陣列a[0..i]的最大和,那麼則有

ci=max{ai,ci1+ai}

子陣列最大和即為maxci。Kadane演算法比上面DP更進一步,不需要用一個數組來記錄中間子陣列和。通過觀察容易得到:若ci10,則ci=ai。用e表示以當前為結束的子陣列的最大和,以替代陣列c;那麼

e=max{ai,e+ai}

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對應於標記emax_so_far記錄已掃描到的子陣列的最大和。Kadane演算法只掃描了一遍陣列,因此時間複雜度為O(n).

3. 參考資料

[1] Jon Bentley, Programming Pearls.
[2] GeeksforGeeks, Largest Sum Contiguous Subarray.