算法 - 前缀和与差分

前缀和和差分是一对逆运算,先给出各自最大的作用:

  • 前缀和
    快速求出原数组中一段数的和(区间和)
  • 差分
    快速对原数组进行统一的操作(eg.某段区间全部+1)

1 前缀和

1.1 是什么

假设有a1, a2, … an,那么前缀和数组Si = a1 + a2 + … + ai,其中S0 = 0,作为边界值。

1.2 作用

前缀和最大的作用是快速求出原数组中一段数的和

  • 预处理:O(n)
  • 每次询问:O(1)

当用O(n)的时间求出前缀和数组后,每次区间和计算都可以用O(1)的时间复杂度求出。
eg.求出[l ~ r]的区间和:S[r] - S[l - 1]
其中:S[r] = a[1] + a[2] + … + a[l - 1] + a[l] + … + a[r]
S[l - 1] = a[1] + a[2] + … + a[l - 1]

而没有前缀和数组时,虽然不用初始化了,但是每次区间和计算都需要O(n),多次区间和计算会大大增加时间复杂度。

1.3 应用

1.3.1 一维前缀和

  • 求前缀和
for(int i = 1; i <= n; i ++) S[i] = S[i - 1] + a[i]; //从1开始
  • 求区间和
    S[r] - S[l - 1]

1.3.2 二维前缀和

  • 求前缀和
    随手画了一个图
    在这里插入图片描述
    想算出上图4 × 4的矩阵和,步骤就是:
    红色矩阵和 + 蓝色矩阵和 - 绿色矩阵和 + 黄色矩阵和
for(int i = 1; i <= n; i ++)
	for(int j = 1; j <= m; j ++)
		S[i][j] = S[i - 1][j] + S[i][j - 1] - S[i - 1][j - 1] + a[i][j]; 
  • 求子矩阵和
    在这里插入图片描述
    想求出绿色(右下角)子矩阵的和,步骤就是:
    红色矩阵和 - 橙色矩阵和 - 蓝色矩阵和 + 紫色矩阵和
    假设红色矩阵的左上角和右下角坐标分别是(x1, y1), (x2, y2),那么公式为:
    S[x2][y2] - S[x1 - 1][y2] - S[x2][y1 - 1] + S[x1 - 1][y1 - 1]

2 差分

2.1 是什么

前面已经提到,差分是前缀和的逆运算。也就是说,给定a1, a2, … an, 构造数组b1, b2, … bn, 使得ai = b1 + b2 + … + bi,数组B就是数组A的差分。(当然,数组A是数组B的前缀和)

2.2 作用

差分最大的作用是快速对原数组进行统一的操作(eg.某段区间全部+1)

  • 求差分数组:O(n)
  • 每次操作:O(1)

当用O(n)的时间复杂度求出差分数组B后,每次的区间操作都可以用O(1)的时间复杂度实现。
eg.[l ~ r]区间都加1,我们只需要对B数组:b[l] + cb[r + 1] - c
当b[l] + c后,对应的前缀和数组A里面的a[l]及之后的都会全+c,再让b[r+1] - c,就可以把不用加的那一段再减回来,保证只在[l, r]区间内操作。

如果不用差分数组的话,每次在原数组上的操作时间复杂度都是O(n),多次的O(n)操作也会大大增加时间复杂度。

2.3 应用

2.3.1 一维差分

  • insert函数
void insert(int l, int r, int c){
	//使a[l ~ r]内全部+c
	b[l] += c;
	b[r + 1] -= c;
}
  • 求差分数组
    实际上,一维差分数组b1 = a1, b2 = a2 - a1, b3 = a3 - a2, … b[n] = a[n] - a[n-1],但我们不从这个结果入手,而是从本质去构造差分数组。
    求差分数组有一个很优雅的方法。前缀和数组里每次读入一个ai,在差分数组里都可以理解为对应位置加上ai,而其他位置不做改变,这样就能保证读完一遍ai,就会有对应的差分数组。
//主程序
for(int i = 1; i <= n; i ++){
	scanf("%d", &a[i]);
	insert(i, i, a[i]);
}
  • 每次操作
    调用insert函数即可

2.3.2 二维差分

  • insert函数
    在这里插入图片描述
    对红色框做统一的操作,步骤是:
    紫色框 - 绿色框 - 蓝色框 + 橙色框
    (比如紫色框,对左上方x1y1的处理影响的就是其右下部分全部)
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;
}
  • 求差分数组
    二维和一维差分数组的构造方式一样,怎样由每次的a[i]得到b[i]呢?
    一维差分的处理方式是给每个b[i ~ i]区间加一个值a[i],二维差分的处理方式就是给每个b[ij ~ ij]子矩阵加上a[i]就好了。
for(int i = 1; i <= n; i ++)
	for(int j = 1; j <= m; j ++)
		insert(i, j, i, j, a[i][j]);
  • 每次操作
    调用insert函数即可

注:
在每次差分处理后,题目一定不会要求给出差分数组,而是处理好的数组,我们对差分数组再构造一次前缀和就行,该一维一维,该二维二维。有谁能想到我的二维构造一开始WA就因为单纯+a[i][j]呢

例题

AcWing 795.前缀和

#include<iostream>

using namespace std;

const int N = 100010;
int a[N], S[N];

int main(){
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);

    for(int i = 1; i <= n; i ++) S[i] = S[i - 1] + a[i];

    while(m --){
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", S[r] - S[l - 1]);
    }

    return 0;
}

AcWing 796. 子矩阵的和

#include<iostream>

using namespace std;

const int N = 1010;
const int M = 1010;
int a[N][M], S[N][M];

int main(){
    int n, m, q;
    scanf("%d%d%d", &n, &m, &q);
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++)
            scanf("%d", &a[i][j]);

    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++)
            S[i][j] = S[i][j - 1] + S[i - 1][j] - S[i - 1][j - 1] + a[i][j];

    while(q --){
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", S[x2][y2] - S[x2][y1 - 1] - S[x1 - 1][y2] + S[x1 -1][y1 - 1]);
    }        

    return 0;

}

AcWing 797. 差分

#include<iostream>

using namespace std;

const int N = 100010;

int a[N], b[N];

void insert(int l, int r, int c){
    //使a[l ~ r]内全部+c
    b[l] += c;
    b[r+1] -= c;
}

int main(){
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i ++) scanf("%d", &a[i]);

    //差分初始化
    for(int i = 1; i <= n; i ++) insert(i, i, a[i]);

    while(m --){
        int l, r, c;
        scanf("%d%d%d", &l, &r, &c);
        insert(l, r, c);
    }

    //用差分构造前缀和
    for(int i = 1; i <= n; i ++){
        a[i] = a[i - 1] + b[i];
        printf("%d ", a[i]);
    }

    return 0;
}

AcWing 798. 差分矩阵

#include<iostream>

using namespace std;

const int N = 1010;
const int M = 1010;
int a[N][M], b[N][M];

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(){
    int n, m, q;
    scanf("%d%d%d", &n, &m, &q);
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++)
            scanf("%d", &a[i][j]);

    //差分初始化
    for(int i = 1; i <= n; i ++)
        for(int j = 1; j <= m; j ++)
            insert(i, j, i, j, a[i][j]);

    //利用差分处理
    while(q --){
        int x1, y1, x2, y2, c;
        scanf("%d%d%d%d%d", &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];
            printf("%d ", a[i][j]);
        }
        printf("\n");
    }


     return 0;       
}

bug汇总:
1.Segmentation Fault
scanf没有加&
2.WA
二维差分变二维前缀和没反应过来用公式…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值