動態規劃專題——2 最大子序列

給出一個整數數組a(正負數都有),如何找出一個連續子數組(可以一個都不取,那麼結果爲0),使得其中的和最大?

例如:-2,11,-4,13,-5,-2,和最大的子段爲:11,-4,13。和爲20。

這裏寫圖片描述

看見這個問題你的第一反應是用什麼算法?

(1) 枚舉?對,枚舉是萬能的!枚舉什麼?子數組的位置!好枚舉一個開頭位置i,一個結尾位置j>=i,再求a[i..j]之間所有數的和,找出最大的就可以啦。好的,時間複雜度?

(1.1)枚舉i,O(n)
(1.2)枚舉j,O(n)
(1.3)求和a[i..j],O(n)

大概是這樣一個計算方法:

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];

        max = Max(max, sum);
    }
}

所以是O(n^3), 複雜度太高?降低一下試試看?
(2) 仍然是枚舉! 能不能在枚舉的同時計算和?
(2.1)枚舉i,O(n)
(2. 2)枚舉j,O(n) ,這裏我們發現a[i..j]的和不是a[i..j – 1]的和加上a[j]麼?所以我們在這裏當j增加1的時候把a[j]加到之前的結果上不就可以了麼?對!所以我們毫不費力地降低了複雜度,得到了一個新地時間複雜度爲O(n^2)的更快的算法。

大概是這樣一段代碼:

for(int i = 1; i <= n; i++)
{
    int sum = 0;

    for(int j = i; j <= n; j++)
    {
        sum += a[j];
        max = Max(max, sum);
    }
}

是不是到極限了?遠遠不止!

(3)分治一下?

我們從中間切開數組,原數組的最大子段和要麼是兩個子數組的最大子段和(分), 要麼是跨越中心分界點的最大子段和(合)。 那麼跨越中心分界點的最大子段合怎麼計算呢?仍然是枚舉! 從中心點往左邊找到走到哪裏可以得到最大的合,再從中心點往右邊檢查走到哪裏可以得到最大的子段合,加起來就可以了。可見原來問題之所以難,是因爲我們不知道子數組從哪裏開始,哪裏結束,沒有“着力點”,有了中心位置這個“着力點”,我們可以很輕鬆地通過循環線性時間找到最大子段和。

於是算法變成了

(3.1)拆分子數組分別求長度近乎一半的數組的最大子段和sum1, sum2

時間複雜度 2* T(n / 2)

(3.2)從中心點往兩邊分別找到最大的和,找到跨越中心分界點的最大子段和sum3 時間複雜度 O(n)

那麼總體時間複雜度是T(n) = 2 * T(n / 2) + O(n) = O(nlogn), 又優化了一大步,不是嗎?

還能優化嗎?再想想,別放棄!

我們在解法(3)裏需要一個“着力點”達到O(n)的子問題時間複雜度,又在解法(2)裏輕易地用之前的和加上一個新的元素得到現在的和,那麼“之前的和”有那麼重要麼?如果之前的和是負數呢?顯然沒用了吧?我們要一段負數的和,還不如從當前元素重新開始了吧?

再想想,如果我要選擇a[j],那麼“之前的和”一定是最大的並且是正的。不然要麼我把“之前的和”換成更優,要麼我直接從a[j]開始,不是更好麼?

動態規劃大顯身手。我們記錄dp[i]表示以a[i]結尾的全部子段中最大的和。我們看一下剛纔想到的,我取不取a[i – 1],如果取a[i – 1]則一定是取以a[i – 1]結尾的子段和中最大的一個,所以是dp[i – 1]。 那如果不取dp[i – 1]呢?那麼我就只取a[i]孤零零一個好了。注意dp[i]的定義要麼一定取a[i]。 那麼我要麼取a[i – 1]要麼不取a[i -1]。 那麼那種情況對dp[i]有利? 顯然取最大的嘛。所以我們有dp[i] = max(dp[i – 1] + a[i], a[i]) 其實它和dp[i] = max(dp[i – 1] , 0) + a[i]是一樣的,意思是說之前能取到的最大和是正的我就要,否則我就不要!初值是什麼?初值是dp[1] = a[1],因爲前面沒的選了。

那麼結果是什麼?我們要取的最大子段和必然以某個a[i]結尾吧?那麼結果就是max(dp[i])了。

這樣,我們的時間複雜度是O(n),空間複雜度也是O(n)——因爲要記錄dp這個數組。
算法達到最優了嗎? 好像是!還可以優化!我們注意到dp[i] = max(dp[i - 1], 0) + a[i], 看它只和dp[i – 1]有關,我們爲什麼要把它全記錄下來呢?爲了求所有dp[i]的最大值?不,最大值我們也可以求一個比較一個嘛。

我們定義endmax表示以當前元素結尾的最大子段和,當加入a[i]時,我們有endmax’ = max(endmax, 0) + a[i], 然後再順便記錄一下最大值就好了。

僞代碼如下;(數組下標從1開始)

endmax = answer = a[1]
for i = 2 to n do
    endmax = max(endmax, 0) + a[i]
    answer = max(answer, endmax)
endfor

時間複雜度?O(n)!空間複雜度?O(1)! 簡單吧?我們不僅優化了時間複雜度和空間複雜度,還使代碼變得簡單明瞭,更不容易出錯。

老生常談的問題來了。我們如何找到一個這樣的子段?請看上面的爲僞代碼endmax = max(endmax, 0) + a[i], 對於endmax它對應的子段的結尾顯然是a[i],我們怎麼知道這個子段的開頭呢? 就看它有沒有被更新。也就是說如果endmax’ = endmax + a[i]則對應子段的開頭就是之前的子段的開頭。否則,顯然endmax開頭和結尾都是a[i]了,讓我們來改一下僞代碼:

start = 1
answerstart = asnwerend = 1
endmax = answer = a[1]
for end = 2 to n do
    if endmax > 0 then
        endmax += a[end]
    else
        endmax = a[end]
        start = end
    endif
    if endmax > answer then
        answer = endmax
        answerstart = start
        answerend = end
    endif
endfor

這裏我們直接用end作爲循環變量,通過更新與否決定start是否改變。

總結:通過不斷優化,我們得到了一個時間複雜度爲 O(n),空間複雜度爲O(1)的簡單的動態規劃算法。動態規劃,就這麼簡單!優化無止境!

最後,由你來寫一段程序,實現這個算法,只有寫出了正確的程序,才能繼續後面的課程。

輸入

第1行:整數序列的長度N(2 <= N <= 50000)
第2 - N + 1行:N個整數(-10^9 <= A[i] <= 10^9)

輸出

輸出最大子段和。

輸入示例

6
-2
11
-4
13
-5
-2

輸出示例

20

參考代碼:

#include<cstdio>
#include<algorithm>
using namespace std;
int main(){
    int N,n[50050],start=1,answerstart=1,answerend=1;
    long long Max,answer;
    scanf("%d",&N);
    scanf("%d",&n[1]);
    answer=Max=n[1];
    for(int end=2;end<=N;end++){
        scanf("%d",n+end);
        if(Max>=0)  Max+=n[end];
        else{
            Max=n[end];
            start=end;
        }
        if (Max > answer){
            answer = Max;
            answerstart = start;
            answerend = end;
        }   
    }   
    printf("%lld",answer);
    return 0;
} 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章