前缀和 与 差分
一、什么是前缀和?
对于一个给定的数列 A,它的前缀和数列 S 为:
S
[
i
]
=
∑
j
=
1
i
A
[
j
]
\displaystyle S[i]=\sum^i_{j=1}A[j]
S[i]=j=1∑iA[j]
简单来说,$ S[i]$ 就是
A
A
A 数列的前
i
i
i 项和。
举个例子:
A : { 1 , 2 , 3 , 4 } A:\{1,2,3,4\} A:{1,2,3,4}
S [ 1 ] = A [ 1 ] , S [ 2 ] = A [ 1 ] + A [ 2 ] , S [ 3 ] = A [ 1 ] + A [ 2 ] + A [ 3 ] . . . S[1] = A[1],S[2] = A[1]+A[2],S[3] = A[1]+A[2]+A[3]... S[1]=A[1],S[2]=A[1]+A[2],S[3]=A[1]+A[2]+A[3]...
S : { 1 , 3 , 6 , 10 } S:\{1,3,6,10\} S:{1,3,6,10}
二、前缀和的常见应用
先看一个问题:给定一个数列: A : { 1 , 4 , 8 , 7 , 9 } A:\{1,4,8,7,9\} A:{1,4,8,7,9}
前缀和的一个最基础的应用就是:求一个给定区间的区间和
给定一个数列 A A A,多次查询,每次给定一个区间 [ l , r ] [l,r] [l,r], 问给定的区间的和是多少?
即求
s
u
m
(
i
,
j
)
=
∑
i
=
l
r
A
[
i
]
=
S
[
r
]
−
S
[
l
−
1
]
\displaystyle sum(i,j) = \sum^{r}_{i=l}A[i] = S[r]-S[l-1]
sum(i,j)=i=l∑rA[i]=S[r]−S[l−1]
当然我们可以暴力,但是每次暴力都要遍历一遍区间。当多次查询的时候我们的时间复杂度就趋近与 O ( n 2 ) O(n^2) O(n2)。
而我们使用前缀和只需要 O ( n ) O(n) O(n)的时间进行预处理,就可以在 O ( 1 ) O(1) O(1) 的时间内求出区间和。
【例题 1】51Nod 1081 子段求和
链接:https://www.51nod.com/Challenge/Problem.html#problemId=1081
本题思路
前缀和的裸题,直接套用
/***********************
*author:ccf
*source:51Nod-1081
*topic:前缀和
************************/
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define ll long long
using namespace std;
const int N = 5e6 + 7;
int n,q;
ll A[N],S[N];
int main() {
//freopen("data.in","r",stdin);
scanf("%d",&n);
for(int i = 1; i <= n; i++) {
scanf("%lld",&A[i]);
S[i] = A[i] + S[i-1];
}
scanf("%d",&q);
for(int i = 0,l,len; i < q; i++){
scanf("%d %d",&l,&len);
printf("%lld\n",S[l + len-1] - S[l-1]);
}
return 0;
}
三、二维前缀和
我们已经学习了一维前缀和,那么我们将前缀和的思想扩展到二维空间去会怎么样呢?
还是先看一个问题:给定一个二维矩阵,每次给出一个矩形的左上角和右下角的点坐标,求这个矩阵的和。
1
2
3
4
2
2
2
2
3
3
3
3
1
3
1
3
\begin{matrix} 1 & 2 & 3 & 4 \\ 2 & 2 & 2 & 2 \\ 3 & 3 & 3 & 3 \\ 1 & 3 & 1 & 3 \\ \end{matrix}
1231223332314233
矩阵以1开始编号,问以
(
2
,
2
)
(2,2)
(2,2)为左上角,
(
3
,
3
)
(3,3)
(3,3)为右下角的矩阵每个元素的和为多少:
2
2
3
3
\begin{matrix} 2 & 2 \\ 3 & 3 \\ \end{matrix}
2323
当然我们也可以用两层循环嵌套来求出,但其时间复杂度是
O
(
n
2
)
O(n^2)
O(n2)。下面运用前缀和思想来解题,
我们首先定义一下二维的前缀和:
g[i][j]
表示二维前缀和,其意义是(1,1)
这个点与(i,j)
这个点两个点分别为左上角和右下角所组成的矩阵内的数的和。
这个前缀和我们怎么求呢?
我们来画个图:
可以看出来,我们要求(1,1)
到(i,j)
的这个矩形的面积,我们可以通过
求(1,1)到(i,j-1)的面积
+(1,1)到(i-1,j)的面积
-(1,1)到(i-1,j-1)的的面积
+灰色区域
来得到(1,1)到(i,j)的面积
即 g(i,j)
为什要减去(1,1)到(i-1,j-1)的的面积
? 因为我们加了两次啊
可以得到: g[i][j]=g[i-1][j]+g[i][j-1]-g[i-1][j-1]+map[i][j]
有了前缀和,我们就要解决我们上面提出的问题了,我们也抽象成图形。
我们可以看出来,我们上面的问题就是要求蓝色区域的和
蓝色区域的面积 = 整个红框面积 - 灰色面积
而灰色面积就是 $(A+B+C)+(A+D+F)-A $
可以得出最后的结论:
蓝色区域的面积 = 红框面积 + 矩形(ADF) + 矩形(ABC) - 绿色面积
本题中的蓝色区域,其本质也是一种前缀和的差,它是**红框面积 + 矩形(ADF) + 矩形(ABC)**和 绿色小矩形的差。这是不是和
s
u
m
(
i
,
j
)
=
S
[
r
]
−
S
[
l
−
1
]
\displaystyle sum(i,j) = S[r]-S[l-1]
sum(i,j)=S[r]−S[l−1]
很像呢。
以(a,b)
为左上角,(x,y)
为右下角的矩阵和,写成公式就是:
ans = g[x][y]-g[a-1][y]-g[x][b-1]+g[a-1][b-1]
【例题 2】1218: [HNOI2003]激光炸弹
链接:https://www.lydsy.com/JudgeOnline/problem.php?id=1218
本题思路
给定一个矩阵,求一个最大的子矩阵的和。二维前缀和的经典题目。
/**************************************************************
Problem: 1218
User: Miserable
Language: C++
Result: Accepted
Time:1464 ms
Memory:98868 kb
****************************************************************/
#include <cstdio>
#include <cstring>
#include <algorithm>
#define ll long long
using namespace std;
const int Max = 5010;
int n,r,g[Max][Max] = {0},lx,ly,maxx = -1;
int main(){
scanf("%d %d",&n,&r);
lx = r,ly = r;
for(int i = 0 ; i < n; i++){
int x,y,w;
scanf("%d %d %d",&x,&y,&w);
g[++x][++y] += w;
ly = max(ly,y);
lx = max(lx,x);
}
for(int i = 1; i <= lx; i++)
for(int j = 1; j <= ly; j++){
g[i][j] += g[i-1][j] + g[i][j-1] - g[i-1][j-1] ;
}
for(int i = r; i <= lx; i++)
for(int j = r; j <= ly; j++){
maxx = max(maxx,g[i][j] - g[i-r][j] - g[i][j-r] + g[i-r][j-r]);
}
printf("%d\n",maxx);
return 0;
}
我写的不好,推荐[hulean的博客]:https://www.cnblogs.com/hulean/p/10824752.html
四、差分
对于一个给定的数列 A A A,它的差分数列 B B B定义为:
B [ 1 ] = A [ 1 ] , B [ i ] = A [ i ] − A [ i − 1 ] ( 2 ≤ i ≤ n ) B[1]=A[1],B[i]=A[i]-A[i-1](2\leq i\leq n) B[1]=A[1],B[i]=A[i]−A[i−1](2≤i≤n)
简单来说差分就是 相邻两个数的差。
还是之前的例子:
A : { 1 , 2 , 3 , 4 } A:\{1,2,3,4\} A:{1,2,3,4}
B [ 1 ] = A [ 1 ] , B [ 2 ] = A [ 2 ] − A [ 1 ] , B [ 3 ] = A [ 3 ] − A [ 2 ] . . . B[1] = A[1],B[2] = A[2]-A[1],B[3] = A[3]- A[2]... B[1]=A[1],B[2]=A[2]−A[1],B[3]=A[3]−A[2]...
B : { 1 , 1 , 1 , 1 } B:\{1,1,1,1\} B:{1,1,1,1}
容易发现,“前缀和” 和 “差分”是一对互逆的运算,
- 差分序列 B B B的前缀和是原数列 A A A
- 前缀和数列 S S S的差分数列也是原序列 A A A
一个常用的技巧:
把数列 A A A的区间 [ l , r ] [l,r] [l,r]里所有的元素都加 d d d,可以转化成把其差分数列 B B B中的 B [ l ] + d B[l]+d B[l]+d, B [ r ] − d B[r]-d B[r]−d , 其他位置不变
这样我们就可以把原数列上的“区间操作”变成差分数列上的“单点操作”,来降低求解难度。
【例题 3】luogu P4552 [Poetize6] IncDec Sequence
本题思路
本题就是 把原数列上的“区间操作”变成差分数列上的“单点操作”
的一个很好应用。
要使 a a a 数列每个数都相等,我们可以将问题转化为:**使 a a a 的差分数列 从第二个开始 都是 0。 **
拿样例来举例:
a : { 1 , 1 , 2 , 2 } a:\{1,1,2,2\} a:{1,1,2,2}
B : { 1 , 0 , 1 , 0 } B:\{1,0,1,0\} B:{1,0,1,0}
我们有两种方法把 a a a 变得一样
- 前两个 1 1 1 都加上 1 1 1, B [ 1 ] + 1 , B [ 3 ] − 1 B[1] + 1,B[3]-1 B[1]+1,B[3]−1 , B B B 变成 { 2 , 0 , 0 } \{2,0,0 \} {2,0,0},这样数列变为全 2 2 2。
- 后两个 2 2 2 都减去 1 1 1, B [ 3 ] + ( − 1 ) , B [ 5 ] − ( − 1 ) B[3]+(-1),B[5]-(-1) B[3]+(−1),B[5]−(−1) , B B B 变成 { 1 , 0 , 0 } \{1,0,0\} {1,0,0},这样数列变为全 1 1 1。
我们可以发现一些规律:
- B [ 1 ] B[1] B[1] 的取值就是 a a a 所有元素大小相等时的值,所以第二问 就是求 B [ 1 ] B[1] B[1] 有多少取值。
- 我们运用上面红字的差分技巧:给原数列的一段区间加 d d d 就是给 B [ l ] + d , B [ r + 1 ] − d B[l]+d,\ \ B[r+1]-d B[l]+d, B[r+1]−d
因为 B [ 1 ] B[1] B[1] 不必为 0,而剩下的都要是 0 ,所以对于差分数列中所有 非零 的元素 B [ k ] B [k] B[k], 我们都可以
用 B [ 1 ] + B [ K ] , B [ k ] − B [ K ] B[1]+B[K],B[k]-B[K] B[1]+B[K],B[k]−B[K] 或者 B [ k ] + ( − B [ K ] ) , B [ n + 1 ] − ( − B [ K ] ) B[k]+(-B[K]),B[n+1]-(-B[K]) B[k]+(−B[K]),B[n+1]−(−B[K]) 使 B [ k ] B[k] B[k]变为 0。
所以我们想到一种步数最小的策略:
可以把 B B B 中不同符号的情况 全部变成 都是一种符号的情况。因为这样一次操作可以操作两个点
我们来举个例子:
a : { 3 , 7 , 5 , 4 , 2 } a:\{3, 7, 5, 4, 2\} a:{3,7,5,4,2}
B : { 3 , 4 , − 2 , − 1 , − 2 } B:\{3,4,-2,-1,-2\} B:{3,4,−2,−1,−2}
我们可以先进行以下步骤:
- B [ 2 ] + ( − 2 ) , B [ 3 ] − ( − 2 ) B[2]+(-2),B[3]-(-2) B[2]+(−2),B[3]−(−2),操作了2次
- B [ 2 ] + ( − 2 ) , B [ 4 ] − ( − 2 ) B[2]+(-2),B[4]-(-2) B[2]+(−2),B[4]−(−2),操作了2次
将 B B B变为
B : { 3 , 0 , 0 , − 1 , 0 } B:\{3,0,0,-1,0\} B:{3,0,0,−1,0}
这样再通过 B [ 1 ] + ( − 1 ) , B [ 4 ] − ( − 1 ) B[1]+(-1),B[4]-(-1) B[1]+(−1),B[4]−(−1) 或者 B [ 4 ] + ( − B [ 4 ] ) , B [ 6 ] − ( − B [ 4 ] ) B[4]+(-B[4]),B[6]-(-B[4]) B[4]+(−B[4]),B[6]−(−B[4]) ,操作 1 次。
一共操作 5 次,得到答案, B [ 1 ] B[1] B[1] 一共有 2 种取值 { 2 , 3 } \{2,3\} {2,3}
根据这个原理 ,我们可以得出答案的结论:
最小的步数就是 【所有负数和的绝对值】和【所有正数和】的较大一个
: ans = max(sum_pos,sum_neg);
可能结果就是 【所有负数和的绝对值】和【所有正数和】的差的绝对值 + 1
:ans = abs(sum_pos-sum_neg) + 1;
对可能结果的个数给出解释:
因为我们消除一个 B [ k ] B[k] B[k],可以通过变化 B [ 1 ] B[1] B[1]和 B [ n + 1 ] B[n+1] B[n+1] 两种方式,所以我们可以
- 不用 B [ 1 ] B[1] B[1],都用 B [ n + 1 ] B[n+1] B[n+1]
- 用一次 B [ 1 ] B[1] B[1],剩下都用 B [ n + 1 ] B[n+1] B[n+1]
- 用二次 B [ 1 ] B[1] B[1],剩下都用 B [ n + 1 ] B[n+1] B[n+1]
- 用三次 B [ 1 ] B[1] B[1],剩下都用 B [ n + 1 ] B[n+1] B[n+1]
- …
- 都用 B [ 1 ] B[1] B[1],不用 B [ n + 1 ] B[n+1] B[n+1]
共 B B B 中非零元素个数 + 1 种情况 , B B B 中非零元素个数也 就是 abs(sum_pos-sum_neg) 想一想为什么。
/***********************
*author:ccf
*source:luogu-P4552 [Poetize6] IncDec Sequence
*topic:差分
************************/
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define ll long long
using namespace std;
const int N = 1e5 + 7;
ll A[N],B[N]; //A[] 原数列 B[] 差分数列
ll sum_neg = 0 ,sum_pos = 0;
int n;
int main(){
//freopen("data.in","r",stdin);
scanf("%d",&n);
for(int i = 1; i <= n; i++) scanf("%lld",A+i);
B[1] = A[1];
for(int i = 2; i <= n; i++){
B[i] = A[i] - A[i-1];
if(B[i] > 0) sum_pos += B[i];
else sum_neg -= B[i];
}
//数据比较大 要用 long long
printf("%lld\n%lld",1ll*max(sum_pos,sum_neg),1ll*abs(sum_pos-sum_neg) + 1);
return 0;
}