連續子數組問題是算法中經常可以見到的一類題目,通過幾個典型的題目分析,可以發現這類題目主要分為兩大類,其解題思路通過最簡單的子串枚舉(枚舉所有的子串起點和終點)來暴力解決大都不難,但是如果考慮到對空間和時間的要求,其解答就需要一定的算法技巧。
- 子數組和問題(前綴和+哈希表)
- 子數組最值問題(多階段決策過程最優化問題,動態規划)
【子數組】子數組和問題(前綴和)
一看到子數組和,有必要先對前綴和思想進行一些考慮。前綴和指的是:數組 第 0 項 到 當前項 的 總和,類似於我們在數學中學到的數列的前n項和。
如果用一個數組 pre[] 表示:pre[i]=nums[0]+nums[1]+···+nums[i]
前綴和的優勢在於:數組中的某一項可以表示為相鄰前綴和之差:nums[i]=pre[i]-pre[i-1]
,因此,從 i 到 j 范圍子數組和就可以表示為:nums[i]+nums[i+1]+···+nums[j]=pre[j]-pre[i-1]
,這一關系就為求解子數組和問題提供了數學依據。
560、和為K的子數組
題目描述:給定一個整數數組和一個整數 k,你需要找到該數組中和為 k 的連續的子數組的個數。
示例:
輸入:nums = [1,1,1], k = 2
輸出: 2 , [1,1] 與 [1,1] 為兩種不同的情況。
解題思路:
解法一:暴力枚舉
暴力枚舉子數組的起點 i 和終點 j,對其中的元素進行求和,得到和判斷是否等於k,時間復雜度為O(n^3)
,但是對於子數組累積求和可以在遍歷的過程中同步進行記錄,因此,暴力法的時間復雜度最多可以優化到O(n^2)
解法二:前綴和+哈希表
正如前面提到的前綴和的優勢,從 i 到 j 范圍子數組和可以表示為:nums[i]+nums[i+1]+···+nums[j]=pre[j]-pre[i-1]
,那么從 i 到 j 范圍子數組和為K這個條件可以表示為:pre[j]-pre[i-1]=k
.
因此,這就可以得到pre[i-1]=pre[j]-k
,所以考慮以 j 結尾的和為 k的連續子數組個數時,只要統計有多少個前綴和為pre[j]-k
的pre[i]
即可。通過建立哈希表,以前綴和作為鍵,該和出現的次數作為值,在遍歷數組時,一邊統計前綴和,一邊從哈希表中得到對應的次數,從而得到最后的答案,具體可以參見代碼實現。
代碼實現:
//解法一:暴力枚舉,時間復雜度O(n^2),空間復雜度O(1)
class Solution {
public int subarraySum(int[] nums, int k) {
int count=0;
for(int i=0;i<nums.length;i++){
int sum=0;
for(int j=i;j<nums.length;j++){ //從i到j這個子數組,累積求和
sum+=nums[j];
if(sum==k)
count++;
}
}
return count;
}
}
//解法二:前綴和+哈希表,時間復雜度O(n),空間復雜度O(n)
class Solution {
public int subarraySum(int[] nums, int k) {
if(nums==null || nums.length==0)
return 0;
Map<Integer,Integer> map=new HashMap<>(); //<前綴和,次數>
map.put(0,1); //注意這是必要的,這可以保證不漏掉只有一個元素的子數組
int count=0,pre=0;
for(int i=0;i<nums.length;i++){
pre+=nums[i];
if(map.containsKey(pre-k)) //找pre-k出現的次數,pre-k
count+=map.get(pre-k);
map.put(pre,map.getOrDefault(pre,0)+1);
}
return count;
}
}
974、和可被K整除的子數組
題目描述:給定一個整數數組 A
,返回其中元素之和可被 K
整除的(連續、非空)子數組的數目。
示例:
輸入:A = [4,5,0,-2,-3,1], K = 5
輸出:7
解釋:
有 7 個子數組滿足其元素之和可被 K = 5 整除:
[4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]
解題思路:
可以看到本題和上一題基本相同,不同之處僅僅在於上一題為和為K,而這里是和可以被K整除。其基本解題思路類似,但是需要用到一個數學上的同余定理:給定一個正整數m,如果兩個整數a和b滿足a-b能夠被m整除,即(a-b)/m得到一個整數,那么就稱整數a與b對模m同余。
解法一:暴力枚舉
和上題類似,枚舉所有子數組,求和后判斷是否能被K整除即可,時間復雜度同樣為O(n^2)。
解法二:前綴和+哈希表
從 i 到 j 范圍子數組和可以被K整除這個條件可以表示為:pre[j]-pre[i-1] mod K == 0
,根據我們提到的同余定理,只需要滿足pre[j] mod K == pre[i-1] mod K
,就可以滿足題意。
因此,可以考慮對數組進行遍歷,在遍歷同時統計答案。當我們遍歷到第 i 個元素時,我們統計以 ji 結尾的符合條件的子數組個數。然后維護一個以前綴和模 K 的值為鍵,出現次數為值的哈希表 ,在遍歷的同時進行更新。這樣類似上一題進行維護和計算即可得到結果。
需要注意的是:不同的語言負數取模的值不一定相同,有的語言為負數,如Java 取模的特殊性,當被除數為負數時取模結果為負數,需要糾正。這是因為比如:k是2,序列是-3 4 9 ,那么模如果不是正數 ,會分別是 -1 1 0 ,而 -1 和 1之間剛好是距離k的,卻不被統計,這就會漏掉子數組為2的這個答案,糾正的方法是:(pre%K+K)%K
代碼實現:
class Solution {
public int subarraysDivByK(int[] A, int K) {
//哈希表+前綴和,時間復雜度O(n),空間復雜度O(n)
//兩個數模K的結果相等, 其差能被K整除
if(A==null || A.length==0)
return 0;
Map<Integer,Integer> map=new HashMap<>(); //<前綴和,次數>
map.put(0,1);
int pre=0,res=0;
for(int i=0;i<A.length;i++){
pre+=A[i]; //求前綴和
int temp=(pre%K+K)%K; //注意 Java 取模的特殊性,當被除數為負數時取模結果為負數,需要糾正
if(map.containsKey(temp))
res+=map.get(temp);
map.put(temp,map.getOrDefault(temp,0)+1);
}
return res;
}
}