連續子數組問題是算法中經常可以見到的一類題目,通過幾個典型的題目分析,可以發現這類題目主要分為兩大類,其解題思路通過最簡單的子串枚舉(枚舉所有的子串起點和終點)來暴力解決大都不難,但是如果考慮到對空間和時間的要求,其解答就需要一定的算法技巧。
- 子數組和問題(前綴和+哈希表)
- 子數組最值問題(多階段決策過程最優化問題,動態規划)
子數組和的問題可以通過前綴和解決,而關於子數組的第二類題目往往會涉及到一些最值問題,比如最大子數組和、最長子數組、乘積最大子數組等等,根據我們的算法積累經驗,這類求最值的問題,往往會用到動態規划的思路。因為尋找滿足一個最值條件的子數組就相當於一個多階段的決策過程最優化問題,這里的決策就是如何選取子數組的起點和終點,最優化可以通過dp的思路進行記憶化搜索。
一般情況下,我們可以用dp[i]
代表以第 i 個元素結尾的子數組的最值,而遞推關系就是考慮和dp[i-1]
之間的關系,然后我們要求的答案就是子數組中的某一個,也就是max(dp[i])
,然后我們只需要枚舉所有的i,邊遍歷邊記錄最大值即可。
53、最大子序和(Easy)
題目描述:給定一個整數數組 nums
,找到一個具有最大和的連續子數組(子數組最少包含一個元素),返回其最大和。
示例:
輸入: [-2,1,-3,4,-1,2,1,-5,4],
輸出: 6
解釋: 連續子數組 [4,-1,2,1] 的和最大,為 6。
解題思路:
這里用dp[i]
代表以第 i 個元素結尾的子數組的最大和,則max(dp[i])
就是要求的最終結果,那么關鍵是如何寫出遞推關系式,明顯的,因為我們考慮的子數組以nums[i]
結尾,那么該數組一定是包含nums[i]
這個元素的,因此需要考慮兩種情況:即nums[i]
單獨成為一段還是與前面的dp[i-1]
一起構成子數組,因此,可以得到如下的遞推關系式:
dp[i]=max(dp[i-1]+nums[i],nums[i])
從以上遞推關系式可以看出,dp[i]
只與dp[i-1]
相關,因此也不需要維護整個dp數組,只需要維護一個變量用以更新遞推關系即可。
代碼實現:
class Solution {
public int maxSubArray(int[] nums) {
int temp=nums[0];
int res=temp;
for(int i=1;i<nums.length;i++){
temp=Math.max(temp+nums[i],nums[i]); //遞推關系
res=Math.max(res,temp);
}
return res;
}
}
152、乘積最大子數組(Medium)
題目描述:給你一個整數數組 nums
,請你找出數組中乘積最大的連續子數組(該子數組中至少包含一個數字),並返回該子數組所對應的乘積。
示例 1:
輸入: [2,3,-2,4] 輸出: 6
解釋: 子數組 [2,3] 有最大乘積 6。
示例 2:
輸入: [-2,0,-1] 輸出: 0
解釋: 結果不能為 2, 因為 [-2,-1] 不是子數組。
解題思路:
解法一:動態規划
上一題我們解決的是子數組和最大,而這里是子數組的積最大,非常相似,這里我們很容易想到類似於上題的遞推關系:dp[i]=max(dp[i-1]*nums[i],nums[i])
,僅僅是將和轉化為積,但是這確實錯誤的,原因在於和與積具有一定的差異,關鍵在於乘積涉及到正負符號的問題,如果當前是一個負數,而以前一項結尾的子數組乘積最大為正數,最小為負數,那么我們的結果在兩項中取其大,無論如何都會得到一個負數,而前一項的最小值*當前值,負負得正,會得到一個正數,顯然乘積更大,所以很明顯上面的遞推關系不夠全面。比如{5,6,−3,4,−3}
.
因此,這里我們可以對正負進行分別考慮。具體說來,如果當前位置如果是一個負數的話,那么我們希望以它前一個位置結尾的某個段的積也是個負數,這樣就可以負負得正,並且我們希望這個積盡可能「負得更多」,即盡可能小。如果當前位置是一個正數的話,我們更希望以它前一個位置結尾的某個段的積也是個正數,並且希望它盡可能地大。
基於這個想法,我們就不難得到進一步的解法,也就是對於每一個結尾元素,既維護一個最大值,也維護一個最小值,在遞推得到結果時,三者取其大。具體見代碼。
解法二:利用乘積的性質
考慮到這里是數組的乘積,因此,我們可以得到一個結論:數組里如果全是正數和偶數個負數,乘起來就最大,如果是奇數個負數,那么要么是沒有第一個負數,要么是沒有最后一個負數,不可能是中間,因為中間會使數組不連續。
基於這樣的結論,就可以正向進行一次乘法,反向進行一次乘法,同時記錄和比較最大值,需要注意的是如果有0存在需要特殊判斷。
代碼實現:
//解法一:動態規划,時間復雜度O(n),空間復雜度O(1)
class Solution {
public int maxProduct(int[] nums) {
//動態規划,注意符號,類似於最大子序和
if(nums==null||nums.length==0)
return 0;
int len=nums.length;
//int[] dp_max=new int[len]; //以nums[i]為結尾的乘積最大子數組
//int[] dp_min=new int[len];
//因為遞推關系dp[i]只和dp[i-1]相關,沒必要用數組,可以用一個變量節省空間
int temp_max=nums[0];
int temp_min=nums[0];
int product=nums[0];
for(int i=1;i<len;i++){
//注意遞推關系為三者取最大(小),因為負負得正,前一項的最小值乘以當前的一個負數很可能就成為最大值
int pre_max=temp_max;
temp_max=Math.max(nums[i],Math.max(temp_max*nums[i],temp_min*nums[i]));
temp_min=Math.min(nums[i],Math.min(pre_max*nums[i],temp_min*nums[i]));
product=Math.max(product,temp_max);
}
return product;
}
}
//解法二:利用乘積的性質,時間復雜度O(n),空間復雜度O(1)
class Solution {
public int maxProduct(int[] nums) {
if(nums==null || nums.length==0)
return 0;
int res=nums[0];
int max=1;
//正向乘,三種情況:全是正數,偶數個負數,或者沒有最后一個負數
for(int i=0;i<nums.length;i++){
max*=nums[i];
res=Math.max(res,max);
if(max==0) //等於0的話需要重新從1開始
max=1;
}
max=1;
//反向乘,沒有第一個負數的情況
for(int i=nums.length-1;i>=0;i--){
max*=nums[i];
res=Math.max(res,max);
if(max==0)
max=1;
}
return res;
}
}