前缀和&差分

前缀和

一维前缀和

输入整数n,q(1<=n,q<=105)n,q(1<=n,q<=10^5),和长为nn的数组aa。
接下来qq行,每行输入两个数l,r(1<=l<=r<=n)l,r(1<=l<=r<=n),要求输出区间[l,r][l,r]内所有元素的和,即∑i=lrai\sum_{i=l}^{r} a_i。

NOTE · 说明

题目描述一般都是1下标,笔者写代码一般都是0下标,故本文所有题目描述均采用1下标,代码部分均为0下标。0下标指将数组第一个元素放到a[0]a[0],1下标指将第一个元素放到a[1]a[1],将a[0]a[0]空出来

暴力做法就是对于每一个询问,遍历ll到rr求和,但这样时间复杂度是O(nq)O(nq),最坏情况下会超时。

这里介绍一下前缀和算法,前缀和是一种常用的算法预处理技巧,通过提前计算数组前若干个元素的累加和,实现快速的区间求和。

注意到

∑i=lrai=∑i=0rai−∑i=0l−1ai。\sum_{i=l}^{r} a_i = \sum_{i=0}^{r} a_i - \sum_{i=0}^{l-1} a_i。

任意一个区间和,都可以转换为两个前缀和相减。
我们可以先预处理所有的前缀和,进而O(1)O(1) 的求出所有区间和,总时间复杂度O(n+q)O(n + q),空间复杂度O(n)O(n)。

定义前缀和数组ss,其中s[i]s[i]表示前ii个元素的和。
初始s[0]=0s[0] = 0,递推公式s[i+1]=s[i]+a[i]s[i + 1] = s[i] + a[i]。

s[0]=0s[1]=a[0]s[2]=a[0]+a[1]⋮s[n]=a[0]+a[1]+⋯+a[n−1]\begin{aligned} s[0] &= 0\\ s[1] &= a[0]\\ s[2] &= a[0]+a[1]\\ &\mathrel{\vdots}\\ s[n] &= a[0]+a[1]+\cdots+a[n-1] \end{aligned}

则有 ∑i=lrai=s[r+1]−s[l]\sum_{i=l}^{r} a_i = s[r + 1] - s[l]

TIP · 技巧

这里定义s[0]=0s[0] = 0,表示前0个数的和为0,避免l=0l=0时需要特判。

例:

a=[1,3,2,5,4] a = [1,3,2,5,4]

对应前缀和数组为

s=[0,1,4,6,11,15] s = [0,1,4,6,11,15]

查询区间[1,3][1,3]的区间和

a[1]+a[2]+a[3]=3+2+5=10=s[4]−s[1] a[1] + a[2] + a[3] = 3 + 2 + 5 = 10 = s[4] - s[1]

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main()
{
int n,q;
cin >> n >> q;
vector<int> a(n);
for (int i = 0;i < n;i++) cin >> a[i];
vector<ll> pre(n + 1);//pre 对应前文的 s
//预处理前缀和数组,时间复杂度O(n)
for (int i = 0;i < n;i++) pre[i + 1] = pre[i] + a[i];
//q次询问,每次询问O(1),时间复杂度O(q)
while (q--)
{
int l,r;
cin >> l >> r;
l--;r--;
cout << pre[r + 1] - pre[l] << endl;
}
}
TIP · 技巧

提示:除了加法以外,还有一些操作也可以运用这种前缀思想,例如前缀异或和等

课后练习

题目 难度 考查知识点
洛谷 P8218 求区间和 ⭐
💡 查看提示一维前缀和模板
LeetCode 724 寻找数组中心下标 ⭐
💡 查看提示左右两侧前缀和
LeetCode 3427 变长子数组求和 ⭐⭐
💡 查看提示区间和计算
LeetCode 1310 子数组异或查询 ⭐⭐
💡 查看提示前缀异或
Codeforces 1539B Love Song ⭐⭐
💡 查看提示字符映射与前缀和

二维前缀和

输入n,m,q(1<=n,m,q<=105,1<=n∗m<=105)n,m,q(1<=n,m,q<=10^5,1<=n*m<=10^5)和一个n∗mn*m的矩阵,接下来qq行,每行给出一个矩阵的左上角和右下角坐标。
输出qq行,每行一个整数,表示这个矩阵的和

暴力做法是遍历每个查询的矩阵,时间复杂度O(nmq)O(nmq),会超时。

按照之前一维前缀和的思路,这里可以用二维前缀和,具体见下图。时间复杂度O(nm+q)O(nm + q)。

alt text

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int main()
{
int n,m,q;
cin >> n >> m >> q;
vector<vector<int>> a(n,vector<int>(m));
for (int i = 0;i < n;i++)
{
for (int j = 0;j < m;j++)
{
cin >> a[i][j];
}
}
vector<vector<ll>> pre(n + 1,vector<ll>(m + 1));//这里pre对应图中sum
//预处理 时间复杂度O(nm)
for (int i = 0;i < n;i++)
{
for (int j = 0;j < m;j++)
{
pre[i + 1][j + 1] = pre[i + 1][j] + pre[i][j + 1] - pre[i][j] + a[i][j];
}
}
//q次询问,每次询问O(1)查询,时间复杂度O(q)
while (q--)
{
int x1,x2,y1,y2;
cin >> x1 >> y1 >> x2 >> y2;//(x1,y1)对应左上角坐标,(x2,y2)对应右下角坐标
x1--;y1--;x2--;y2--;
ll ans = pre[x2 + 1][y2 + 1] - pre[x2 + 1][y1] - pre[x1][y2 + 1] + pre[x1][y1];
cout << ans << endl;
}
}

课后练习

题目 难度 考查知识点
LeetCode 304 二维区域和检索 ⭐
💡 查看提示二维前缀和模板,利用容斥原理计算子矩阵和
LeetCode 1314 矩阵区域和 ⭐⭐
💡 查看提示二维前缀和,注意处理矩阵边界
洛谷 P2004 领地选择 ⭐⭐
💡 查看提示枚举固定大小的子矩阵,利用二维前缀和快速求和
洛谷 P1387 最大正方形 ⭐⭐⭐
💡 查看提示二维前缀和判断某个正方形是否全部为 1,枚举边长与位置

前缀和与哈希表

输入n(1<=n<=105),k(−107<=k<=107)和一个长为n的数组a(−1000<=a[i]<=1000),输入n(1<=n<=10^5),k(-10^7<=k<=10^7)和一个长为n的数组a(-1000<=a[i]<=1000),
输出数组中和为k的子数组个数.输出数组中和为k的子数组个数.

暴力做法是统计所有子数组的和,如果和为kk,ans++ans++,最后输出ansans,时间复杂度O(n2)O(n^2)。

NOTE · 说明

长为n的数组一共有n∗(n+1)2\frac{n*(n+1)}{2}个非空子数组。
当左端点为1时,右端点可以为1,2,...,n1,2,...,n,总共nn个子数组
当左端点为2时,右端点可以为2,3,...,n2,3,...,n,总共n−1n-1个子数组
当左端点为n时,右端点可以为nn,总共11个子数组
故总共有∑i=1ni=n∗(n+1)2\sum_{i=1}^{n} i = \frac{n*(n+1)}{2}个子数组

根据我们刚刚求区间和的思路,我们需要求满足s[r+1]−s[l]==ks[r + 1] - s[l] == k的不同(l,r)(l,r)的数量。即求s[r+1]−k==s[l]s[r + 1] - k == s[l]的(l,r)(l,r)数量,我们可以创建一个哈希表用来统计数量,接着我们遍历前缀和数组,假设遍历到下标ii,记x=s[i]x = s[i],则满足要求的数字为x−kx-k,所有满足s[j]=x−ks[j]=x-k的下标jj都可以与ii配对,故答案加上cnt[x−k]cnt[x-k],接着cnt[x]++cnt[x]++。时间复杂度O(n)O(n)

WARNING · 警告

只有位于当前位置之前的前缀和才能与当前前缀和组成合法的非空子数组,因此必须先查询答案,再更新哈希表。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main()
{
int n,k;
cin >> n >> k;
vector<int> a(n);
for (int i = 0;i < n;i++) cin >> a[i];
vector<ll> s(n + 1);
//预处理 时间复杂度O(n)
for (int i = 0;i < n;i++) s[i + 1] = s[i] + a[i];
ll ans = 0;
unordered_map<ll,ll> cnt;
//遍历前缀和数组,时间复杂度O(n)
for (ll x : s)
{
//cnt[y] 表示当前遍历位置之前,前缀和 y 出现的次数。我们必须先查询、再更新,保证左右端点不重合,并且子数组非空。
ans += cnt[x - k];
cnt[x]++;
}
cout << ans << endl;
}

课后练习

题目 难度 考查知识点
LeetCode 560 和为 K 的子数组 ⭐
💡 查看提示前缀和 + 哈希表模板,统计此前出现过的目标前缀和
LeetCode 930 和相同的二元子数组 ⭐⭐
💡 查看提示前缀和 + 哈希表,统计和等于 goal 的子数组数量
LeetCode 525 连续数组 ⭐⭐
💡 查看提示将 0 转换为 -1,问题转化为寻找和为 0 的最长子数组,哈希表记录前缀和首次出现的位置
LeetCode 974 和可被 K 整除的子数组 ⭐⭐⭐
💡 查看提示利用同余性质,统计模 K 后余数相同的前缀和,注意负数取模

差分

一维差分

输入n,q(1<=n,q<=105)n,q(1<=n,q<=10^5)和长为n的数组a(−109<=a[i]<=109)(-10^9<=a[i]<=10^9),接下来q行,每行三个整数l,r,x(1<=l<=r<=n,−109<=x<=109)l,r,x(1<=l<=r<=n,-10^9<=x<=10^9)表示将下标在[l,r][l,r]的元素加上x。
输出一行n个整数,表示进行所有操作后的数组aa。

暴力做法是对于每个询问,遍历数组aa的[l,r][l,r],给每个数加上x。时间复杂度O(nq)O(nq),会超时。
这里引入一个新的概念——差分

对于数组aa,相邻元素两两做差,再在前面补上a[0]a[0],就得到了差分数组dd
例:a=[1,3,3,5,8]a = [1,3,3,5,8],d=[1,2,0,2,3]d = [1,2,0,2,3]

差分与前缀和的关系就类似积分与导数的关系,对差分数组求前缀和就能得到原数组。

那么差分数组有什么性质呢?

假设我们将数组a中[1,3][1,3]的元素加上3,得到新数组a′=[1,6,6,8,8]a' = [1,6,6,8,8],新差分数组d′=[1,5,0,2,0]d' = [1,5,0,2,0]
再将数组a′中[2,4]a'中[2,4]的元素加上5,得到新数组a′′=[1,6,11,13,13]a'' = [1,6,11,13,13],新差分数组d′′=[1,5,5,2,0]d'' = [1,5,5,2,0]

注意到,改变原数组[1,3][1,3]时,差分数组d[1]d[1]加了3,d[2],d[3]d[2],d[3]没变,d[4]d[4]减了3。
改变原数组[2,4][2,4]时,差分数组d[2]d[2]加了5,d[3],d[4]d[3],d[4]没变。

由此我们可以得出,对于原数组[l,r][l,r]加上xx时,对应的差分数组d[l]+=x,d[r+1]−=xd[l]+=x,d[r+1]-=x。

因此对于这道题,我们可以先求出原数组a的差分数组,再在差分数组上进行这些操作,最后再对差分数组求前缀和从而得到新数组。时间复杂度O(n+q)O(n + q)。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int main()
{
int n,q;
cin >> n >> q;
vector<int> a(n);
for (int i = 0;i < n;i++) cin >> a[i];
vector<ll> d(n + 1);//这里开n+1是为了防止r等于n时越界
d[0] = a[0];
//预处理 时间复杂度O(n)
for (int i = 1;i < n;i++) d[i] = a[i] - a[i - 1];
//q次区间修改,每次修改O(1),时间复杂度O(q)
while (q--)
{
int l,r,x;
cin >> l >> r >> x;
l--;r--;
d[l] += x;
d[r + 1] -= x;
}
ll ans = 0;
for (int i = 0;i < n;i++)
{
ans += d[i];
cout << ans << " ";
}
cout << endl;
}

课后练习

题目 难度 考查知识点
洛谷 P2367 语文成绩 ⭐
💡 查看提示一维差分模板,区间加法修改,最后还原数组求最小值
LeetCode 1094 拼车 ⭐⭐
💡 查看提示差分数组处理区间人数变化,注意乘客在终点下车,区间为左闭右开
LeetCode 1109 航班预订统计 ⭐⭐
💡 查看提示多次区间加法操作,最后通过前缀和还原每个航班的预订数量