動態規劃之"最大連續子序列"

最大連續子序列問題

問題定義

給定K個整數的序列{ N1, N2, ..., Nk },其任意連續子序列可表示為{ Ni, Ni+1, ..., Nj },其中 1 <= i <= j <= K。最大連續子序列是所有連續子序列中元素和最大的一個, 例如給定序列{ -2, 11, -4, 13, -5, -2 },其最大連續子序列為{ 11, -4, 13 },最大和為20

解法1:樸素解法, 時間復雜度 O(K^2)

//假設給定序列:a1,a2,...,aK
maxsum=0; // 最大的連續子序列的和
for(int i=0; i<K; i++){
    tmpSum=0;
    for(int j=i; j<K; j++){
        tmpSum += a[j]
        if(tmpSum > maxsum){
            maxsum = tmpSum;
        }
    }
}

解法2:分治算法, 時間復雜度:O(nlogn)

對于任意一個序列{a1, a2, ...,am,.... an}, ( m=(n+1)/2 ) 最大的連續子序列在該序列中的位置存在三種情況:

  1. 位于中間部分的左邊;
  2. 位于中間部分的右邊 ;
  3. 左邊和右邊都含有最大的連續子序列的一部分, e.g. ai, ..., am, ...., aj.

對于情況1,2, 使用遞歸算法可以輕松計算出;對于情況3, 則通過求出前半部分的最大和(包含前半部分的最后一個元素)以及后半部分的最大和(包含后半部分的第一個元素)而得到,然后將這兩個和在一起, 最后,三種情況中最大的結果就是要求的結果。

int MaxSubSum(const int A[], int Left, int Right)
{
  int MaxLeftSum,MaxRightSum;
  int MaxLeftBorderSum,MaxRightBorderSum;
  int LeftBorderSum,RightBorderSum;
  int mid,i;
  
  if(Left == Right) // 處理只有一個元素的子序列
  {
    if(A[Left] > 0)
      return A[Left];
    else // 對于小于等于0的元素, 
      return 0;
  }
  
  mid= (Left + Right)/2;
  // 情況1
  MaxLeftSum = MaxSubSum(A,Left,mid);
  // 情況2
  MaxRightSum = MaxSubSum(A,mid+1,Right);
  
  // 情況3
  MaxLeftBorderSum = 0;
  LeftBorderSum = 0;
  for(i = mid;i >= Left;i--)// 求解最大序列的左邊部分
  {
    LeftBorderSum += A[i];
    if(LeftBorderSum > MaxLeftBorderSum)
      MaxLeftBorderSum = LeftBorderSum;
  }
  
  MaxRightBorderSum = 0;
  RightBorderSum = 0;
  for(i = mid+1;i <= Right;i++)// 求解最大序列的右邊部分
  {
    RightBorderSum += A[i];
    if(RightBorderSum > MaxRightBorderSum)
      MaxRightBorderSum = RightBorderSum;
  } 
  
  return Max(MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum); // 返回三種情況中最大的結果
}

解法3: 動態規劃 , 時間復雜度O(n)

引理1: 以負數開頭的子序列不會是最大子序列。
證明:令子序列為{ai, ..., aj}, 其中開頭的元素 ai < 0, 則 ai + ... + aj < ai+1+...+aj 顯然成立。

引理2:對子序列 {ai, ..., aj} , 如果該子序列滿足兩個條件:

  1. 如果對x取 [i, j) 中的任意整數(包含i,不包含j) sum{ai, ..., ax} >0.
  2. sum{ai, ..., aj}<0.

以該子序列中的任何元素ap開頭的以aj為終結的任意子序列的和必定小于0

證明:從兩個條件中易推斷出:aj<0, 且由引理1知 以負數開頭的連續子序列不可能是最大連續子序列,則: ai > 0.
顯然有 0 >= sum{ai, ..., aj} >= sum{ai-1, ..., aj} >= sum{ap, ..., aj}, 其中 p 是[i, j)之間的整數。
反證法:假設sum{ap, ..., aj}>0, p取 [i, j) 之間的整數, 由引理2條件 sum{ai, ..., aj}<0 得出sum{ai, ..., ap-1}<0,該結論違反了引理2中的條件:如果對x取[i, j)中的任意整數(包含i,不包含j) sum{ai, ..., ax} >0. 得證。

由引理1可知,若a[i]<0, 則應跳到a[i+1]作為子序列的開頭元素(如果a[i+1]>0);
由引理2可知, 若a[i]+...+a[j]<=0且滿足引理2的第一個條件,則應以a[j+1]作為最大連續子序列的開頭元素(如果a[j+1]>0). 實質上,引理1是引理2的特例。

引理1和2可歸結為該狀態方程: maxsum(i)= max( maxsum(i-1)+ary(i), ary(i) ); (也可以由動態規劃方法處理的準則:最優子結構”、“子問題重疊”、“邊界”和“子問題獨立”得到)
通過對給定序列順序地反復運用引理1和引理2,最終可求得該序列的最大連續子序列。
代碼如下:

int maxSubSeq(int[] ary){
    int maxsum=0;
    int localSum=0;
    for (int i=0; i<ary.length; ++i){
        localSum += ary[i];
        if(localSum > maxsum){
            maxsum= localSum;
        }else if (localSum < 0){ 
            localSum=0; // 不考慮 ai~aj中的元素作為子序列的開頭, 其中ai>0, aj<0
        } //else  ==> localSum >0, 就是引理2中的條件1
    }
      return maxsum;
}

注意:解法2對于數組中全部是負數的數組返回0,而不是數組中的最大值。

解法4:動態規劃(可以處理數組中全部是負數的情況,該方法會返回數組中的最大值)

從解法2的分治思想得到提示,可以考慮數組的第一個元素A[0], 以及和最大的一段數組(A[i], .., A[j]), A[0] 和 和最大的一段數組的關系如下:

  1. 當0=i=j時,元素A[0]自己構成和最大的一段。
  2. 當0=i<j時,元素和最大的一段數組以A[0]開頭A[j]結尾。
  3. 當0 < i時,元素和和最大的一段數組沒有關系。

因此,我們將一個大問題(具有N個元素的數組)轉換成較小的問題(具有N-1個元素的數組)

all[1] 為 A[1],...,A[N-1]中 和最大的一段數組之和
start[1] 為 A[1], ..., A[N-1]中 以A[1]開頭的和最大的一段數組之和

不難發現,(A[0], A[1], ..., A[N-1]) 中和最大的一段數組的和 是 三種情況的最大值 max(A[0], A[0]+start[1], all[1])

可以看出該問題無后效性,可以使用動態規劃的方案解決。

因此我們可以得到初始的算法:

 public static int maxSum1(int[] A){
        int[] start = new int[A.length];
        int[] all = new int[A.length];
        
        all[A.length-1] = A[A.length-1];
        start[A.length-1] = A[A.length - 1];
        for(int i = A.length-2; i>=0; --i){
            start[i] = Math.max(A[i], A[i] + start[i+1]);
            all[i] = Math.max(start[i], all[i+1]);
        }
        return all[0];
    }

算法優化
可以看到,計算start[i] 時,和 start[i+1]有關,計算all[i] 時,和all[i+1]有關
因此,我們可以使用兩個變量進行優化。

public static int maxSum1(int[] A){
       int nStart = A[A.length-1];
       int nAll = A[A.length-1];
        for(int i = A.length-2; i>=0; --i){
            nStart = Math.max(A[i], A[i] + nStart);
            nAll = Math.max(nStart , nAll);
        }
        return nAll;
    }

從上述優化算法可以看出:當nStart < 0時,nStart被賦值為A[i].

因此我們可以將算法改寫為更清晰的寫法:

public static int maxSum(int[] A){
        int nStart = A[A.length-1];
        int nAll = A[A.length - 1];
        for(int i = A.length-2; i>=0; --i){
            if(nStart < 0){
                nStart = 0;
            }
            nStart += A[i];
            if(nStart > nAll){ /// 即使數組中全部是負數,我們也會選出具有最大值的數。
                nAll = nStart;
            }
        }
        return nAll;
    }

擴展問題

問題1

如果數組(A[0], ..., A[n-1])首尾相連,即我們被允許找到一段數字(A[i],..., A[n-1], A[0], ..., A[j])式其和最大。

問題分解:

  1. 解沒有穿過A[n-1]和A[0]連接
  2. 解穿過了A[n-1]和A[0]連接
    2.1. 解包含A[0], ..., A[n-1]
    2.2. 解包含兩部分:(1)從A[0]開始的一段 (A[0], ..., A[j]) (0<=j <n); (2) 從A[i]開始的一段(A[i], .., A[n-1]) (j<i<n)

尋找2.2.的解 相當于 從A數組中刪除一塊子數組(A[j+1],....,A[i-1])且刪除的子數組的和是負數且其絕對值最大。這相當于將問題轉為子問題1。

問題的解:取兩種情況的最大值。

時間復雜度 :求解子問題2只需遍歷數組一次,子問題1可以使用前面介紹的方法求解時間復雜度O(N). 所以時間復雜度共O(N)

代碼:(該代碼尚未驗證其正確性,請讀者自行驗證,如有錯誤請留言評論)

/**
     * The correctness should be validated in the future!!!
     * @param A
     * @return
     */
    public static int maxSumCycle(int[] A){
        int s1 = maxSum(A);

        int s2 = 0;

        int nAll = A[A.length-2];
        int nStart = A[A.length-2];
        for(int i=A.length-1; i>=0; --i){
            s2 += A[i];

            // Find maximum abs value from range 1~A.length-2
            if(i>=1 && i<=A.length-3){
                nStart = Math.min(nStart, A[i] + nStart);
                nAll = Math.min(nStart, nAll);
            }

        }
        if(nAll>0) nAll = 0;
        return Math.max(s1, Math.max(s2, s2 + nAll));
    }

問題2

如果要求通知返回最大子數組的位置,應該如何修改算法,使保持O(N)的復雜度?

public static int maxSum(int[] A){

        int s=A.length-1, e=A.length-1; // [s, e]
        int p =0;
        int nStart = A[A.length-1];
        int nAll = A[A.length - 1];
        for(int i = A.length-2; i>=0; --i){
            if(nStart < 0){  // 以 A[i+1] 開頭的子數組的和,不可能是最優解,新的最優解的終點應該是 A[i]
                nStart = 0;
                p = i;
            }
            nStart += A[i];
            if(nStart > nAll){
                if(nStart==A[i]) e = p; //  表明以p為終點的最優解 開始計算。
                nAll = nStart;
                s = i; // 如果 nStart > nAll, 說明以當前 A[i] 開始一段數組,具有目前最優的解。
            }
        }
        System.out.printf("sidx=%d, eidx=%d\n", s, e);
        return nAll;
    }

//測試實例,讀者可自行實驗,推導
//        int[] ary = {1, -2, 3, 10, -4, 7, 2, -5};
//        int[] ary = {0, -2, 3, 5, -1, 2};
//        int[] ary = {-9, -2, -3, -5, -3};
//        int[] ary = {1, -2, 3, 5, -3, 2};

注:解法4和擴展問題,都是引用《編程之美》上面的解法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,462評論 2 378

推薦閱讀更多精彩內容