前缀和 与 差分
一、什么是前缀和?
对于一个给定的数列 A,它的前缀和数列 S 为:
简单来说,$ S[i]$ 就是
数列的前
项和。
举个例子:
二、前缀和的常见应用
先看一个问题:给定一个数列:
前缀和的一个最基础的应用就是:求一个给定区间的区间和
给定一个数列 ,多次查询,每次给定一个区间 , 问给定的区间的和是多少?
即求
当然我们可以暴力,但是每次暴力都要遍历一遍区间。当多次查询的时候我们的时间复杂度就趋近与 。
而我们使用前缀和只需要 的时间进行预处理,就可以在 的时间内求出区间和。
【例题 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开始编号,问以
为左上角,
为右下角的矩阵每个元素的和为多少:
当然我们也可以用两层循环嵌套来求出,但其时间复杂度是
。下面运用前缀和思想来解题,
我们首先定义一下二维的前缀和:
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)**和 绿色小矩形的差。这是不是和
很像呢。
以(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
四、差分
对于一个给定的数列 ,它的差分数列 定义为:
简单来说差分就是 相邻两个数的差。
还是之前的例子:
容易发现,“前缀和” 和 “差分”是一对互逆的运算,
- 差分序列 的前缀和是原数列
- 前缀和数列 的差分数列也是原序列
一个常用的技巧:
把数列 的区间 里所有的元素都加 ,可以转化成把其差分数列 中的 , , 其他位置不变
这样我们就可以把原数列上的“区间操作”变成差分数列上的“单点操作”,来降低求解难度。
【例题 3】luogu P4552 [Poetize6] IncDec Sequence
本题思路
本题就是 把原数列上的“区间操作”变成差分数列上的“单点操作”
的一个很好应用。
要使 数列每个数都相等,我们可以将问题转化为:**使 的差分数列 从第二个开始 都是 0。 **
拿样例来举例:
我们有两种方法把 变得一样
- 前两个 都加上 , , 变成 ,这样数列变为全 。
- 后两个 都减去 , , 变成 ,这样数列变为全 。
我们可以发现一些规律:
- 的取值就是 所有元素大小相等时的值,所以第二问 就是求 有多少取值。
- 我们运用上面红字的差分技巧:给原数列的一段区间加 就是给
因为 不必为 0,而剩下的都要是 0 ,所以对于差分数列中所有 非零 的元素 , 我们都可以
用 或者 使 变为 0。
所以我们想到一种步数最小的策略:
可以把 中不同符号的情况 全部变成 都是一种符号的情况。因为这样一次操作可以操作两个点
我们来举个例子:
我们可以先进行以下步骤:
- ,操作了2次
- ,操作了2次
将 变为
这样再通过 或者 ,操作 1 次。
一共操作 5 次,得到答案, 一共有 2 种取值
根据这个原理 ,我们可以得出答案的结论:
最小的步数就是 【所有负数和的绝对值】和【所有正数和】的较大一个
: ans = max(sum_pos,sum_neg);
可能结果就是 【所有负数和的绝对值】和【所有正数和】的差的绝对值 + 1
:ans = abs(sum_pos-sum_neg) + 1;
对可能结果的个数给出解释:
因为我们消除一个 ,可以通过变化 和 两种方式,所以我们可以
- 不用 ,都用
- 用一次 ,剩下都用
- 用二次 ,剩下都用
- 用三次 ,剩下都用
- …
- 都用 ,不用
共 中非零元素个数 + 1 种情况 , 中非零元素个数也 就是 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;
}