前缀和与差分
第一讲 前缀和与差分
听讲人:24级算法组小萌新
卑微讲题人:22049212-梁军斌(本人正在疯狂整理入党材料中)
我认为,每个人最重要的不是过去,而是现在;虽然背负着昨日的重担,但也有自己想要追寻的东西。
希望你从今以后无论去往何处都不要忘记,世界上有那么多爱你的人。
你道你机关算尽,可你真算到了自己也要下场当棋子吗?
坐看日月行,细数千帆过。
年年今日,灯明如昼。原火不灭,愿人依旧。
先来看一道例题
题目描述
给定一个长度为 \(n\) 的整数序列。接下来输入 \(m\) 个询问,每个询问由一对整数 \(l, r\) 组成。
对于每个询问,需要计算并输出原序列中从第 \(l\) 个数到第 \(r\) 个数的和。
输入格式
- 第一行包含两个整数 \(n\) 和 \(m\)。
- 第二行包含 \(n\) 个整数,表示整数数列。
- 接下来 \(m\) 行,每行包含两个整数 \(l\) 和 \(r\),表示一个询问的区间范围。
输出格式
- 共 \(m\) 行,每行输出一个询问的结果。
数据范围
- \(1 \leq l \leq r \leq n\)
- \(1 \leq n, m \leq 100000\)
- \(-1000 \leq\) 数列中元素的值 \(\leq 1000\)
示例
输入样例
5 3
2 1 3 6 4
1 2
1 3
2 4
输出样例
3
6
10
参考代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
using i64 = long long;
const int N = 3e5+10;
void solve()
{
int n,m;
cin>>n>>m;
vector<int> a(n+1,0);
for(int i=1;i<=n;i++) cin>>a[i];
vector<int> s(n+1,0);
for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
while(m--)
{
int l,r;
cin>>l>>r;
cout<<s[r]-s[l-1]<<endl;
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int _=1;
//cin>>_;
while(_--)
{
solve();
}
}
前缀和
定义
前缀和可以简单理解为「数列的前 \(n\) 项的和」,是一种重要的预处理方式,能大大降低查询的时间复杂度。
\(C++\) 标准库中实现了前缀和函数 std::partial_sum
,定义于头文件 <numeric>
中。
设数列的前\(x\)项的和为\(S_x\)
-
已知一个长度为\(n\)的数组\(a\):
-
\(S_1\) = \(a_1\)
-
\(S_2\) = \(a_1\) + \(a_2\)
-
\(S_3\) = \(a_1\) + \(a_2\) + \(a_3\)
-
\(...\)
-
\(S_{n-1}\) = \(a_1\) + \(a_2\) + \(a_3\) + \(...\) + \(a_{n-1}\)
-
\(S_{n}\) = \(a_1\) + \(a_2\) + \(a_3\) + \(...\) + \(a_{n-1}\) + \(a_{n}\)
知道前缀和,便可以知道任意区间内所有数之和,即\(Sum_{l,r}\) = \(S_r\) - \(S_{l-1}\):
- \(S_{l-1}\) = \(a_1\) + \(a_2\) + \(a_3\) + \(...\) + \(a_{l-1}\)
- \(S_{r}\) = \(a_1\) + \(a_2\) + \(a_3\) + \(...\) + \(a_{r-1}\) + \(a_{r}\)
- \(Sum_{l,r}\) = \(a_l\) + \(a_{l+1}\) + \(a_{l+2}\) + \(...\) + \(a_{r}\) = \(S_r\) - \(S_{l-1}\);
例:
- \(Sum_{3,9}\) = \(S_9\) - \(S_2\)
- \(Sum_{3,7}\) = \(S_7\) - \(S_2\)
- \(Sum_{3,3}\) = \(S_3\) - \(S_2\)
如何求前缀和?
首先初始化\(a[0]=0\),随后遍历一遍数组\(a\),得到前缀和数组
a[0]=0;
for(int i = 1;i <= n;i ++)
{
a[i]+=a[i-1];
// s[i]=s[i-1]+a[i];
}
二维前缀和
基于容斥原理
这种方法多用于二维前缀和的情形。给定大小为 \(m\times n\) 的二维数组 \(A\),要求出其前缀和 \(S\)。那么,\(S\) 同样是大小为 \(m\times n\) 的二维数组,且
类比一维的情形,\(S_{i,j}\) 应该可以基于 \(S_{i-1,j}\) 或 \(S_{i,j-1}\) 计算,从而避免重复计算前面若干项的和。但是,如果直接将 \(S_{i-1,j}\) 和 \(S_{i,j-1}\) 相加,再加上 \(A_{i,j}\),会导致重复计算 \(S_{i-1,j-1}\) 这一重叠部分的前缀和,所以还需要再将这部分减掉。这就是 容斥原理。由此得到如下递推关系:
实现时,直接遍历 \((i,j)\) 求和即可。
考虑一个具体的例子。
这里,\(S\) 是给定矩阵 \(A\) 的前缀和。根据定义,\(S_{3,3}\) 是左图中虚线方框中的子矩阵的和。这里,\(S_{3,2}\) 是蓝色子矩阵的和,\(S_{2,3}\) 是红色子矩阵的和,它们重叠部分的和是 \(S_{2,2}\)。由此可见,如果直接相加 \(S_{3,2}\) 和 \(S_{2,3}\),会重复计算 \(S_{2,2}\),所以应该有
同样的道理,在已经预处理出二位前缀和后,要查询左上角为 \((i_1,j_1)\)、右下角为 \((i_2,j_2)\) 的子矩阵的和,可以计算
这可以在 \(O(1)\) 时间内完成。
在二维的情形,以上算法的时间复杂度可以简单认为是 \(O(mn)\),即与给定数组的大小成线性关系。但是,当维度 \(k\) 增大时,由于容斥原理涉及的项数以指数级的速度增长,时间复杂度会成为 \(O(2^kN)\),这里 \(k\) 是数组维度,而 \(N\) 是给定数组大小。因此,该算法不再适用。
逐维前缀和
对于一般的情形,给定 \(k\) 维数组 \(A\),大小为 \(N\),同样要求得其前缀和 \(S\)。这里,
从上式可以看出,\(k\) 维前缀和就等于 \(k\) 次求和。所以,一个显然的算法是,每次只考虑一个维度,固定所有其它维度,然后求若干个一维前缀和,这样对所有 \(k\) 个维度分别求和之后,得到的就是 \(k\) 维前缀和。
树上前缀和
设 \(\textit{sum}_i\) 表示结点 \(i\) 到根节点的权值总和。
然后:
-
若是点权,\(x,y\) 路径上的和为 \(\textit{sum}_x + \textit{sum}_y - \textit{sum}_\textit{lca} - \textit{sum}_{\textit{fa}_\textit{lca}}\)。
-
若是边权,\(x,y\) 路径上的和为 \(\textit{sum}_x + \textit{sum}_y - 2\cdot\textit{sum}_{lca}\)。
LCA 的求法参见 最近公共祖先。
题目描述
给定一个 \(n\) 行 \(m\) 列的整数矩阵,以及 \(q\) 个询问。每个询问包含四个整数 \(x_1, y_1, x_2, y_2\),表示一个子矩阵的左上角坐标和右下角坐标。
对于每个询问,输出该子矩阵中所有数的和。
输入格式
- 第一行包含三个整数 \(n\), \(m\), \(q\)。
- 接下来的 \(n\) 行,每行包含 \(m\) 个整数,表示整数矩阵。
- 接下来 \(q\) 行,每行包含四个整数 \(x_1, y_1, x_2, y_2\),表示一组询问。
输出格式
共 \(q\) 行,每行输出一个询问的结果。
数据范围
- \(1 \leq n, m \leq 1000\)
- \(1 \leq q \leq 200000\)
- \(1 \leq x_1 \leq x_2 \leq n\)
- \(1 \leq y_1 \leq y_2 \leq m\)
- \(-1000 \leq\) 矩阵内元素的值 \(\leq 1000\)
输入样例
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
输出样例
17
27
21
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
typedef long long ll;
int a[N][N];
int s[N][N];
int n,m,q;
int main()
{
cin>>n>>m>>q;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j],s[i][j]=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
s[i][j]=s[i-1][j]+s[i][j-1]+a[i][j]-s[i-1][j-1];
}
}
while(q--)
{
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
cout<<s[x2][y2]-s[x1-1][y2]-s[x2][y1-1]+s[x1-1][y1-1]<<endl;
}
}
差分
解释
差分是一种和前缀和相对的策略,可以当做是求和的逆运算。
这种策略的定义是令 \(b_i=\begin{cases}a_i-a_{i-1}\,&i \in[2,n] \\ a_1\,&i=1\end{cases}\)
性质
- \(a_i\) 的值是 \(b_i\) 的前缀和,即 \(a_n=\sum\limits_{i=1}^nb_i\)
- 计算 \(a_i\) 的前缀和 \(sum=\sum\limits_{i=1}^na_i=\sum\limits_{i=1}^n\sum\limits_{j=1}^{i}b_j=\sum\limits_{i=1}^n(n-i+1)b_i\)
它可以维护多次对序列的一个区间加上一个数,并在最后询问某一位的数或是多次询问某一位的数。注意修改操作一定要在查询操作之前。
譬如使 \([l,r]\) 中的每个数加上一个 \(k\),即
其中 \(b_l+k=a_l+k-a_{l-1}\),\(b_{r+1}-k=a_{r+1}-(a_r+k)\)
最后做一遍前缀和就好了。
C++ 标准库中实现了差分函数 std::adjacent_difference
,定义于头文件 <numeric>
中。
题目描述
输入一个长度为 \(n\) 的整数序列。
接下来输入 \(m\) 个操作,每个操作包含三个整数 \(l, r, c\),表示将序列中 \([l, r]\) 之间的每个数加上 \(c\)。
请你输出进行完所有操作后的序列。
输入格式
- 第一行包含两个整数 \(n\) 和 \(m\)。
- 第二行包含 \(n\) 个整数,表示整数序列。
- 接下来 \(m\) 行,每行包含三个整数 \(l, r, c\),表示一个操作。
输出格式
共一行,包含 \(n\) 个整数,表示最终序列。
数据范围
- \(1 \leq n, m \leq 100000\)
- \(1 \leq l \leq r \leq n\)
- \(-1000 \leq c \leq 1000\)
- \(-1000 \leq\) 整数序列中元素的值 \(\leq 1000\)
输入样例
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例
3 4 5 3 4 2
参考代码:
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n,m;
cin>>n>>m;
vector<int> a(n+1,0);
vector<int> b(n+1,0);
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++) b[i]=a[i]-a[i-1];
while(m--)
{
int l,r,c;
cin>>l>>r>>c;
b[r+1]-=c;
b[l]+=c;
}
for(int i=1;i<=n;i++) b[i]+=b[i-1];
for(int i=1;i<=n;i++) cout<<b[i]<<" \n"[i==n];
}
二维差分
题目描述
输入一个 \(n\) 行 \(m\) 列的整数矩阵,再输入 \(q\) 个操作,每个操作包含五个整数 \(x_1, y_1, x_2, y_2, c\),其中 \((x_1, y_1)\) 和 \((x_2, y_2)\) 表示一个子矩阵的左上角坐标和右下角坐标。
每个操作都要将选中的子矩阵中的每个元素的值加上 \(c\)。
请你将进行完所有操作后的矩阵输出。
输入格式
- 第一行包含整数 \(n, m, q\)。
- 接下来 \(n\) 行,每行包含 \(m\) 个整数,表示整数矩阵。
- 接下来 \(q\) 行,每行包含 5 个整数 \(x_1, y_1, x_2, y_2, c\),表示一个操作。
输出格式
共 \(n\) 行,每行 \(m\) 个整数,表示所有操作进行完毕后的最终矩阵。
数据范围
- \(1 \leq n, m \leq 1000\)
- \(1 \leq q \leq 100000\)
- \(1 \leq x_1 \leq x_2 \leq n\)
- \(1 \leq y_1 \leq y_2 \leq m\)
- \(-1000 \leq c \leq 1000\)
- \(-1000 \leq\) 矩阵内元素的值 \(\leq 1000\)
输入样例
3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1
输出样例
2 3 4 1
4 3 4 1
2 2 2 2
参考代码:
#include<bits/stdc++.h>
using namespace std;
int a[1010][1010];
int b[1010][1010];
int n,m,q;
int main()
{
cin>>n>>m>>q;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
cin>>a[i][j];
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
b[i][j]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1];
}
}
while(q--)
{
int x1,y1,x2,y2,c;
cin>>x1>>y1>>x2>>y2>>c;
b[x1][y1]+=c;
b[x2+1][y1]-=c;
b[x1][y2+1]-=c;
b[x2+1][y2+1]+=c;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
a[i][j]=a[i-1][j]+a[i][j-1]-a[i-1][j-1]+b[i][j];
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
cout<<a[i][j]<<" \n"[j==m];
}
}
习题
前缀和:
- 洛谷 B3612【深进 1. 例 1】求区间和
- 洛谷 U69096 前缀和的逆
- AtCoder joi2007ho_a 最大の和
- 「USACO16JAN」子共七 Subsequences Summing to Sevens
- 「USACO05JAN」Moo Volume S
二维/多维前缀和:
- HDU 6514 Monitor
- 洛谷 P1387 最大正方形
- 「HNOI2003」激光炸弹
- CF 165E Compatible Numbers
- CF 383E Vowels
- ARC 100C Or Plus Max
树上前缀和:
差分:
树上差分: