几道用 前缀和+哈希表优化【必要时加入状态压缩】来解决连续子数组的题目小结

5 篇文章 0 订阅
5 篇文章 0 订阅

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值