最大子數組問題和前文講過的 經典動態規劃:最長遞增子序列 的套路非常相似,代表着一類比較特殊的動態規劃問題的思路:
思路分析
其實第一次看到這道題,我首先想到的是滑動窗口算法,因爲我們前文說過嘛,滑動窗口算法就是專門處理子串/子數組問題的,這裏不就是子數組問題麼?
但是,稍加分析就發現,這道題還不能用滑動窗口算法,因爲數組中的數字可以是負數。
滑動窗口算法無非就是雙指針形成的窗口掃描整個數組/子串,但關鍵是,你得清楚地知道什麼時候應該移動右側指針來擴大窗口,什麼時候移動左側指針來減小窗口。
而對於這道題目,你想想,當窗口擴大的時候可能遇到負數,窗口中的值也就可能增加也可能減少,這種情況下不知道什麼時機去收縮左側窗口,也就無法求出「最大子數組和」。
解決這個問題需要動態規劃技巧,但是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]
中的最大子數組和:
那麼在上圖這種情況中,利用數學歸納法,你能用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狀態轉移方程。
_____________