最大連續子序列和


  本文主要總結最大連續子序列和的問題及其歷史,這個題目在很多公司的面試中出現,編程之美也有講述。本文主要介紹一維的情形,環形和二維的擴展在下一篇講述。

  最大連續子序列和最早是在編程珠璣講述,這個問題最初由布朗大學的統計學家UIF Grenander在處理圖片時提出的,當時是處理二維數組的子數組,為了簡化問題,才先提出一維數組的解法。

  問題定義:對一個有n個元素的數組,求最大的連續子數組的和。數組的元素必然有正數也有負數才有意義,如果全是正數,那最大的子數組就是本身;如果全部為負數,那最大子數組就是空數組。例如下面的數組,其最大子數組序列和為187,子數組為X[2,..,6]:

31 -41 59 26 -53 58 97 -93 -23 84

這個問題有很強的代表性,有很多實現方法,復雜度從O(n^3)到O(n)都用,另外這個問題可以擴展到二維數組,甚至擴展到環,我們來逐一討論,步步深入。

1、最直接的方法O(n^3)

  每個問題往往都有一個最直接而魯莽的方法,雖然這樣的方法不是我們最終想要的,但直接有效的方法能啟發我們進一步優化。這里最直接的方法就是遍歷所有的子數組,比較每一個子數組的和即得到最大的子數組和。實現代碼如下,只需要記錄一個當前最大值即可。  

1 def maxSeqSum1(data): 2     n = len(data) 3     maxsofar = 0 4     for i in xrange(n): 5         for j in xrange(i,n): 6             s = sum(data[i:j+1]) 7             if s > maxsofar: 8                 maxsofar = s 9     return maxsofar

   這個算法的時間復雜度是O(n^3),雖然看上去只要兩層循環,但最里面的求和並不是常量時間。最外層循環n次,第二層最多循環n次,里面的求和長度最大也為n,因此整個計算的復雜度是O(n^3)。 

2、小小的改進O(n^2)

  當數組長度大於1000后,O(n^3)的算法就已經顯得力不從心了。想一想我們可以稍作改變就降低到O(n^2),這里需要比較的子數組個數為n*(n-1)/2,已經是n^2的數量級了,因此我們想能不能在求和上做改進,使得求子數組的和的復雜度為O(1)。上面的算法的i、j分別是子數組的上邊界和下邊界,子數組X[i,..j]的和sum(X[i,..j]) = sum(X[i,..j-1])+X[j],於是得到如下的算法:

 1 def maxSeqSum2(data):  2     n = len(data)  3     maxsofar = 0  4     for i in xrange(n):  5         s = 0  6         for j in xrange(i,n):  7             s += data[j]  8             if s > maxsofar:  9                 maxsofar = s 10     return maxsofar

 還有另外一種方法,將計算子數組的和的復雜度降到O(1),它先用數組accu[n]表示累加和,即accu[i]表示X前i個元素之和,這樣sum(X[i,..j])=accu[j]-accu[i-1],實現代碼如下:

def maxSeqSum3(X): '''maxSeqSum3 O(n^2)''' n = len(X) accu = [0] * (n+1) for i in xrange(n): accu[i] = accu[i-1] + X[i] maxsofar = 0 for i in xrange(n): for j in xrange(i,n): s = accu[j] - accu[i-1] if s > maxsofar: maxsofar = s return maxsofar

  這里利用了python的一個特別之處,即列表下標可以為負數,x[-1]即為x[n-1],用C語言實現需要修改一下,我們可以用accu[i+1]表示前i個元素之和,這樣C代碼如下:

 1 int maxSeqSum3(int X[],int n){  2     int accu[n+1];  3     accu[0] = 0;  4     for(int i=0 ; i < n; i++)  5         accu[i+1] = accu[i] + X[i];  6     int s,maxsofar = -1;  7     for(int i = 0; i < n; i++){  8         for(int j =i; j < n; j++){  9             s = accu[j+1]-accu[i]; 10             if(s > maxsofar) 11                 maxsofar = s; 12  } 13  } 14     return maxsofar; 15 
16 }

3、更近一步的方法O(nlog(n))

  O(n^2)的復雜度還是太高,當n大於10,000后,需要等待的計算時間就已經超過8分鍾了。如果能把這個問題規模分解成兩個子問題,那就能轉化為遞歸問題,很可能就能得到一個O(nlog(n))的算法。實際上,我們把原來的數組X[0,...n)分成兩個等長的子數組X1和X2:

X1 X2

  原數組X的最大連續子數組只能是以下三種情況之一:

  • 只在X1中,即X的最大和連續子數組就是X1的最大和連續子數組m1;
  • 只在X2中,即X的最大和連續子數組就是X2的最大和連續子數組m2; 
  m1     m2  
  • 即在X1中又在X2中,如m3所示
  m3  

  X1和X2的最大和連續子數組m1、m2可以通過遞歸來求解,現在我們只需要把m3求出來,比較其中的最大值就得到了X的最大和。不知道大家能否理解,m3是X1右邊的一個子數組之和加上X2左邊一個子數組之和。遞歸求解問題需要定義平凡解,即當子數組的長度為0時,最大和應該為0,當子數組長度為1時,最大和應該為max(xi[0],0)。這樣我們便實現了一個遞歸的解法:

 1 def recursionMax(X,l,u):  2     if l > u:#lenght is 0
 3         return 0  4     if l == u:  5         return max(X[l],0)  6     m = (l + u) / 2
 7     lmax = s = 0  8     for i in xrange(m,l,-1):  9         s += X[i] 10         lmax = max(lmax,s) 11     rmax = s = 0 12     for i in xrange(m+1,u-1): 13         s += X[i] 14         rmax = max(rmax,s) 15     return max(rmax+lmax,recursionMax(X,l,m),recursionMax(X,m+1,u)) 16 def maxSeqSum4(X): 17     '''maxSeqSum4 O(nlogn)'''
18     return recursionMax(X,0,len(X)-1)

  原問題的規模為T(n),用上面的遞歸解法,T(n) = 2T(n/2)+O(n)=O(nlogn)

4、哇哦~O(n)

  1977年UIF Grenander把這個問題告訴了CMU的Michael Shmos,Michael Shmos連夜想出來了上面的O(nlogn)算法,並與Jon Bentley分享,他們當時認為這是最好的算法了,可是幾天后,Shmos在CMU的一個報告會上向統計學家Jay Kadane描述了這個問題,Kadane幾分鍾就想出來一個O(n)的算法,太牛叉啦~

  Kadane的O(n)的算法只需要一個掃描數組X一次,掃描的過程記錄一個當前最大值maxsofar與一個以當前元素為右端點的最大值maxendinghere,在掃描的過程中如果maxendinghere比0小,那就從開始重新記錄。算法如下:

1 def maxSeqSum5(X): 2     '''maxSeqSum5 O(n)'''
3     maxsofar = maxendinghere = 0 4     for i in xrange(len(X)): 5         maxendinghere += X[i] 6         if maxendinghere < 0: 7             maxendinghere = 0 8         maxsofar = max(maxsofar,maxendinghere) 9     return maxsofar

   這已經是最優的解法了,任何正確的算法必須是O(n)復雜度的。假設最大和子數組為X[i,..,j],那么為了確定X[i,..,j]最大,我們必須遍歷X[0,i)和X(j,n)的所有元素,不論X[1,i)多么的小,都不能排除加上X[0]后變得超級大,從而最大子數組成了X[0,...,j]。

  前面的幾個解法,都只是求出了最大值,並沒有指出數組的范圍,但找出來也很容易,下面的代碼改進了一下O(n)的解法,同時返回子數組的下標起點和終點:

 1 def maxSeqSum6(X):  2     '''maxSeqSum5 O(n)'''
 3     # s1,su is the start and end of so far ,
 4     # el,eu is start and end of endinghere
 5     # take care that the subvector is X[s1,su), not X[s1,su]
 6     maxsofar = maxendinghere = 0  7     sl = su = el = eu = 0  8     for i in xrange(len(X)):  9         maxendinghere += X[i] 10         if maxendinghere < 0: 11             maxendinghere = 0 12             el = eu = i+1
13         else: 14             eu += 1
15         if maxsofar < maxendinghere: 16             maxsofar = maxendinghere 17             sl = el 18             su = eu 19     return maxsofar,sl,su-1

  

變形1 如果是要求至少選擇一個元素,應該如下處理: 

    public int maxSubArray(int[] A) {
        int cur=0;
        int max=Integer.MIN_VALUE;
        for(int i=0;i<A.length;i++){
            cur = Math.max(cur + A[i], A[i]);
            max = Math.max(max,cur);
        }
        return max;
    }

變形2 求樹的最大路徑之和,端點結點可以為任意結點,參考leetcode

private int maxSum;
    public int maxPathSum(TreeNode root) {
        if(root == null) return 0;
        maxSum = Integer.MIN_VALUE;
        dfs(root);
        return maxSum;
    }
    
    public int dfs(TreeNode root) {
        if(root == null) return 0;
        int left = dfs(root.left);
        int right = dfs(root.right);
        int sum = root.val;
        if(left > 0) sum += left;
        if(right > 0) sum += right;
        maxSum = Math.max(sum,maxSum);
        int max = Math.max(left,right);
        return max > 0 ? max + root.val : root.val;
        
    }

 

轉載請注明出處:www.cnblogs.com/fengfenggirl

 


免責聲明!

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



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