洛谷题单 2.8 前缀和差分
0.写在前面
好久没学 A C M ACM ACM了,有点摆烂,上学期期末、寒假、这学期开学一个月,都是一点 A C M ACM ACM没碰,马上开始一堆比赛了,蓝桥杯、小米杯等等,必须要好好学了,准备今天先学完基础算法里的题,后面更一下 d p dp dp和图论,就当复习了。
1.随便讲讲
前缀和和差分是两个非常巧妙的方法,具体是什么呢
对于数组
a
[
n
]
a[n]
a[n],它的前缀和数组
s
[
n
]
s[n]
s[n]代表
a
[
1
]
+
a
[
2
]
+
.
.
.
+
a
[
n
]
a[1]+a[2]+...+a[n]
a[1]+a[2]+...+a[n],它的差分数组
b
[
n
]
b[n]
b[n]代表
a
[
n
]
−
a
[
n
−
1
]
a[n]-a[n-1]
a[n]−a[n−1],特别地,
s
[
1
]
=
b
[
1
]
=
a
[
1
]
s[1]=b[1]=a[1]
s[1]=b[1]=a[1]。
为什么要维护这两个数组呢,很简单,我们举如下的例子。
给定数组
a
[
n
]
,
n
≤
50000
a[n],n\leq 50000
a[n],n≤50000,并给定
m
(
≤
50000
)
m(\leq 50000)
m(≤50000)个询问,询问任意长度的区间和,即询问
s
u
m
=
a
[
l
]
+
a
[
l
+
1
]
+
.
.
.
+
a
[
r
]
sum=a[l]+a[l+1]+...+a[r]
sum=a[l]+a[l+1]+...+a[r],那么如果我们用普通的暴力枚举是一定过不了的。
这时我们就维护一个前缀和数组,那么所询问的
s
u
m
sum
sum就转化为
s
[
r
]
−
s
[
l
−
1
]
s[r]-s[l-1]
s[r]−s[l−1]。
前缀和维护的复杂度是
O
(
n
)
O(n)
O(n),查询任意区间和(区间查询)的复杂度是
O
(
1
)
O(1)
O(1)
这时又有新的例子了,给定数组
a
[
n
]
,
n
≤
50000
a[n],n \leq 50000
a[n],n≤50000,并给定
m
(
≤
50000
)
m(\leq 50000)
m(≤50000)个操作,每次将任意长度的区间内所有数
+
v
+v
+v,最后求
a
[
x
]
a[x]
a[x]的值,那么如果我们用普通的模拟操作肯定是过不了的。
这时我们就维护一个差分数组,那么所要求的将
a
[
l
]
,
a
[
l
+
1
]
,
.
.
.
,
a
[
r
]
a[l],a[l+1],...,a[r]
a[l],a[l+1],...,a[r]都
+
v
+v
+v就转化成
b
[
l
]
+
v
,
b
[
r
+
1
]
−
v
b[l]+v,b[r+1]-v
b[l]+v,b[r+1]−v,这时我们想求
a
[
x
]
a[x]
a[x],那么只需求
b
[
1
]
+
b
[
2
]
+
.
.
.
+
b
[
x
]
b[1]+b[2]+...+b[x]
b[1]+b[2]+...+b[x]即可。
差分数组维护的复杂度是
O
(
n
)
O(n)
O(n),区间修改的复杂度是
O
(
1
)
O(1)
O(1),单点查询的复杂度是
O
(
n
)
O(n)
O(n)。
这时你肯定有问题了,那我想区间修改区间查询怎么办,好问题,这时前缀和和差分就解决不了了,我们需要用更高级的数据结构,比如线段树和树状数组,想学线段树可以看我之前的博客:一篇不知道线段树是什么树的博客,树状数组我之前学的不错可惜没留下博客记录哈哈,后面应该会写的。
上例题
2.例题
P3131 [USACO16JAN]Subsequences Summing to Sevens S
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:维护模7的前缀和,分别找出模7余0,1,2,3,4,5,6的最长区间,取其中最大的即可。
#include<bits/stdc++.h>
#define N 50050
#define reg register
using namespace std;
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,ans,a[N],s[N],mf[10],ml[10];
int main(){
read(n);
for(int i=1;i<=n;i++)read(a[i]),s[i]=(s[i-1]+a[i])%7;
memset(mf,-1,sizeof mf);mf[0]=0;
for(int j=0;j<7;j++){
for(int i=1;i<=n;i++){
if(s[i]==j){
if(mf[j]==-1)mf[j]=i;
else ml[j]=i;
}
}
if(ml[j]>=0&&mf[j]>=0)ans=max(ans,ml[j]-mf[j]);
}
cout<<ans<<endl;
}
P1387 最大正方形
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:维护二位前缀和,每次判断正方形边长的平方和该正方形区域内的二位前缀和是否相等,如果相等则输出,不相等则继续判断。
#include<bits/stdc++.h>
#define N 110
#define reg register
using namespace std;
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,m,ans,sqr[N][N],sum[N][N];
int main(){
read(n),read(m);
for(reg int i=1;i<=n;i++){
for(reg int j=1;j<=m;j++){
read(sqr[i][j]),sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+sqr[i][j];
}
}
for(reg int k=min(n,m)-1;~k;k--){
for(reg int i=1;i+k<=n;i++){
for(reg int j=1;j+k<=m;j++){
if((k+1)*(k+1)==sum[i+k][j+k]-sum[i-1][j+k]+sum[i-1][j-1]-sum[i+k][j-1]){
cout<<k+1<<endl;
return 0;
}
}
}
}
}
P3397 地毯
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:维护二维差分,最后利用二维前缀和求出单点的值
#include<bits/stdc++.h>
#define N 1100
#define reg register
using namespace std;
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,m,ans,x,y,a,b,sqr[N][N],sum[N][N];
int main(){
read(n),read(m);
for(reg int i=1;i<=m;i++){
read(x),read(y),read(a),read(b);
sum[x][y]++,sum[a+1][b+1]++,sum[x][b+1]--,sum[a+1][y]--;
}
for(reg int i=1;i<=n;i++){
for(reg int j=1;j<=n;j++)sqr[i][j]=sqr[i-1][j]+sqr[i][j-1]-sqr[i-1][j-1]+sum[i][j],printf("%d ",sqr[i][j]);
putchar('\n');
}
}
P2280 [HNOI2003]激光炸弹
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:维护二维前缀和进行暴力枚举即可,由于本题是老年题,题面描述非常不清晰,有很多细节需要注意。首先他给的坐标下标是从0开始的,所以数据是
0
−
5000
0-5000
0−5000,因此我们先将下标都加一,其次这个正方形是大小为
n
−
1
n-1
n−1的,因为他说边界不算进去,这个也特别模糊,需要特殊注意。
#include<bits/stdc++.h>
#define N 5050
#define reg register
using namespace std;
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,m,ans,x,y,v,sum[N][N];
int main(){
read(m),read(n);
for(reg int i=1;i<=m;i++){
read(x),read(y),read(v);
sum[x+1][y+1]+=v;
}
for(int i=1;i<=5001;i++){
for(int j=1;j<=5001;j++)sum[i][j]+=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
}
for(reg int i=0;i+n<=5001;i++){
for(reg int j=0;j+n<=5001;j++)ans=max(ans,sum[i+n][j+n]-sum[i][j+n]-sum[i+n][j]+sum[i][j]);
}
cout<<ans<<endl;
}
P4552 [Poetize6] IncDec Sequence
题面
S
o
l
u
t
i
o
n
:
Solution:
Solution:维护差分数组,我们想将所有数均变为一个相同的数,那么最后的差分数组为
a
[
1
]
,
0
,
0
,
0
,
.
.
.
,
0
a[1],0,0,0,...,0
a[1],0,0,0,...,0
首先对于第一问求最少操作次数,我们已知以下结论
如果操作
[
l
,
r
]
+
1
[l,r]+1
[l,r]+1或
−
1
-1
−1那么差分数组会做出如下改变
b
[
l
]
+
1
,
b
[
r
+
1
]
−
1
b[l]+1,b[r+1]-1
b[l]+1,b[r+1]−1或
b
[
l
]
−
1
,
b
[
r
+
1
]
+
1
b[l]-1,b[r+1]+1
b[l]−1,b[r+1]+1
如果操作
[
l
,
n
]
+
1
[l,n]+1
[l,n]+1或
−
1
-1
−1那么差分数组会做出如下改变
b
[
l
]
+
1
b[l]+1
b[l]+1或
b
[
l
]
−
1
b[l]-1
b[l]−1
因此我们只需统计出差分数组中除
b
[
1
]
b[1]
b[1]外正数的个数
x
x
x和负数的个数
y
y
y,然后取
m
a
x
{
x
,
y
}
max\{x,y\}
max{x,y}的即为答案
对于第二问,可以有多少种数列,那么对于只改变
b
[
l
]
b[l]
b[l]的操作,因为改变
b
[
1
]
b[1]
b[1]的值不影响正确性,所以可以看作改变
b
[
1
]
b[1]
b[1]和
b
[
l
]
b[l]
b[l],因此不同的
b
[
1
]
有
b[1]有
b[1]有|x-y|$种。
#include<bits/stdc++.h>
#define N 100010
#define reg register
#define int long long
using namespace std;
inline void read(int &x){
int s=0,w=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){s=(s<<3)+(s<<1)+(ch&15);ch=getchar();}
x=s*w;
}
int n,x,y,a[N],b[N];
signed main(){
read(n);
for(reg int i=1;i<=n;i++)read(a[i]),b[i]=a[i]-a[i-1];
for(reg int i=2;i<=n;i++){
if(b[i]>0)x+=b[i];
else y-=b[i];
}
cout<<max(x,y)<<endl<<abs(x-y)+1<<endl;
}