經典動態規劃:最大子數組問題

最大子數組問題和前文講過的 經典動態規劃:最長遞增子序列 的套路非常相似,代表着一類比較特殊的動態規劃問題的思路:

62eaa70490a93418de98f67455620a19.jpeg

思路分析

其實第一次看到這道題,我首先想到的是滑動窗口算法,因爲我們前文說過嘛,滑動窗口算法就是專門處理子串/子數組問題的,這裏不就是子數組問題麼?

但是,稍加分析就發現,這道題還不能用滑動窗口算法,因爲數組中的數字可以是負數

滑動窗口算法無非就是雙指針形成的窗口掃描整個數組/子串,但關鍵是,你得清楚地知道什麼時候應該移動右側指針來擴大窗口,什麼時候移動左側指針來減小窗口。

而對於這道題目,你想想,當窗口擴大的時候可能遇到負數,窗口中的值也就可能增加也可能減少,這種情況下不知道什麼時機去收縮左側窗口,也就無法求出「最大子數組和」。

解決這個問題需要動態規劃技巧,但是dp數組的定義比較特殊。按照我們常規的動態規劃思路,一般是這樣定義dp數組:

nums[0..i]中的「最大的子數組和」爲dp[i]

如果這樣定義的話,整個nums數組的「最大子數組和」就是dp[n-1]。如何找狀態轉移方程呢?按照數學歸納法,假設我們知道了dp[i-1],如何推導出dp[i]呢?

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在labuladong的算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種算法套路後投再入題海就如魚得水了。

如下圖,按照我們剛纔對dp數組的定義,dp[i] = 5,也就是等於nums[0..i]中的最大子數組和:

66aa78aa0290be2300c79ac3ba0d9369.jpeg

那麼在上圖這種情況中,利用數學歸納法,你能用dp[i]推出dp[i+1]嗎?

實際上是不行的,因爲子數組一定是連續的,按照我們當前dp數組定義,並不能保證nums[0..i]中的最大子數組與nums[i+1]是相鄰的,也就沒辦法從dp[i]推導出dp[i+1]

所以說我們這樣定義dp數組是不正確的,無法得到合適的狀態轉移方程。對於這類子數組問題,我們就要重新定義dp數組的含義:

nums[i]爲結尾的「最大子數組和」爲dp[i]

這種定義之下,想得到整個nums數組的「最大子數組和」,不能直接返回dp[n-1],而需要遍歷整個dp數組:

int res = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
    res = Math.max(res, dp[i]);
}
return res;

依然使用數學歸納法來找狀態轉移關係:假設我們已經算出了dp[i-1],如何推導出dp[i]呢?

可以做到,dp[i]有兩種「選擇」,要麼與前面的相鄰子數組連接,形成一個和更大的子數組;要麼不與前面的子數組連接,自成一派,自己作爲一個子數組。

如何選擇?既然要求「最大子數組和」,當然選擇結果更大的那個啦:

// 要麼自成一派,要麼和前面的子數組合並
dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);

綜上,我們已經寫出了狀態轉移方程,就可以直接寫出解法了:

int maxSubArray(int[] nums) {
    int n = nums.length;
    if (n == 0) return 0;
    int[] dp = new int[n];
    // base case
    // 第一個元素前面沒有子數組
    dp[0] = nums[0];
    // 狀態轉移方程
    for (int i = 1; i < n; i++) {
        dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);
    }
    // 得到 nums 的最大子數組
    int res = Integer.MIN_VALUE;
    for (int i = 0; i < n; i++) {
        res = Math.max(res, dp[i]);
    }
    return res;
}

以上解法時間複雜度是 O(N),空間複雜度也是 O(N),較暴力解法已經很優秀了,不過注意到dp[i]僅僅和dp[i-1]的狀態有關,那麼我們可以進行「狀態壓縮」,將空間複雜度降低:

int maxSubArray(int[] nums) {
    int n = nums.length;
    if (n == 0) return 0;
    // base case
    int dp_0 = nums[0];
    int dp_1 = 0, res = dp_0;

    for (int i = 1; i < n; i++) {
        // dp[i] = max(nums[i], nums[i] + dp[i-1])
        dp_1 = Math.max(nums[i], nums[i] + dp_0);
        dp_0 = dp_1;
        // 順便計算最大的結果
        res = Math.max(res, dp_1);
    }

    return res;
}

最後總結

雖然說動態規劃推狀態轉移方程確實比較玄學,但大部分還是有些規律可循的。

今天這道「最大子數組和」就和「最長遞增子序列」非常類似,dp數組的定義是「以nums[i]爲結尾的最大子數組和/最長遞增子序列爲dp[i]」。因爲只有這樣定義才能將dp[i+1]dp[i]建立起聯繫,利用數學歸納法寫出x狀態轉移方程。

_____________


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章