『学习笔记』前缀和与差分
今天我在 艰难地学废了前缀和与差分,其设计十分精妙,用于序列中进行各种操作的场景,例如区间加法、减法,求出区间和等等。
为方便表示,定义左上角为
前缀和
前缀和是一种重要的预处理,能大大降低查询的时间复杂度。可以简单理解为“数列的前
n 项的和”。 ——\texttt{OI-Wiki}
一维前缀和
定义一个数组
构造
我们要建立一个前缀和数组
由此我们可以得到构造
由于在 C++ 中,数组开在全局时默认全部为
我们可以一边读入一边建立数组,像这样:
for(int i=1; i<=n; i++){
cin >> a[i];
sum[i]=sum[i-1]+a[i];
}
那么可得前缀和数组
区间查询
假设要查询区间
可以由
cout << sum[r]-sum[l-1] << endl;
可见前缀和也是很简单的。
例题
U53525 前缀和(例题)
这题就和上面的栗子一模一样,输入
#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
T X=0; bool flag=1; char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
if(flag) return X;
return ~(X-1);
}
template<typename T=int>
inline void write(T X){
if(X<0) putchar('-'),X=~(X-1);
T s[20],top=0;
while(X) s[++top]=X%10,X/=10;
if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
putchar('\n');
}
int n,a[105],sum[105];
int main(){
cin >> n;
for(int i=1; i<=n; i++){
cin >> a[i];
sum[i]=sum[i-1]+a[i];
cout << sum[i] << " ";
}
puts("");
return 0;
}
但可以进行空间优化,使用滚动数组,因为每次只需用到
#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
T X=0; bool flag=1; char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
if(flag) return X;
return ~(X-1);
}
template<typename T=int>
inline void write(T X){
if(X<0) putchar('-'),X=~(X-1);
T s[20],top=0;
while(X) s[++top]=X%10,X/=10;
if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
putchar('\n');
}
int main(){
int n,a=0,b=0;
cin >> n;
while(n--){
cin >> b;
b+=a;
cout << b << " ";
a=b;
}
return 0;
}
P1115 最大子段和
上面两道题就是构造数组,这道相对综合一点。
给定一个长度为
由于要求的是连续子段和,很容易想到递推。也可以说是选择性的前缀和。
老样子,滚动数组,只不过滚动的是前缀和数组,代码中的
至于
代码:
#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
T X=0; bool flag=1; char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
if(flag) return X;
return ~(X-1);
}
template<typename T=int>
inline void write(T X){
if(X<0) putchar('-'),X=~(X-1);
T s[20],top=0;
while(X) s[++top]=X%10,X/=10;
if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
putchar('\n');
}
int n,a,b,c,ans=-0x7fffffff;
int main(){
cin >> n;
for(int i=1; i<=n; i++){
cin >> a;
c=max(b+a,a);
ans=max(c,ans);
b=c;
}
cout << ans << endl;
return 0;
}
习题
- P1114 “非常男女”计划
- P3131 [USACO16JAN]Subsequences Summing to Sevens S
- P3406 海底高铁
- P5638 【CSGRound2】光骓者的荣耀
- P6568 [NOI Online #3 提高组] 水壶
二维前缀和
我们看
设
构造
现在的第一个问题是如何建立这个数组。
图中应该说得很清楚了,于是我们得到公式:
区间查询
如何截取一个矩形?也很简单。
设要截取的矩形的长为
可以发现,这和上面的构造差不多。查询公式(设要求矩阵为
例题
U184421 退钱
是的,你没看错。这就是我那道 WFOI 被毙掉的题(真的好垃)。
有一个
- 矩阵的数的总和
sum \le m ,输出Oh no!。 -
怎么样,跟板子没区别吧?
官方题解,应该还可以吧?
P1387 最大正方形
我们要在
这题数据范围很小,
首先构造前缀和数组,不用多说,直接套:
两层循环,
题目要求输出正方形,再套一层循环,变量
每次判断:
代码:
#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
T X=0; bool flag=1; char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
if(flag) return X;
return ~(X-1);
}
template<typename T=int>
inline void write(T X){
if(X<0) putchar('-'),X=~(X-1);
T s[20],top=0;
while(X) s[++top]=X%10,X/=10;
if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
putchar('\n');
}
int n,m,a[105][105],sum[105][105],ans;
int main(){
cin >> n >> m;
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
cin >> a[i][j];
sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j];
}
}
for(int i=1; i<n; i++){
for(int j=1; j<m; j++){
for(int l=1,t=min(n-i,m-j); l<=t; l++){
if(sum[i+l][j+l]-sum[i+l][j]-sum[i][j+l]+sum[i][j]==l*l){
ans=max(ans,l);
}
}
}
}
cout << ans << endl;
return 0;
}
习题
- P1719 最大加权矩形
- P2004 领地选择
- P2280 [HNOI2003]激光炸弹
- P3397 地毯
差分
差分是一种和前缀和相对的策略,可以当做是求和的逆运算。
——
OI-Wiki
上面提到过差分,差分是前缀和的逆运算,也就是将一个前缀和数组转换成原数组。
一维差分
差分可以用来解决区间加法之类的问题,但中间如果要修改就不太适合了。
一般情况下,使用差分,会将原数组看作一个前缀和数组,然后将每一项差分成这个前缀和数组的原数组,递推式为:
如何修改呢?我们可以分析一下原数组改动对前缀和数组的影响(
这式修改
构造差分数组:
for(int i=1; i<=n; i++){
cin >> a[i];
dif[i]=a[i]-a[i-1];
}
区间加(将区间
dif[l]+=k;
dif[r+1]-=k;
最后求原数组时,只需对
通过这些精妙的计算,可以快速地实现区间加法,并在最后使用
例题
U69096 前缀和的逆
读入前缀和数组
正常爆栈数组版代码:
#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
T X=0; bool flag=1; char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
if(flag) return X;
return ~(X-1);
}
template<typename T=int>
inline void write(T X){
if(X<0) putchar('-'),X=~(X-1);
T s[20],top=0;
while(X) s[++top]=X%10,X/=10;
if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
putchar('\n');
}
int n,a[105],sum[105];
int main(){
cin >> n;
for(int i=1; i<=n; i++){
cin >> sum[i];
a[i]=sum[i]-sum[i-1];
cout << a[i] << " ";
}
puts("");
return 0;
}
滚动数组版代码:
#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
T X=0; bool flag=1; char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
if(flag) return X;
return ~(X-1);
}
template<typename T=int>
inline void write(T X){
if(X<0) putchar('-'),X=~(X-1);
T s[20],top=0;
while(X) s[++top]=X%10,X/=10;
if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
putchar('\n');
}
int n,a,b;
int main(){
cin >> n;
while(n--){
cin >> b;
int t=a;
a=b;
b-=t;
cout << b << " ";
}
return 0;
}
这一题是建立差分数组的题。
话说明明到底男的还是女的?
P2367 语文成绩
题目大意:有一个长度为
很简单,对原数列进行差分,每次操作区间加,最后进行前缀和操作,可以一边进行前缀和一边找最小值,具体见代码。
#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
T X=0; bool flag=1; char ch=getchar();
while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
if(flag) return X;
return ~(X-1);
}
template<typename T=int>
inline void write(T X){
if(X<0) putchar('-'),X=~(X-1);
T s[20],top=0;
while(X) s[++top]=X%10,X/=10;
if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
putchar('\n');
}
int n,p,a[5000005],dif[5000005],ans=0xfffffff;
int main(){
cin >> n >> p;
for(int i=1; i<=n; i++){
cin >> a[i];
dif[i]=a[i]-a[i-1]; // 构造差分数组
}
while(p--){
int x,y,z;
cin >> x >> y >> z;
dif[x]+=z,dif[y+1]-=z; // 实现区间加
}
for(int i=1; i<=n; i++){
a[i]=a[i-1]+dif[i]; // 对dif进行前缀和操作,得到原数组
ans=min(ans,a[i]); // 在前缀和的同时,可以直接统计答案
}
cout << ans << endl;
return 0;
}
习题
- CF44C Holidays
- CF816B Karen and Coffee
- CF845C Two TVs
- P4552 [Poetize6] IncDec Sequence
- P7404 [JOI 2021 Final] とてもたのしい家庭菜園 4
- SP10500 HAYBALE - Haybale stacking
二维差分
就是二维矩阵上进行差分,和前缀和很像。
我们定义一个原矩阵
构造
构造就先看图就行了:
如果不太懂的话可以看下面的区间修改,和构造非常像。
区间修改
若将
设要修改的矩阵为
然而,矩阵
最后,还有一个问题,就是矩阵
代码:
dif[x1][y1]+=c;
dif[x2+1][y1]-=c;
dif[x1][y2+1]-=c;
dif[x2+2][y2+1]+=c;
单点查询
只需对
代码:
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
a[i][j]=b[i][j]+a[i][j-1]+a[i-1][j]-a[i-1][j-1];
}
}
习题
- P6070 『MdOI R1』Decrease
- P2038 [NOIP2014 提高组] 无线网络发射器选址,此题有一种解法是二维前缀和&差分(感谢 @xrk2006 提供)。