前缀和的一个应用是差分,差分是前缀和的逆运算。
定义一个数组D[] D[i]=a[i]-a[i-1] a是原始数组 可以表示前缀和
一般定义D的大小为 size+1(size为a数组的大小),初始全为0
区间修改
//[L,R]中每个元素加d
D[L]+=d;
D[R+1]-=d;
利用查分数组,把O(n)区间修改变为O(1)的端点修改。
k值区间
问题描述
给你一个整数数组 nums
和一个整数 k
,请你统计并返回 该数组中和为 k
的子数组的个数 。
子数组是数组中元素的连续非空序列。
思路分析
用哈希表mp[i]记录前缀和为i的个数,mp[0]初始为1,每个区间[l,r]的元素和为s=sum[r]-sum[l-1]
,每次遍历得到一个sum[r],要知道
s=k的右边界为r的区间的个数,只需知道前面共有多少个前缀和sum[l-1]
等于sum[r]-k
即可
代码
int subarraySum(vector<int>& nums, int k) {
unordered_map<int, int> mp;
mp[0] = 1;
int count = 0, pre = 0;
for (auto& x:nums) {
pre += x;
if (mp.count(pre - k)) {
count += mp[pre - k];
}
mp[pre]++;
}
return count;
}
k倍区间
问题描述
给定长度为n的数列a1,a2,a3…,其中一段[i,j] (i<=j)区间和是k的倍数,我们称[i,j]为k倍区间。
求给定数列的k倍区间数量。
思路
定义前缀和sum[i]
为区间 [1,i]
的元素和 (sum[0]=0)。任意区间和 a[i,j]=sum[j]-sum[i-1]
若遍历左右区间边界i,j求解时间复杂度为O(n^2).会超时。
可利用一个数学技巧降低时间复杂度。
两个数除k的余数相等时,他们相减的差一定是k的倍数
例 (a=7,b=4,k=3)
我们可以枚举一个右边界 因为 任意区间和 a[i,j]=sum[j]-sum[i-1]
而sum[j]
是固定的 (余k得数固定,设为a),我们只需求出[1,j]中有多少个 子前缀和的余k得数为a 便能求得以j为右边界有多少区间为k倍区间。
代码
#include<iostream>
#include<cstdio>
#include<vector>
#define ll long long
using namespace std;
int main(){
int n,k;
cin>>n>>k;
vector<ll>sum(n+1);
vector<int>cnt(k);
cnt[0]=1; //初始cnt[0]=1,免去特殊情况的处理。
ll tar=0;
for(int i=1;i<=n;i++){
cin>>sum[i];
sum[i]+=sum[i-1];
tar+=cnt[sum[i]%k];
cnt[sum[i]%k]++; //余数为sum[i]%k的前缀和数加1.
}
cout<<tar;
return 0;
}
长度k倍区间
给你一个整数数组 nums
和一个整数 k
。
返回 nums
中一个 非空子数组 的 最大 和,要求该子数组的长度可以 被 k
整除 。
思路分析
与上一题不同,这题求的是子数组长度为k的倍数的最大子数组和。
相似地,区间和a[i,j]=sum[j]-sum[i-1]
,要使a[i,j]
最大,sum[i-1]
越小越好,同时要满足长度是k的倍数。
可以定义一个数组minn
,minn[i]
记录了当前遍历到的所有下标余k等于i的元素中的最小值。
若区间和a[i,j]
的长度(j-i+1
)为k
的倍数,其满足j
和i-1
余k的结果相同(等于j%k),而minn[j%k]
中存储了满足条件的最小sum[i-1]
,此时sum[j]-minn[j%k]
即是以j
为右边界长度为k
的倍数的最大子数组和。依次遍历右边界,更新历史最大值ans,最后即可求得答案。
具体实现时,minn[0]~minn[k-2]
初始值为对应的nums
值,minn[k-1]
为0
,ans
初始值为LONG_MIN
。
代码
long long maxSubarraySum(vector<int>& nums, int k) {
int n=nums.size();
vector<long long>minn(k);
long long ans=LONG_MIN,sum=0;
for(int i=0;i<k-1;i++){
sum+=nums[i];
minn[i]=sum;
}
minn[k-1]=0;
for(int i=k-1;i<n;i++){
sum+=nums[i];
ans=max(sum-minn[i%k],ans);
minn[i%k]=min(minn[i%k],sum);
}
return ans;
}
零数组变换|
问题描述
给定一个长度为 n
的整数数组 nums
和一个二维数组 queries
,其中 queries[i] = [li, ri]
。
对于每个查询 queries[i]
:
- 在
nums
的下标范围[li, ri]
内选择一个下标子集。 - 将选中的每个下标对应的元素值减 1。
零数组 是指所有元素都等于 0 的数组。
如果在按顺序处理所有查询后,可以将 nums
转换为 零数组 ,则返回 true
,否则返回 false
。
数组的 子集 是对数组元素的选择(可能为空)。
思路分析
定义一个数组sum
,sum[i]
存储的是对nums[i]
的最大减量。
遍历queries
数组,对于每个区间[l,r]
,对sum
中该区间的所有元素都加一,这可以用差分在O(1)
时间复杂度内实现。
定义一个差分数组d,d[i]=sum[i]-sum[i-1]
最后遍历sum
数组,如果sum[i]<nums[i]
,说明无法将该元素减为0,也就返回false
;遍历到最后,说明所有元素都可减为0,返回true
。
时间复杂度O(n+m)
。
代码
bool isZeroArray(vector<int>& nums, vector<vector<int>>& queries) {
int n=nums.size(),m=queries.size();
vector<int>d(n+1); //差分数组
for(int i=0;i<m;i++){
d[queries[i][0]]++; d[queries[i][1]+1]--;
}
int sum=0;
for(int i=0;i<n;i++){
sum+=d[i]; //sum为对当前元素nums[i]最大的减量
if(sum<nums[i]) return false;
}
return true;
}
零数组变换||
问题描述
给你一个长度为 n
的整数数组 nums
和一个二维数组 queries
,其中 queries[i] = [li, ri, vali]
。
每个 queries[i]
表示在 nums
上执行以下操作:
- 将
nums
中[li, ri]
范围内的每个下标对应元素的值 最多 减少vali
。 - 每个下标的减少的数值可以独立选择。
零数组 是指所有元素都等于 0 的数组。
返回 k
可以取到的 最小非负值,使得在 顺序 处理前 k
个查询后,nums
变成 零数组。如果不存在这样的 k
,则返回 -1。
思路分析
与**零数组变换|**类似,定义虚拟减量数组sum的差分数组d,d[i]=sum[i]-sum[i-1]
遍历nums
数组,如果当前nums[i]
对应的最大减量sum[i]
<nums[i]
,则说明当前选择的区间不足以使nums数组变为零数组,需要内层向右遍历queries
继续选择区间,直到sum>=nums[i]或ans>=m
(ans为当前选择区间数)时结束。
如果内层遍历完,sum
还是小于nums[i]
,说明所有区间选择完,该元素还是不能置0,直接返回-1即可。
最后外层遍历结束,返回ans
。
时间复杂度O(n+m)
。
代码
int minZeroArray(vector<int>& nums, vector<vector<int>>& queries) {
int n=nums.size(),m=queries.size();
vector<int>d(n+1);
int ans=0,sum=0;
for(int i=0;i<n;i++){
sum+=d[i];
while(sum<nums[i]&&ans<m){
int x=queries[ans][0],y=queries[ans][1],z=queries[ans][2];
d[x]+=z;
d[y+1]-=z;
if(i>=x&&i<=y) sum+=z; //最大减量加z
ans++; //选择的区间数=1
}
if(sum<nums[i]) return -1;
}
return ans;
}
零数组变换|||
问题描述
给你一个长度为 n
的整数数组 nums
和一个二维数组 queries
,其中 queries[i] = [li, ri]
。
每一个 queries[i]
表示对于 nums
的以下操作:
- 将
nums
中下标在范围[li, ri]
之间的每一个元素 最多 减少 1 。 - 坐标范围内每一个元素减少的值相互 独立 。
零数组 指的是一个数组里所有元素都等于 0 。
请你返回 最多 可以从 queries
中删除多少个元素,使得 queries
中剩下的元素仍然能将 nums
变为一个 零数组 。如果无法将 nums
变为一个 零数组 ,返回 -1 。
思路分析
贪心+大顶堆+差分
题目问最多可以删除多少个元素,也就是说最少可保留多少个元素。
从左往右遍历nums数组,对于nums[i]=k,我们需要在queries中选择k个左端点小于等于i的区间,因为所选区间要最少,所以所选区间的右边界越大越好。
这启发我们用大顶堆维护左端点 ≤i 的未选区间的右端点。
代码实现时,还需要把 queries 按照左端点排序,这样我们便于遍历 左端点 ≤i 的区间。
代码
int maxRemoval(vector<int>& nums, vector<vector<int>>& queries) {
ranges::sort(queries, {}, [](auto& a) { return a[0]; }); //按左端点升序排序
int n = nums.size(), j = 0, sum_d = 0;
vector<int> diff(n + 1, 0); //差分数组
priority_queue<int> pq; //存的是能删除的区间右边界
for (int i = 0; i < n; i++) {
sum_d += diff[i]; //当前元素的最大减量
while (j < queries.size() && queries[j][0] <= i) {
pq.push(queries[j][1]); //先将左边界小于等于i的区间的右边界值全部入队
j++;
}
while (sum_d < nums[i] && !pq.empty() && pq.top() >= i) { //队列中的区间左边界都是小于等于i的
diff[pq.top() + 1]--;
pq.pop(); //出队表示区间保留
sum_d++; //sum_d直接加1
}
if (sum_d < nums[i]) { //还是不能满足要求
return -1;
}
}
return pq.size();
}
- 时间复杂度:O(n+qlogq),其中 n 是 nums 的长度,q 是 queries 的长度。
疑问1:为什么在区间保留时不用执行 左区间边界对应的差分加一的操作?
答:在遍历到nums
的下标i
时,队列中的区间的左边界都大于等于i,执行了diff[left]++
(假设left为对应的左边界),是为了让sum_d++
,而这可以直接执行sum_d++
,在遍历后面的下标i
时,不会再用到diff[left]
,所以也就可以免去执行diff[left]++
的操作