前缀和&差分详解及经典例题补充

声明

前缀和 与 差分可以理解为互逆的 , 求前缀和 跟 差分通常设数组首项下标为1,方便思考的计算。

一维前缀和介绍

对于一维数组 a[n] , 存在数组 sum[n],使得 sum[i] = a[1] + a[2] + … + a[i]

前缀和优势

以O(1)的时间复杂度得到某块区间的总和
举个例子:
输入一个长度为 n 的整数序列。
接下来再输入 m 个询问,每个询问输入一对 l,r。
对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。

常规算法 是两遍for循环求区间和,题目范围小还可以接受,若是开到1e5+,必然会爆int的。所以这里使用前缀和让时间复杂度降到O(1)
一维前缀和相对较简单,不做过多介绍

二维前缀和介绍

如果数组变成二维数组又该怎么做呢,做法与一维前缀和相类似,只是多了个容斥原理
如果我们要求给定坐标的前缀和,如图
在这里插入图片描述
若果我们要求(3,3)的前缀和 , 先算出(3,2),(2,3)的和,再减去图中红绿交汇的部分即(2,2) ,再加上坐标(3,3)的数值,就可以求出(3,3)的和

因此,二维前缀和公式s[i][j] = s[i-1][j] + s[i][j-1] - s[i-1][j-1] +a[i][j]

那么怎么求任意矩阵任意位置坐标的和呢
比如
在这里插入图片描述
我们要求(x1,y1)到(x2,y2)前缀和,那么就是求图中红色区域,那么用(x2,y2)的前缀和(3*3矩阵)- 绿色区域 - 蓝色区间 + 绿兰交汇区域
因此,求任意区域的前缀和公式也很容易得出:
ans = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1]

二维前缀和模板题

在这里插入图片描述
在这里插入图片描述
很典型的二维前缀模板题
思路 : 先求前缀和 , 再利用公式ans = s[x2][y2] - s[x2][y1-1] - s[x1-1][y2] + s[x1-1][y1-1] 求出答案即可
代码如下

# include <iostream>
using namespace std ; 
const int N = 1e3 + 10 ; 
int a[N][N];
int sum[N][N];
int main(){
	int m , n , q;
	cin >> n >> m >> q;
	for (int i = 1 ; i <= n ; i ++)
		for (int j = 1 ; j <= m ; j ++)
			cin >> a[i][j];
	//求前缀和
	for (int i = 1 ; i <= n ; i ++)
		for (int j = 1 ; j <= m ; j ++)
			sum[i][j] = sum[i-1][j] + sum[i][j-1] -sum[i-1][j-1] +a[i][j];
	// 求指定区间的和
	while (q --){
		int x1 , x2 , y1 , y2 ;
		cin >> x1 >> y1 >> x2 >> y2 ;
		cout << sum[x2][y2] - sum[x1-1][y2] - sum[x2][y1-1] + sum[x1-1][y1-1] <<endl; 
	}
	return 0 ;
}

一维差分

类似于数学中的求导和积分,差分可以看成前缀和的逆运算。

首先我们给定一个原数组a:a[1], a[2], a[3], a[n];

然后我们构造一个数组b : b[1] ,b[2] , b[3], b[i];

使得 a[i] = b[1] + b[2 ]+ b[3] +, + b[n];
也就是说,我们原始的数组是我们构造的数组的前缀和。
那么我们如何求差分数组呢?
b[1] = a[1] - a[0];
b[2] = a[2] - a[1];
b[3] =a [3] - a[2];

b[n] = a[n] - a[n-1];
因此通过数组b,我们可以在O(1)内对原始数组进行改造

一维差分例题

在这里插入图片描述
在这里插入图片描述
题意很简单,就是给定区间 , 对原数组进行修改。
常规做法,两层循环,复杂度O(n*m) 写法很简单,但必爆int。
简单梳理一下,为什么会跟差分有关联。
给定区间[l ,r ],让我们把a数组中的[ l, r]区间中的每一个数都加上c,即 a[l] + c , a[l+1] + c , a[l+2] + c , a[r] + c。
所以,只要对差分数组b[n]加上c,那么它的前缀和数组a[n]也会因此而改变。比如b[k]+c,那么a[k]及其往后的数全部要加k。
所以当给定我们一个区间,做法也是同样的:
首先让差分数组b中的 b[l] + c ,通过前缀和运算,a数组变成 a[l] + c ,a[l+1] + c, a[n] + c。
然后 b[r+1] - c, 通过前缀和运算,a数组变成 a[r+1] - c,a[r+2] - c,a[n] - c。
这里为什么要有b[r+1] - c 呢?下面画图解释一下:
在这里插入图片描述
我们要让[l,r]区间的原始数组a的数值变化,通过b[l]+c,让a[l]及其之后的数组都+c,但r之后的数我们并不想让它+c,所以我们还要让r之后的数组-c。
所以,b[r+1] - c就是这个道理。
代码块:

# include <iostream>
using namespace std ; 
const int N = 1e5 + 10 ; 
int a[N];
int b[N];
int main(){
	int n , t ; 
	cin >> n >> t;
	for (int i = 1 ; i <= n ; i ++){
		cin >> a[i];
		b[i] = a[i] - a[i-1];
	}
	while (t --){
		int l , r , c ;
		cin >> l >> r >> c ;
		b[l] += c;
		b[r+1] -= c;
	}
	for (int i = 1 ; i <= n ; i++){
		b[i] += b[i-1];// 求前缀和,就是求a的各项
		cout << b[i] << " ";
	}
	return 0 ;
}

二维差分

二维差分和二维前缀和都运用到了容斥原理
同样,a[][]数组是b[][]数组的前缀和数组,那么b[][]是a[][]的差分数组。
那么a数组中a[i][j]是b数组左上角(1,1)到右下角(i,j)所包围矩形元素的和。

如何构造二维差分数组
对于差分,我们始终要记住,b[i][j]的改变会影响a[i][j]及其之后的元素。
若b数组已经构造好,让指定区域的元素加上c
b[x1][y1] + = c;
b[x1][y2+1] - = c;
b[x2+1][y1] - = c;
b[x2+1][y2+1] + = c;
我们画个图来理解一下
在这里插入图片描述
比如我们要改变的区间是在(x1,y1)到(x2,y2)
首先对b[x1][y1]+c ,就是图中红色区域。
再对绿色和紫色区域操作: b[x1][y2+1] - c ,b[x2+1][y1] - c.
又因为粉色重叠部分多减了一次,再加上 b[x2+1][y2+1] + c。
再详细分析一下
我们要求这个区域:
在这里插入图片描述
必然先要求这个区域:
在这里插入图片描述
再求这两个区域:
在这里插入图片描述
最后减去交汇的区域
在这里插入图片描述
我们对原始数组进行区域增减,实际上就是对b数组的增减,通过这个方法创造差分数组。
方法:

  for(int i=1;i<=n;i++)
      for(int j=1;j<=m;j++){
     	b[i][j] += c;
     	b[i+1][j] -= c;
     	b[i][j+1] -= c;
     	b[i+1][j+1] += c;	
     }
就等同于对原始数组进行修改
for (int i = 1 ; i <= n ; i ++)
	for (int j = 1; j <= m ; j ++)
		a[i][j] += c;

假设a,b数组均为空,实际上不为空。我们每次让以(i,j)为左上角到以(i,j)为右上角面积内元素(其实就是一个小方格的面积)去插入 c=a[i][j],等价于原数组a中(i,j) 到(i,j)范围内 加上了 a[i][j] ,因此执行n*m次插入操作,就成功构建了差分b数组.

二维差分习题

在这里插入图片描述
在这里插入图片描述
和上面讲的思路完全一样,直接上代码

#include <iostream>

using namespace std;

const int N = 1000 + 10;

int a[N][N];
int b[N][N];

int n, m, q;

void insert(int x1, int y1, int x2, int y2, int c){ // 写成函数形式方便操作
    b[x1][y1] += c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y1] -= c;
    b[x2 + 1][y2 + 1] += c;
}

int main(){
    cin >> n >> m >> q;
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            scanf("%d",a[i] + j);
            insert(i, j, i, j, a[i][j]); //
        }
    }
    
    while(q--){
        int x1,y1,x2,y2,c;
        cin >> x1 >> y1 >> x2 >> y2 >> c;
        insert(x1, y1, x2, y2, c);
    }
    
    for(int i = 1; i <= n; i++){  // 根据二维前缀和的定义来对原数组进行复原,注意这里不是二维前缀和的操作,而是以某点为右下角的前缀和数组的构造
        for(int j = 1; j <= m; j++){
            a[i][j] = a[i - 1][j] + a[i][j - 1] - a[i - 1][j - 1] + b[i][j];
        }
    }
    
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            printf("%d ", a[i][j]);
        }
        printf("\n");
    }
    return 0;
}
注意a是b的前缀和, b[i][j] = a[i][j] - a[i-1][j] - a[i][j-1] + a[i-1][j-1],移项就能求出a[i][j]
  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值