幾道用 前綴和+哈希表優化【必要時加入狀態壓縮】來解決連續子數組的題目小結

Leetcode 560.和爲 K 的子數組

問題描述

給定一個整數數組和一個整數 k,你需要找到該數組中和爲 k 的連續的子數組的個數。

示例 1 :

輸入:nums = [1,1,1], k = 2
輸出: 2 , [1,1] 與 [1,1] 爲兩種不同的情況。
說明 :

數組的長度爲 [1, 20,000]。
數組中元素的範圍是 [-1000, 1000] ,且整數 k 的範圍是 [-1e7, 1e7]。

解題報告

一個連續區間的和爲 k 意味着 preSum[j]-preSum[i]==k,所以我們提前將前綴和放到哈希表中,然後在順序遍歷過程中直接查找當前的前綴和減去 k 是否存在然後累加出現次數即可。

實現代碼

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        unordered_map<int, int> mp;
        mp[0] = 1;
        int ans = 0, preSum = 0;
        for (auto& x:nums) {
            preSum += x;
            if (mp.find(preSum - k) != mp.end()) ans += mp[preSum - k];
            mp[preSum]++;
        }
        return ans;
    }
};

Leetcode 1248. 統計「優美子數組」

問題描述

給你一個整數數組 nums 和一個整數 k。

如果某個 連續 子數組中恰好有 k 個奇數數字,我們就認爲這個子數組是「優美子數組」。

請返回這個數組中「優美子數組」的數目。

示例 1:

輸入:nums = [1,1,2,1,1], k = 3
輸出:2
解釋:包含 3 個奇數的子數組是 [1,1,2,1] 和 [1,2,1,1] 。

解題報告

只記錄奇數個數, preSum[j] 表示 nums[0:j] 這個區間奇數出現的次數。所以 某個 連續 子數組中恰好有 k 個奇數數字 意味着 preSum[j]-preSum[i]==k,其他部分和上一題大差不差。

實現代碼

class Solution {
public:
    int numberOfSubarrays(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> count(n+1, 0);
        count[0] = 1;
        int res = 0, odd = 0;
        for (int i = 0; i < n; ++i) {
            odd += nums[i]&1;
            if (odd >= k) res += count[odd-k];
            count[odd]++;
        }
        return res;
    }
};

Leetcode 523. 連續的子數組和

問題描述

給定一個包含非負數的數組和一個目標整數 k,編寫一個函數來判斷該數組是否含有連續的子數組,其大小至少爲 2,總和爲 k 的倍數,即總和爲 n*k,其中 n 也是一個整數。

示例 1:

輸入: [23,2,4,6,7], k = 6
輸出: True
解釋: [2,4] 是一個大小爲 2 的子數組,並且和爲 6。

解題報告

根據題意,(preSum[j]-preSum[i])%k==0 ,根據同餘定理可知 preSum[i]%k==preSum[j]%k。所以我們將前綴和對 k 的取餘存在哈希表中即可。

\color{red}本題中的數組是非負的

實現代碼

class Solution {
public:
    bool checkSubarraySum(vector<int>& nums, int k) {        
        int sum=0;
        unordered_map<int,int> map;
        map.insert({0,-1});
        for(int i=0;i<nums.size();i++){
            sum+=nums[i];
            if(k!=0)
                sum=sum%k;
            if(map.count(sum)){
                if(i-map[sum]>1)
                    return true;
            }
            else
                map.insert({sum,i});
        }
        return false;
    }
};

Leetcode 1124. 表現良好的最長時間段

問題描述

給你一份工作時間表 hours,上面記錄着某一位員工每天的工作小時數。

我們認爲當員工一天中的工作小時數大於 8 小時的時候,那麼這一天就是「勞累的一天」。

所謂「表現良好的時間段」,意味在這段時間內,「勞累的天數」是嚴格 大於「不勞累的天數」。

請你返回「表現良好時間段」的最大長度。

示例 1:

輸入:hours = [9,9,6,0,6,6,9]
輸出:3
解釋:最長的表現良好時間段是 [9,9,6]。

提示:

  • 1 <= hours.length <= 10000
  • 0 <= hours[i] <= 16

解題報告

將工作時大於 8 的那一天映射成 1,將工作時小於等於 8 的那一天映射成 -1。在哈希表中存該前綴和的下標。

表面上看 「表現良好時間段」的最大長度是子數組的和爲 1,但是這樣理解是錯誤的。

  • 當前綴和爲正數時,意味着我們還有餘力負擔更多的,但是前綴和本來就是從 0 開始累加的,所以直接取爲 i+1
  • 當前綴和爲負數時 ,我們負擔不起這個前綴和對應的區間前面過多的「不勞累的天數」,所以需要將子數組的區間適當縮小。

實現代碼

class Solution {
public:
    int longestWPI(vector<int>& hours) {
        int ans=0,preSum=0;
        unordered_map<int,int>mp;
        for(int i=0;i<hours.size();i++){
            preSum+=(hours[i]>8?1:-1);
            if(preSum>0) {
                ans=i+1;
                continue;
            }
            if(mp[preSum-1]!=0)
                ans=max(ans,i-mp[preSum-1]+1);
            if(mp[preSum]==0) mp[preSum]=i+1;
        }
        return ans;
    }
};

Leetcode 1371. 每個元音包含偶數次的最長子字符串

問題描述

給你一個字符串 s ,請你返回滿足以下條件的最長子字符串的長度:每個元音字母,即 ‘a’,‘e’,‘i’,‘o’,‘u’ ,在子字符串中都恰好出現了偶數次。

示例 1:

輸入:s = “eleetminicoworoep”
輸出:13
解釋:最長子字符串是 “leetminicowor” ,它包含 e,i,o 各 2 個,以及 0 個 a,u 。

解題報告

\color{red}這道題真的是太巧妙了。

將每個元音出現次數的 奇偶性 作爲累加的 前綴和

符合條件的區間中,區間 [0,i] 中各元音字母出現次數的奇偶性必定和 [0,j] 中各元音字母出現次數的 奇偶性相同,只有這樣,區間 [i,j] 中各元音字母纔會出現偶數次。

那麼問題就變成了如何累加 各個元音字母出現次數的 奇偶性 呢?

  • 首先用 五位的二進制數 來表示 各個元音字母出現次數的 奇偶性。第一位二進制數【從左到右】表示 u,第二位二進制數【從左到右】表示 o,第三位二進制數【從左到右】表示 i,第四位二進制數【從左到右】表示 e,第五位二進制數【從左到右】表示 a
    00000 表示每個元音字母均出現偶數次;
    11111表示每個元音字母均出現奇數次。
  • 然後每出現一次元音字母,將 前綴和 和該元音字母所對應的二進制位進行異或操作,得到一個新的 前綴和
  • 最後在哈希表中查找是否存在該 前綴和 狀態,如果存在的話,更新答案,如果不存在的話,將該狀態存入哈希表中。

實現代碼

class Solution {
public:
    int findTheLongestSubstring(string s) {
        vector<int> pre(32,INT_MAX);
        pre[0]=-1;
        const int N=s.size();
        int cur=0;
        int ans=0;
        for(int i=0;i<N;++i){
            switch(s[i]){
                case 'a':cur^=1;break;
                case 'e':cur^=2;break;
                case 'i':cur^=4;break;
                case 'o':cur^=8;break;
                case 'u':cur^=16;break;
                default:break;
            }
            if(pre[cur]==INT_MAX) pre[cur]=i;
            else ans=max(ans,i-pre[cur]);
        }
        return ans;
    }
};

// 作者:mnizy
// 鏈接:https://leetcode-cn.com/problems/find-the-longest-substring-containing-vowels-in-even-counts/solution/jian-dan-de-si-lu-by-mnizy/
// 來源:力扣(LeetCode)
// 著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

總結

這裏引入哈希表完全是爲了減少時間消耗,以空間來換時間。

常規題目中,前綴和指的是一個區間的和;非常規題目中,前綴和指的是某個元素出現的次數;像最後一題中的高級題目中,使用二進制位來表示前綴和。

參考資料

[1] Leetcode 560.和爲 K 的子數組
[2] 哈希表優化系列【空間換時間】-Leetcode 560.和爲 K 的子數組
[3] Leetcode 1248. 統計「優美子數組」
[4] Leetcode 1248. 統計「優美子數組」【記錄奇數位置&滑動窗口&前綴和】
[5] 【每日算法Day 107】面試必考:良心推薦,一題三解,不看後悔一輩子
[6] 523. 連續的子數組和
[7] Leetcode 523. 連續的子數組和【前綴和+同餘定理】
[8] 題解區:官方答案
[9] Leetcode 1124. 表現良好的最長時間段
[10] Leetcode 1371. 每個元音包含偶數次的最長子字符串
[11] Leetcode 1371 題解區:mnizy

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