前缀和 前缀积(k倍区间)
题目一
给定一个长度为$N$的数列,$a1,a2,a3,----an$ ,
如果其中一段连续的子序列 $ai,ai+1,ai+2,----aj$ 之和是$K$的倍数,我们就称这个区间 $[i,j]$是$K$倍区间。
你能求出数列中总共有多少个$K$倍区间吗?
输入格式
第一行包含两个整数 $N$ 和$K$。
以下$N$行每行包含一个整数 $Ai$ 。
输出格式
输出一个整数,代表 $K$倍区间的数目。
数据范围
1≤N,K≤100000 ,
1≤A i ≤100000
输入样例:
5 2
1
2
3
4
5
输出样例:
6
首先看到这个题看到这数据范围是一脸懵逼的,第一感觉一定是超时的
这个题解写的很好可以看看传送门
这里用到了知识点就是(sum[r]-sum[l])%k==sum[r]%k-sum[l]%k
所以我们只需要维护一个sum[i]%k就行,
就是每一次我们:
ans+=cnt[sum[i]]; cnt[sum[i]]++;
这个可以求出最后的答案,一定要注意一开始要初始化cnt[0]=1,或者最后加上cnt[0]
#include <iostream> #include <algorithm> using namespace std; const int N = 100010; typedef long long ll; int n, k, cnt[N], sum[N]; ll ans; int main(){ cnt[0] = 1; scanf("%d%d", &n, &k); for(int i = 1; i <= n; i++){ scanf("%d", &sum[i]); sum[i] += sum[i - 1] % k; //数很大,要不断取模,事实上影响能否整除这一性质的只有余数,所以保存取模的结果即可 ans += cnt[sum[i] % k]; //找前面有多少个j cnt[sum[i] % k]++; //对于当前这个i要计数 } printf("%lld\n", ans); return 0; }
题目二:
给定一个长度为$n$且不包含$0$的整数序列 $a1 ,a2 ,…,an$。
请你计算以下两值:
- 使得$al×al+1×…×ar$为负的索引对$(l,r)(l≤r)$的数量。
- 使得$al×al+1×…×ar$为正的索引对$(l,r)(l≤r)$的数量。
输入格
第一行一个整数 $n$ 。
第二行包含$n$个整数 $a1 ,…,an$ 。
输出格式
共一行,输出单个空格隔开的两个整数,分别表示负的索引对数和正的索引对数。
数据范围
1≤n≤2×1e5
−1e9≤ai≤1e9,ai≠0
。
输入样例1:
5
5 -3 3 -1 1
输出样例1:
8 7
输入样例2:
10
4 2 -4 3 1 2 -4 3 2 3
输出样例2:
28 27
输入样例3:
5
-1 -2 -3 -4 -5
输出样例3:
9 6
这个题可以用上一个前缀和的思想来解决这个题,就是不考虑大小只考虑正负的话
维护一个sum[i]为前i个数乘积的正负,如果大于0的话那么找一下前面的sum[j]的正数的个数,
就是这个a[i]为正数的贡献,我们可以用中的区间减去正的或者负的这一得到另一个,就是这样。
这个前缀乘积就是和上面的思想是差不多的
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
int n;
int main()
{
scanf("%d", &n);
LL rp = 0, rn = 0;
int sp = 1, sn = 0, s = 1;
while (n -- )
{
int a;
scanf("%d", &a);
if (a < 0) s *= -1;
if (s > 0) rp += sp, rn += sn, sp ++ ;
else rp += sn, rn += sp, sn ++ ;
}
printf("%lld %lld\n", rn, rp);
return 0;
}
题目三:
525. 连续数组
给定一个二进制数组, 找到含有相同数量的 0 和 1 的最长连续子数组(的长度)。
示例 1:
输入: [0,1]
输出: 2
说明: [0, 1] 是具有相同数量0和1的最长连续子数组。
示例 2:
输入: [0,1,0]
输出: 2
说明: [0, 1] (或 [1, 0]) 是具有相同数量0和1的最长连续子数组。
注意: 给定的二进制数组的长度不会超过50000。
class Solution { public: int findMaxLength(vector<int>& nums) { int n=nums.size(); vector<int>v(n); unordered_map<int,int>mp; mp[0]=-1; int sum=0,ans=0; for(int i=0;i<n;i++){ if(nums[i]==1){ sum+=1; } else sum-=1; if(!mp.count(sum)){ mp[sum]=i; } else{ ans = max(ans,i-mp[sum]); } } return ans; } };
题目四:
560. 和为K的子数组
给定一个整数数组和一个整数k,你需要找到该数组中和为k的连续的子数组的个数。
示例 1 :
输入:nums = [1,1,1], k = 2
输出: 2 , [1,1] 与 [1,1] 为两种不同的情况。
说明 :
数组的长度为 [1, 20,000]。
数组中元素的范围是 [-1000, 1000] ,且整数 k 的范围是 [-1e7, 1e7]。
这个题也是一样的,就是用mp存一个前缀和出现的次数然后每次求一下这个ans+=mp[sum-k];
然后就是需要注意一下这个mp[0]=1;其实还是前缀和的应用
class Solution { public: int subarraySum(vector<int>& nums, int k) { unordered_map<int,int>mp; int n=nums.size(); mp[0]=1; int sum=0; int ans=0; for(int i=0;i<n;i++){ sum+=nums[i]; ans+=mp[sum-k]; mp[sum]++; } return ans; } };
题目五:
930. 和相同的二元子数组
给你一个二元数组$nums$,和一个整数$goal$,请你统计并有多少个和为$goal$的 非空 子数组。
子数组 是数组的一段连续部分。
示例 1:
输入:nums = [1,0,1,0,1], goal = 2
输出:4
解释:
如下面黑体所示,有 4 个满足题目要求的子数组:
[1,0,1,0,1]
[1,0,1,0,1]
[1,0,1,0,1]
[1,0,1,0,1]
示例 2:
输入:nums = [0,0,0,0,0], goal = 0
输出:15
提示:
1 <= nums.length <= 3 * 104
nums[i] 不是 0 就是 1
0 <= goal <= nums.length
这个题也是很简单的
class Solution { public: int numSubarraysWithSum(vector<int>& nums, int S) { unordered_map<int,int> mp; mp[0] ++; int ans = 0, s = 0; for(auto x:A){ s += x; ans += mp[s - S]; mp[s] ++; } return ans; } };
题目六:
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]
提示:
1 <= A.length <= 30000
-10000 <= A[i] <= 10000
2 <= K <= 10000
这个题还是一个前缀和的题目sum[r]-sum[l])%k==sum[r]%k-sum[l]%k
然后你要(sum[r]-sum[l])%k==0,然后就是sum[r]%k==sum[l]%k的数量
然后对于每一个值i,都有count += m[sum];//取余之后的
class Solution { public: int subarraysDivByK(vector<int>& A, int K) { unordered_map<int,int> m;//和,出现次数 m[0] = 1;//0出现1次 int count = 0, sum = 0; for(int i = 0; i < A.size(); ++i) { // sum += A[i]+10000*K;//+10000*K,防止负数 sum += (A[i]%K+K)%K;//更专业的写法,效果同上 sum %= K;//除余sum在 0-K-1之间 count += m[sum];//前面 0 - K-1的状态有多少个,之间的连续和可被K整除 m[sum]++; } return count; } };
题目七:
给你一个正整数数组$nums$,请你移除最短子数组(可以为 空),使得剩余元素和能被$p$整除。不允许将整个数组都移除。
请你返回你需要移除的最短子数组的长度,如果无法满足题目要求,返回 $-1$ 。
子数组 定义为原数组中连续的一组元素。
示例 1:
输入:nums = [3,1,4,2], p = 6
输出:1
解释:nums 中元素和为 10,不能被 p 整除。我们可以移除子数组 [4] ,剩余元素的和为 6 。
示例 2:
输入:nums = [6,3,5,2], p = 9
输出:2
解释:我们无法移除任何一个元素使得和被 9 整除,最优方案是移除子数组 [5,2] ,剩余元素为 [6,3],和为 9 。
示例 3:
输入:nums = [1,2,3], p = 3
输出:0
解释:和恰好为 6 ,已经能被 3 整除了。所以我们不需要移除任何元素。
示例 4:
输入:nums = [1,2,3], p = 7
输出:-1
解释:没有任何方案使得移除子数组后剩余元素的和被 7 整除。
示例 5:
输入:nums = [1000000000,1000000000,1000000000], p = 3
输出:0
提示:
1 <= nums.length <= 1e5
1 <= nums[i] <= 1e9
1 <= p <= 1e9
这个题还是用到(sum[r]-sum[l])%k==sum[r]%k-sum[l]%k
总体思路:用哈希表存储前缀和余数,通过余数找待移除的子数组
看不懂没关系往下看就懂了
首先,整个数组的和对p取余数,记录余数mod
然后开始遍历数组,求前缀和
对于前缀和数组,用哈希表记录其对p的余数及索引
也就是key为前缀和子数组的余数sub_mod,val为该前缀和数组的末尾索引i
在求前缀和及其余数的过程中,我们想要找哈希表中是否已经存在和当前余数相差为mod的前缀和子数组
因为如果两个前缀和子数组余数相差为mod,证明这两个数组相差部分构成的子数组就是对p取余数为mod的子数组
把这个子数组去掉,数组的剩余部分的和就可以被p整除
取满足条件的子数组的最小长度
还有一些细节如下,不想看的话可以直接看代码
i. 如果余数mod本身就是0无需移除子数组,返回0
ii. res已经为1的话可以进行剪枝,因为已经是最小的了,但也要注意如果数组长度也是1的话不可以剪枝,因为这样就把整个数组都移除了
iii. res如果为n的话说明没有符合条件的子数组,题目说了不允许将整个数组都移除,因此返回-1
iv. sub_mod可能比mod要小,为了避免target出现负数,target中多加了一个p,也就是target = (sub_mod - mod + p) % p
这个题我也没有理解为什么这样取连续的一段是可以的有待理解
所以说还是自行理解把
typedef long long ll; class Solution { public: int minSubarray(vector<int>& nums, int p) { ll mod=0,sum=0; int n=nums.size(); for(auto x: nums){ mod = (mod + nums[i])%p; sum += nums[i]; } if(mod==0){ return 0; } if(sum<p){ return -1; } unordered_map<int, int> mp; ll s=0; int minlen = INT_MAX; mp[0]=-1; for(int i=0;i<n;i++){ s=(s+nums[i])%p; int z=(s-mod+p)%p; if(mp.find(target) != mp.end()) { minlen = min(minlen, i-mp[target]); } mp[s] = i; } return minlen >= nums.size() ? -1 : minlen; } };
还有一好多类似的题传送门
LeetCode 523. 连续的子数组和(求余 哈希)
LeetCode 525. 连续数组(前缀和+哈希)
LeetCode 560. 和为K的子数组(前缀和差分)
LeetCode 974. 和可被 K 整除的子数组(哈希map)
LeetCode 1248. 统计「优美子数组」(要复习)