0
点赞
收藏
分享

微信扫一扫

前缀和&差分

吴陆奇 2022-02-18 阅读 55
数据结构

前缀和是一个数组在某个下标之前的所有数组元素之和(包括此元素)。
差分是将数列中的每一项分别与前一项做差。

一维前缀和

定义式: b [ i ] = ∑ j = 0 i a [ j ] b[i] = \sum_{j=0}^{i}a[j] b[i]=j=0ia[j]
递推公式: b [ 0 ] = 0 ; b [ i ] = b [ i − 1 ] + a [ i ] b[0]=0; b[i] = b[i-1]+a[i] b[0]=0;b[i]=b[i1]+a[i];

初始化 O ( n ) O(n) O(n):

for(int i = 1; i <= n; i++){
	cin >> a[i];
	b[i] = b[i-1]+a[i];
}

查询 O ( 1 ) O(1) O(1):

cout << b[l] - b[r-1] << endl;

上道例题 AT1894総和

思路:本题数据范围过大,我们无法使用暴力,我们可以先计算整个数列的前缀和,然后计算数列中的所有长度为 k k k的连续的部分的总和。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define endl '\\n'
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
const int INF = 0x7fffffff;
const int N = 1e5+10;

ll arr[N], n, k, res;

int main(){
	cin >> n >> k;
	for(int i = 1; i <= n; i++){
		cin >> arr[i];
		arr[i] += arr[i-1];
	}
	for(int i = 1; i <= n-k+1; i++){
		res += arr[k+i-1] - arr[i-1];
	}
	cout << res << endl;
	return 0;
}

一维差分

例如 A : 1 , 2 , 3 , 4 , 5 , 6 A:1,2,3,4,5,6 A:123456,差分后是 B : 1 , 1 , 1 , 1 , 1 , 1 B:1,1,1,1,1,1 B:111111
所以我们可以知道,对于一个给定的数列 A [ ] A[] A[],它的差分数列 B [ ] B[] B[]为: B [ 1 ] = A [ 1 ] , B [ i ] = A [ i ] − A [ i − 1 ] B[1] = A[1],B[i] = A[i] - A[i-1] B[1]=A[1]B[i]=A[i]A[i1]。(数列 A [ ] A[] A[]是数列 B [ ] B[] B[]的前缀和)

单点修改 A [ i ] + n u m A[i]+num A[i]+num
若将 A [ 2 ] + 1 A[2]+1 A[2]+1,则 A : 1 , 3 , 3 , 4 , 5 , 6 B : 1 , 2 , 0 , 1 , 1 , 1 A:1,3,3,4,5,6\quad B:1,2,0,1,1,1 A:1,3,3,4,5,6B:1,2,0,1,1,1
所以我们发现单点修改,只要改变差分数列中的自己和后一个数即可(即 A [ i ] + n u m , A [ i + 1 ] − n u m A[i]+num,A[i+1]-num A[i]+num,A[i+1]num

区间修改:区间 [ L , R ] [L,R] [L,R]增加 n u m num num
若将 A A A数组 [ 2 , 5 ] [2,5] [2,5]每个元素增加 1 1 1,则 A : 1 , 3 , 4 , 5 , 6 , 6 B : 1 , 2 , 1 , 1 , 1 , 0 A:1,3,4,5,6,6 \quad B:1,2,1,1,1,0 A:1,3,4,5,6,6B:1,2,1,1,1,0
所以我们发现区间修改,只需要改变 B [ L ] B[L] B[L] B [ R + 1 ] B[R+1] B[R+1]即可(即 A [ L ] + n u m , A [ R + 1 ] − n u m A[L]+num,A[R+1]-num A[L]+num,A[R+1]num

同样上道例题 CF44C

思路:这道题的数据范围很小,我们使用暴力也是可以解决的,但在这里我们还是使用一维差分的思想。那么我们应该如何实现?很简单,我们只需要开一个数组,对它进行区间修改(即在 a r r [ l ] arr[l] arr[l]上加 1 1 1表示浇过一天水,再在 a r r [ r + 1 ] arr[r+1] arr[r+1]上减去 1 1 1表示到这一天截止),最后从头到尾遍历一遍, a r r [ i ] + = a r r [ i − 1 ] arr[i]+=arr[i-1] arr[i]+=arr[i1]即可。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define endl '\\n'
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
const int INF = 0x7fffffff;
const int N = 1e5+10;

int arr[N], n, m, l, r;

int main(){
	cin >> n >> m;
	for(int i = 1; i <= m; i++){
		cin >> l >> r;
		arr[l] += 1;
		arr[r+1] -= 1;//做差分
	}
	for(int i = 1; i <= n; i++){
		arr[i] += arr[i-1];
		if(arr[i] != 1){
			cout << i << " " << arr[i] << endl;
			return 0;
		}
	}
	cout << "OK" << endl;
	return 0;
}

二维前缀和

定义式: b [ x ] [ y ] = ∑ i = 0 x ∑ j = 0 y a [ i ] [ j ] b[x][y]=\sum_{i=0}^{x}\sum_{j=0}^{y}a[i][j] b[x][y]=i=0xj=0ya[i][j]

递推公式: b [ x ] [ y ] = b [ x − 1 ] [ y ] + b [ x ] [ y − 1 ] − b [ x − 1 ] [ y − 1 ] + a [ x ] [ y ] b[x][y]=b[x-1][y]+b[x][y-1]-b[x-1][y-1]+a[x][y] b[x][y]=b[x1][y]+b[x][y1]b[x1][y1]+a[x][y]

初始化 O ( n m ) O(nm) O(nm):

for(int i = 1; i <= n; i++){
	for(int j = 1; j <= m; j++){
		cin >> a[i][j];
		b[i][j] = b[i-1][j] + b[i][j-1] - b[i-1][j-1] + a[i][j];
	}
}

下面我们来理解为什么二维前缀和的递推公式为 b [ i ] [ j ] = b [ i − 1 ] [ j ] + b [ i ] [ j − 1 ] − b [ i − 1 ] [ j − 1 ] + a [ i ] [ j ] b[i][j] = b[i-1][j] + b[i][j-1] - b[i-1][j-1] + a[i][j] b[i][j]=b[i1][j]+b[i][j1]b[i1][j1]+a[i][j]

在这里插入图片描述

现在我们来计算区间 A [ 2 ] [ 2 ] A[2][2] A[2][2]的前缀和:
如果我们直接用递推公式 B [ 2 ] [ 2 ] = B [ 2 ] [ 1 ] + B [ 1 ] [ 2 ] − A [ 1 ] [ 1 ] + A [ 2 ] [ 2 ] = 9 + 5 − 2 + 4 = 16 B[2][2]=B[2][1]+B[1][2]-A[1][1]+A[2][2]=9+5-2+4=16 B[2][2]=B[2][1]+B[1][2]A[1][1]+A[2][2]=9+52+4=16;为什么呐?
B [ 2 ] [ 2 ] = A [ 1 ] [ 1 ] + A [ 1 ] [ 2 ] + A [ 2 ] [ 1 ] + A [ 2 ] [ 2 ] = 2 + 3 + 7 + 4 = 16 B[2][2]=A[1][1]+A[1][2]+A[2][1]+A[2][2]=2+3+7+4=16 B[2][2]=A[1][1]+A[1][2]+A[2][1]+A[2][2]=2+3+7+4=16;
B [ 2 ] [ 1 ] = A [ 1 ] [ 1 ] + A [ 2 ] [ 1 ] B[2][1]=A[1][1]+A[2][1] B[2][1]=A[1][1]+A[2][1];
B [ 1 ] [ 2 ] = A [ 1 ] [ 1 ] + A [ 1 ] [ 2 ] B[1][2]=A[1][1]+A[1][2] B[1][2]=A[1][1]+A[1][2];
使用在我们计算 A [ 2 ] [ 2 ] A[2][2] A[2][2]前缀和的时候就可以用 A [ 1 ] [ 2 ] A[1][2] A[1][2]的前缀和加上 A [ 2 ] [ 1 ] A[2][1] A[2][1]的前缀和,再减去多加的 A [ 1 ] [ 1 ] A[1][1] A[1][1],最后加上自己本身的数值。

查询 O ( 1 ) O(1) O(1):

sum = b[x2][y2] - b[x2][y1-1] - b[x1-1][y2] + b[x1-1][y1-1];

同样的下面我们来理解二维前缀和的查询代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s7VAPOXL-1645189342897)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/6a31d6d6-56de-438c-9fac-715c7d4e5e19/Untitled.png)]

现在我们想求区间 A [ 2 ] [ 3 ] A[2][3] A[2][3] A [ 3 ] [ 3 ] A[3][3] A[3][3]的数值之和 r e s res res
如果我们直接使用代码 r e s = B [ 3 ] [ 3 ] − B [ 3 ] [ 2 ] − B [ 1 ] [ 3 ] + B [ 1 ] [ 2 ] = 52 − 30 − 9 + 5 = 18 res=B[3][3]-B[3][2]-B[1][3]+B[1][2]=52-30-9+5=18 res=B[3][3]B[3][2]B[1][3]+B[1][2]=52309+5=18; W h y Why Why?
我们来看 B [ 3 ] [ 3 ] B[3][3] B[3][3]等于区间 A [ 1 ] [ 1 ] A[1][1] A[1][1] A [ 3 ] [ 3 ] A[3][3] A[3][3]的和, B [ 2 ] [ 3 ] B[2][3] B[2][3]等于区间 A [ 1 ] [ 1 ] A[1][1] A[1][1] A [ 2 ] [ 3 ] A[2][3] A[2][3]的和
现在我们计算区间 A [ 2 ] [ 3 ] A[2][3] A[2][3] A [ 3 ] [ 3 ] A[3][3] A[3][3]的和,就等于我们在区间 A [ 1 ] [ 1 ] A[1][1] A[1][1] A [ 3 ] [ 3 ] A[3][3] A[3][3]上删除区间 A [ 1 ] [ 1 ] A[1][1] A[1][1] A [ 3 ] [ 2 ] A[3][2] A[3][2]和区间 A [ 1 ] [ 1 ] A[1][1] A[1][1] A [ 1 ] [ 3 ] A[1][3] A[1][3],但我们又发现我们多删除了区间 A [ 1 ] [ 1 ] A[1][1] A[1][1] A [ 1 ] [ 2 ] A[1][2] A[1][2],所以我们要加上它.
所以 r e s = B [ 3 ] [ 3 ] − B [ 3 ] [ 2 ] − B [ 1 ] [ 3 ] + B [ 1 ] [ 2 ] res=B[3][3]-B[3][2]-B[1][3]+B[1][2] res=B[3][3]B[3][2]B[1][3]+B[1][2]

来检验检验自己是否理解P1719最大加权矩形

思路:计算出 N × N N\times N N×N矩阵的前缀和,如果枚举找出一个矩形,这个矩形内包含的所有元素的和最大。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define endl '\\n'
#define IOS ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
const int INF = 0x7fffffff;
const int N = 1e5+10;

int MAX = -INF;
int n, m, c, x, y, t, sum[130][130];
int main(){
	cin >> n;
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= n; j++){
			cin >> t;
			sum[i][j] = sum[i-1][j] + sum[i][j-1] - sum[i-1][j-1] + t;
		}
	}
	 for(int x1 = 1; x1 <= n; x1++){
    	for(int y1 = 1; y1 <= n; y1++){
    		for(int x2 = 1; x2 <= n; x2++){
    			for(int y2 = 1; y2 <= n; y2++){
    				if(x2 < x1 || y2 < y1) continue;
    				MAX = max(MAX,sum[x2][y2] + sum[x1-1][y1-1] - sum[x2][y1-1] - sum[x1-1][y2]);
    			}
    		}
    	}
    }
	cout << MAX << endl;
	return 0;
}

二维差分

这里我们写的是前缀和和差分的对比理解,所以在这里我们使用的是二维差分的直接构造方法,

在一维差分中,我们曾说过这样一句话:数列 A [ ] A[] A[](原数组)是数列 B [ ] B[] B[](差分数组)的前缀和。同样的道理:在二维差分中,原数组也是差分数组的前缀和,使用我们通过前缀和的构造公式: b [ i ] [ j ] = b [ i − 1 ] [ j ] + b [ i ] [ j − 1 ] − b [ i − 1 ] [ j − 1 ] + a [ i ] [ j ] b[i][j] = b[i-1][j] + b[i][j-1] - b[i-1][j-1] + a[i][j] b[i][j]=b[i1][j]+b[i][j1]b[i1][j1]+a[i][j](这里 b [ ] [ ] b[][] b[][]是前缀和数组, A [ ] [ ] A[][] A[][]是原数组),我们可以知道二维差分的构造公式为: b [ i ] [ j ] = a [ i ] [ j ] − a [ i − 1 ] [ j ] − a [ i ] [ j − 1 ] + a [ i − 1 ] [ j − 1 ] b[i][j]=a[i][j]−a[i−1][j]−a[i][j−1]+a[i−1][j−1] b[i][j]=a[i][j]a[i1][j]a[i][j1]+a[i1][j1](这里 b [ ] [ ] b[][] b[][]是差分数组, A [ ] [ ] A[][] A[][]是原数组)。

构造:

for(int i = 1; i <= n; i++){
		for(int j = 1; j <= m; j++){
			cin >> arr[i][j];
			b[i][j] = arr[i][j] - arr[i-1][j] - arr[i][j-1] + arr[i-1][j-1];
		}
	}

下面我们先把修改操作的代码给大家

for(int i = 1; i <= q; i++){
		cin >> x2 >> y2 >> x3 >> y3 >> c;
		b[x2][y2] += c;
		b[x2][y3+1] -= c;
		b[x3+1][y2] -= c;
		b[x3+1][y3+1] += c;
	}

每次对 b b b数组执行以上操作,等价于

for(int i = x2; i <= x3; i++){
	for(int j = y2; j <= y3; j++){
        arr[i][j] += c;
    }
}

下面我们画图来理解这个修改过程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-saGrB1iq-1645189342898)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/aac5fd11-9a9d-49f6-bac6-75263af4576a/Untitled.png)]

b [ x 1 ] [ y 1 ] + = c b[x_1][y_1] += c b[x1][y1]+=c;让整个 a r r arr arr数组中的黄色矩形内的每个元素都加上 c c c
b [ x 1 ] [ y 2 + 1 ] − = c b[x_1][y_2+1] -= c b[x1][y2+1]=c;让整个 a r r arr arr数组中的红色矩形内的每个元素都减去 c c c,使其内元素不发生改变。
b [ x 2 + 1 ] [ y 1 ] − = c b[x_2+1][y_1] -= c b[x2+1][y1]=c;让整个 a r r arr arr数组中的紫色矩形内的每个元素都减去 c c c,使其内元素不发生改变。
b [ x 2 + 1 ] [ y 2 + 1 ] + = c b[x_2+1][y_2+1] += c b[x2+1][y2+1]+=c;让整个 a r r arr arr数组中的绿色矩形内的每个元素都加上 c c c,绿色内的元素相当于减去了两次,所以我们要再加上一次,使其内的元素保持不变。
这就是整个二维差分的修改操作,最后我们只需要再求一次差分数组的前缀和,就可以得到被修改的原数组了。

最后我们看一道例题,来更好的理解二维差分798. 差分矩阵

思路:这是一道二维差分的简单运用,我们可以先求出原数组的差分数组,在差分数组进行修改,最后在求差分数组的前缀和,即修改后的原数组。

#include <bits/stdc++.h>
#include <stack>
using namespace std;
typedef long long ll;
#define endl '\\n'
const int INF = 0x3fffffff;
const int N = 1e6+10;

int n, m, q, x2, y2, x3, y3, c;
int arr[1010][1010], b[1010][1010];

int main(){
	cin >> n >> m >> q;
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= m; j++){
			cin >> arr[i][j];
            //求二维差分数组
			b[i][j] = arr[i][j] - arr[i-1][j] - arr[i][j-1] + arr[i-1][j-1];
		}
	}
	for(int i = 1; i <= q; i++){//修改二维差分数组
		cin >> x2 >> y2 >> x3 >> y3 >> c;
		b[x2][y2] += c;
		b[x2][y3+1] -= c;
		b[x3+1][y2] -= c;
		b[x3+1][y3+1] += c;
	}
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= m; j++){
            //求修改后的原数组
			b[i][j] = b[i-1][j] + b[i][j-1] - b[i-1][j-1] + b[i][j];
		}
	}
	for(int i = 1; i <= n; i++){
		for(int j = 1; j <= m; j++){
			cout << b[i][j] << " ";
		}
		cout << endl;
	}
	return 0;
}
举报

相关推荐

0 条评论