前缀和 与 差分

前缀和 与 差分

一、什么是前缀和?


对于一个给定的数列 A,它的前缀和数列 S 为:
S [ i ] = j = 1 i A [ j ] \displaystyle S[i]=\sum^i_{j=1}A[j]
简单来说,$ S[i]$ 就是 A A 数列的前 i i 项和。

举个例子:

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 , 3 , 6 , 10 } S:\{1,3,6,10\}

二、前缀和的常见应用


先看一个问题:给定一个数列: A : { 1 , 4 , 8 , 7 , 9 } A:\{1,4,8,7,9\}

前缀和的一个最基础的应用就是:求一个给定区间的区间和

给定一个数列 A A ,多次查询,每次给定一个区间 [ 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]

当然我们可以暴力,但是每次暴力都要遍历一遍区间。当多次查询的时候我们的时间复杂度就趋近与 O ( n 2 ) O(n^2)

而我们使用前缀和只需要 O ( n ) O(n) 的时间进行预处理,就可以在 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}
矩阵以1开始编号,问以 ( 2 , 2 ) (2,2) 为左上角, ( 3 , 3 ) (3,3) 为右下角的矩阵每个元素的和为多少:
2 2 3 3 \begin{matrix} 2 & 2 \\ 3 & 3 \\ \end{matrix}
当然我们也可以用两层循环嵌套来求出,但其时间复杂度是 O ( n 2 ) O(n^2) 。下面运用前缀和思想来解题,

我们首先定义一下二维的前缀和:

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]
很像呢。

(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 ,它的差分数列 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)

简单来说差分就是 相邻两个数的差。

还是之前的例子:

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 , 1 , 1 , 1 } B:\{1,1,1,1\}

容易发现,“前缀和” 和 “差分”是一对互逆的运算,

  • 差分序列 B B 的前缀和是原数列 A A
  • 前缀和数列 S S 的差分数列也是原序列 A A

一个常用的技巧:

把数列 A A 的区间 [ l , r ] [l,r] 里所有的元素都加 d d ,可以转化成把其差分数列 B B 中的 B [ l ] + d B[l]+d B [ r ] d B[r]-d , 其他位置不变

这样我们就可以把原数列上的“区间操作”变成差分数列上的“单点操作”,来降低求解难度。

【例题 3】luogu P4552 [Poetize6] IncDec Sequence
在这里插入图片描述

本题思路

本题就是 把原数列上的“区间操作”变成差分数列上的“单点操作”的一个很好应用。

要使 a a 数列每个数都相等,我们可以将问题转化为:**使 a a 的差分数列 从第二个开始 都是 0。 **

拿样例来举例:

a : { 1 , 1 , 2 , 2 } a:\{1,1,2,2\}

B : { 1 , 0 , 1 , 0 } B:\{1,0,1,0\}

我们有两种方法把 a a 变得一样

  • 前两个 1 1 都加上 1 1 B [ 1 ] + 1 , B [ 3 ] 1 B[1] + 1,B[3]-1 , B B 变成 { 2 , 0 , 0 } \{2,0,0 \} ,这样数列变为全 2 2
  • 后两个 2 2 都减去 1 1 B [ 3 ] + 1 B [ 5 ] ( 1 ) B[3]+(-1),B[5]-(-1) , B B 变成 { 1 , 0 , 0 } \{1,0,0\} ,这样数列变为全 1 1

我们可以发现一些规律:

  1. B [ 1 ] B[1] 的取值就是 a a 所有元素大小相等时的值,所以第二问 就是求 B [ 1 ] B[1] 有多少取值。
  1. 我们运用上面红字的差分技巧:给原数列的一段区间加 d d 就是给 B [ l ] + d ,    B [ r + 1 ] d B[l]+d,\ \ B[r+1]-d

因为 B [ 1 ] B[1] 不必为 0,而剩下的都要是 0 ,所以对于差分数列中所有 非零 的元素 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] 变为 0。

所以我们想到一种步数最小的策略:

可以把 B B 中不同符号的情况 全部变成 都是一种符号的情况。因为这样一次操作可以操作两个点

我们来举个例子:

a : { 3 , 7 , 5 , 4 , 2 } a:\{3, 7, 5, 4, 2\}

B : { 3 , 4 , 2 , 1 , 2 } B:\{3,4,-2,-1,-2\}

我们可以先进行以下步骤:

  1. B [ 2 ] + ( 2 ) , B [ 3 ] ( 2 ) B[2]+(-2),B[3]-(-2) ,操作了2次
  2. B [ 2 ] + ( 2 ) , B [ 4 ] ( 2 ) B[2]+(-2),B[4]-(-2) ,操作了2次

B B 变为

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 [ 4 ] + ( B [ 4 ] ) B [ 6 ] ( B [ 4 ] ) B[4]+(-B[4]),B[6]-(-B[4]) ,操作 1 次。

一共操作 5 次,得到答案, B [ 1 ] B[1] 一共有 2 种取值 { 2 , 3 } \{2,3\}

根据这个原理 ,我们可以得出答案的结论:

最小的步数就是 【所有负数和的绝对值】和【所有正数和】的较大一个: ans = max(sum_pos,sum_neg);

可能结果就是 【所有负数和的绝对值】和【所有正数和】的差的绝对值 + 1ans = abs(sum_pos-sum_neg) + 1;

对可能结果的个数给出解释:

因为我们消除一个 B [ k ] B[k] ,可以通过变化 B [ 1 ] B[1] B [ n + 1 ] B[n+1] 两种方式,所以我们可以

  • 不用 B [ 1 ] B[1] ,都用 B [ n + 1 ] B[n+1]
  • 用一次 B [ 1 ] B[1] ,剩下都用 B [ n + 1 ] B[n+1]
  • 用二次 B [ 1 ] B[1] ,剩下都用 B [ n + 1 ] B[n+1]
  • 用三次 B [ 1 ] B[1] ,剩下都用 B [ n + 1 ] B[n+1]
  • 都用 B [ 1 ] B[1] ,不用 B [ n + 1 ] B[n+1]

B B 中非零元素个数 + 1 种情况 , 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;
}

发布了141 篇原创文章 · 获赞 71 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/sinat_40872274/article/details/103034602