【算法】-前缀和与差分

目录

前缀和

从「一维数组的动态和」说起

前缀和是什么

前缀和有什么用

用途一:求数组前 i 个数之和

用途二:求数组的区间和

 二维数组的前缀和(子矩阵的和)

差分

一维差分

(1)如何构造差分b数组?

(2)构造差分数组有什么用?

(3)如何求经过操作后的原数组呢

二维差分

(1)如何构造差分数组?

(2)如何给子矩阵中的每一个数都加上c?

(3)如何求经过操作后的原数组?


前缀和

从「一维数组的动态和」说起

        前缀和,英文是 preSum,是一种基础算法。

        为了了解背景,咱们先看 LeetCode 上一个简单题目:1480. 一维数组的动态和

        这个题让我们求 runningSum[i] = sum(nums[0]…nums[i]),如果你没有了解过「前缀和」,可能会写出两重循环:每个 runningSum[i],累加从 0 位置到 i 位置的 nums[i]。即,写出下面的代码:

int* runningSum(int* nums, int numsSize, int* returnSize){
    int* preSum = malloc(sizeof(int) * numsSize);
    *returnSize = numsSize;
    for (int i = 0; i < numsSize; ++i) {
        int sum = 0;
        for (int j = 0; j <= i; ++j) {
            sum += nums[j];
        }
        preSum[i] = sum;
    }
    return preSum;
}

        两重循环的时间复杂度是 O(n*2),效率比较低。 ​

        其实我们只要稍微转变一下思路,就发现没必要用两重循环。 ​当已经求出 runningSum[i] = sum(nums[0]…nums[i]) 那么 runningSum[i + 1] = sum(nums[0]…nums[i]) + nums[i + 1] = runningSum[i] + nums[i + 1]

int* runningSum(int* nums, int numsSize, int* returnSize){
    int* preSum = malloc(sizeof(int) * numsSize);
    *returnSize = numsSize;
    for (int i = 0; i < numsSize; ++i) {
        if (i == 0) {
            preSum[i] = nums[i];
        } else {
            preSum[i] = preSum[i - 1] + nums[i]; 
        }
    }
    return preSum;
}

前缀和是什么

        通过上面的内容,很容易理解preSum 数组其实就是「前缀和」。 ​         

        「前缀和」 是从 nums 数组中的第 0 位置开始累加,到第 i 位置的累加结果,我们常把这个结果保存到数组 preSum 中,记为 preSum[i]。         

        在前面计算「前缀和」的代码中,计算公式为 preSum[i] = preSum[i - 1] + nums[i] ,为了防止当 i = 0 的时候数组越界,所以加了个 if (i == 0) 的判断,即 i == 0 时让 preSum[i] = nums[i]。 ​

        在其他常见的写法中,为了省去这个 if 判断,我们常常把「前缀和」数组 preSum 的长度定义为原数组长度+1。preSum 的第 0 个位置,相当于一个占位符,置为 0。 那么就可以把 preSum 的公式统一为 preSum[i] = preSum[i - 1] + nums[i - 1],此时的 preSum[i] 表示 nums 中 iii 元素左边所有元素之和(不包含当前元素 iii)。 ​         

        下面以 [1, 12, -5, -6, 50, 3] 为例,用动图讲解一下如何求 preSum。

1614561004-TEQwGZ-303,preSum.gif

上图表示:

  • preSum[0] = 0;
  • preSum[1] = preSum[0] + nums[0];
  • preSum[2] = preSum[1] + nums[1];
  • ...

前缀和有什么用

用途一:求数组前 i 个数之和

        求数组前 i 个数之和,是「前缀和」数组的定义,所以是最基本的用法。

        如果要求 nums 数组中的前 2 个数的和(即 sum(nums[0], nums[1])) ,直接返回 preSum[2] 即可。 同理,如果要求 nums 数组中所有元素的和(即 sum(nums[0]..nums[length−1])),直接返回 preSum[length] 即可。

用途二:求数组的区间和

        利用 preSum 数组,可以在 O(1) 的时间内快速求出 nums 任意区间 [i,j](两端都包含) 内的所有元素之和

        公式为: sum(i ,j) = preSum[j+1] − preSum[i];

        其实就是消除公共部分即 0~i-1 部分的和,那么就能得到 i~j 部分的区间和。注意上面的式子中,使用的是 preSum[j + 1] 和 preSum[i]。

  • preSum[j + 1] 表示的是 nums 数组中 [0,j] 的所有数字之和(包含 0 和 j)。
  • preSum[i] 表示的是 nums 数组中 [0,i−1] 的所有数字之和(包含 0 和 i−1)。
  • 当两者相减时,结果留下了 nums 数组中 [i,j] 的所有数字之和。

        接下来我们以 643. 子数组最大平均数 I 为例,这个题目要求数组nums中所有长度为k的连续子数组中的最大的平均数。

        这个题可以用「前缀和」来解决,也可以用固定大小为 k 的「滑动窗口」来解决。

        要求大小为 k 的窗口内的最大平均数,可以求 [i, i + k] 区间的最大「和」再除以 k,即要求 (preSum[i] - preSum[i - k]) / k 的最大值。


        总之,如果题目要求「区间和」的时候,那么就可以考虑使用「前缀和」。

double findMaxAverage(int* nums, int numsSize, int k) {
    double preSum[numsSize + 1];
    preSum[0] = 0;
    double max = INT_MIN; 
    for (int i = 1; i <= numsSize; ++i) { 
        preSum[i] = preSum[i - 1] + nums[i - 1];
        if(i - k >= 0) {
            double average = (preSum[i] - preSum[i - k]) / k;
            max = max > average ? max : average; 
        }
    }
    return max;
}

 二维数组的前缀和(子矩阵的和)

        初始化前缀和数组,定义一个二维数组s[i][j],用来记录(代表)前(i, j)到原点所有数的和项数据的和。S[i, j]即为图中绿色部分所有数的的和为:

        S[i,j]怎么计算:S[i, j] = S[i, j−1] + S[i−1, j]−S[i−1, j−1] + a[i, j]

         那么(x1,y1),(x2,y2)这一子矩阵中所有数的和怎么计算呢?

        公式是:res = S(x2, y2) - S(x1 - 1, y2) - S(x2, y1 - 1) +S(x1 - 1, y1 - 1)

        其中表示从原始数组中左上角到坐标为的矩形区域的和。

        学习了以上二维数组的知识,我们就可以尝试解决 304. 二维区域和检索 - 矩阵不可变

 

typedef struct {
    int** preSumMatrix;
    int rowNum;
} NumMatrix;

NumMatrix* numMatrixCreate(int** matrix, int matrixSize, int* matrixColSize) {
    NumMatrix* obj = (NumMatrix*)calloc(1, sizeof(NumMatrix));
    obj->preSumMatrix = (int**)calloc(matrixSize + 1, sizeof(int*));
    obj->rowNum = matrixSize + 1;
    for (int i = 0; i <= matrixSize; i++) {
        obj->preSumMatrix[i] = (int*)calloc(matrixColSize[0] + 1, sizeof(int));
    }
    //计算前缀和数组S[i][j]
    for (int i = 0; i < matrixSize; i++) {
        for (int j = 0; j < matrixColSize[i]; j++) {
            obj->preSumMatrix[i + 1][j + 1] =
                obj->preSumMatrix[i][j + 1] + obj->preSumMatrix[i + 1][j] - obj->preSumMatrix[i][j] + matrix[i][j];
        }
    }
    return obj;
}
// res = S(x2, y2) - S(x1 - 1, y2) - S(x2, y1 - 1) +S(x1 - 1, y1 - 1)
int numMatrixSumRegion(NumMatrix* obj, int row1, int col1, int row2, int col2) {
    return obj->preSumMatrix[row2 + 1][col2 + 1] -obj->preSumMatrix[row2 + 1][col1] - obj->preSumMatrix[row1][col2 + 1] + 
            obj->preSumMatrix[row1][col1];
}

void numMatrixFree(NumMatrix* obj) {
    for (int i = 0; i < obj->rowNum; i++) {
        free(obj->preSumMatrix[i]);
    }
    free(obj->preSumMatrix);
}
/**
 * Your NumMatrix struct will be instantiated and called as such:
 * NumMatrix* obj = numMatrixCreate(matrix, matrixSize, matrixColSize);
 * int param_1 = numMatrixSumRegion(obj, row1, col1, row2, col2);

 * numMatrixFree(obj);
*/

差分

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

一维差分

差分数组:

        首先给定一个原数组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[i]

        a数组是b数组的前缀和数组,反过来我们把b数组叫做a数组的差分数组。即,每一个a[i]都是b数组中从头开始到i的的一段区间和。 

(1)如何构造差分b数组?

        最为直接的方法如下:

        a[0 ]= 0;

        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[1] + b[2] + ,,, + b[n] = a[n]

        我们只要有b数组,最后通过前缀和运算,就可以在O(n) 的时间内得到a数组。

(2)构造差分数组有什么用?

        现在我们要给a数组在给定区间[l, r]上的每一个数加上c,即a[l] + c , a[l+1] + c , a[l+2] + c ,,,, a[r] + c。暴力的做法是for枚举l~r,时间复杂度是O(n),如果我们需要对原数组执行m次这样的操作,时间复杂度就会变成O(n*m)。当数据量很庞大时很可能就会超时,那有没有更高效的方法呢?可以考虑一下差分操作~

        始终要记得,a数组是b数组的前缀和数组,对b数组的b[i]的修改,就一定会影响到a数组中从a[i]及往后的每一个数

        通过上面的解释,那我们如何如和用差分数组b实现给a数组在[l ,r]上+c呢?

  • 首先让差分b数组中的 b[l] + c ,a数组变成 a[l] + c ,a[l+1] + c,,,,,, a[n] + c;,但我们想要的是给a数组在定区间[l,r]上+c,即多了从r+1开始到后面所有数,这一段数据)。

  • 为此,我们打个补丁,b[r+1] - c, a数组变成 a[r+1] - c,a[r+2] - c,,,,,,,a[n] - c;(即b[r+1] - c完成了减去上述从r+1开始都后面多出来的数据)

(3)如何求经过操作后的原数组呢

        我们只要有了b数组,并对它进行b[l] += c,b[r + 1] -= c操作(使得a数组在[l,r]区间上每一个数+C),最后通过对b数组进行前缀和运算,就可以在O(n) 的时间内得到a数组


二维差分

        如果扩展到二维,我们需要让二维数组被选中的子矩阵中的每个元素的值加上c,是否也可以达到O(1)的时间复杂度呢?

        答案是可以的,考虑二维差分。 a[][]数组是b[][]数组的前缀和数组,那么b[][]是a[][]的差分数组。只要用原数组a[i][j] 去构造差分数组:b[i][j] ,使得a数组中a[i][j]是b数组左上角(1,1)到右下角(i,j)所包围矩形元素的和。

(1)如何构造差分数组?

        我们去逆向思考。同一维差分,我们构造二维差分数组目的是为了 让原二维数组a中所选中子矩阵中的每一个元素加上c的操作,可以由O(n*n)的时间复杂度优化成O(1)

        在对子矩阵每一个数 + C操作之前,我们先要构造好差分数组b[i][j]。代码如下:

// 构造差分数组
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            b[i][j] = a[i][j] - a[i - 1][j] - a[i][j - 1] + a[i - 1][j - 1];
            


(2)如何给子矩阵中的每一个数都加上c?

        已知原数组a中被选中的子矩阵为 以(x1, y1)为左上角,以(x2, y2)为右上角所围成的矩形区域;

        始终要记得,a数组是b数组的前缀和数组,比如对b数组的b[i][j]的修改,会影响到a数组中从a[i][j]及往后的每一个数。

        假定我们已经构造好了b数组,类比一维差分,我们执行以下操作来使被选中的子矩阵中的每个元素的值加上c

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

        b[x1][y1] + = c;

        b[x1][y2+1] - = c;

        b[x2+1][y1] - = c;

        b[x2+1][y2+1] + = c;

for(int i = x1; i <= x2; i++) {
    for(int j = y1; j <= y2; j++) {
        a[i][j] += c;
    }
}

我们画个图去理解一下这个过程:

    b[x1][y1] += c ; 对应图1 ,让整个a数组中蓝色矩形面积的元素都加上了c
    b[x1][y2+1] -= c ;对应图2 ,让整个a数组中绿色矩形面积的元素再减去c,使其内元素不发生改变。
    b[x2+1][y1] -=c ;对应图3 ,让整个a数组中紫色矩形面积的元素再减去c,使其内元素不发生改变。
    b[x2+1][y2+1] += c; 对应图4,,让整个a数组中红色矩形面积的元素再加上c,红色内的相当于被减了两次,再加上一次c,才能使其恢复。

我们将上述操作(子矩阵每一个数 + C)代码如下。时间复杂度O(1):

{   
	//对a数组中的(x1,y1)到(x2,y2)之间的元素都加上了c
    b[x1][y1] += c;
    b[x2+1][y1] -= c;
    b[x1][y2+1] -= c;
    b[x2+1][y2+1] += c;
}

(3)如何求经过操作后的原数组?

        当我们构造了二维差分数组b[i][j],并对b[i][j]进行了

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

        4个操作(使得a数组的子矩阵每一个数都加上了c),最后通过对b数组进行前缀和运算(二维前缀和运算),就可以在短时间时间内得到a数组。 

【acwing 798. 差分矩阵】

输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1) 和 (x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。

每个操作都要将选中的子矩阵中的每个元素的值加上 c。

请你将进行完所有操作后的矩阵输出。

输入格式

第一行包含整数 n,m,q。

接下来 n 行,每行包含 m 个整数,表示整数矩阵。

接下来 q 行,每行包含 5 个整数 x1,y1,x2,y2,c,表示一个操作。

** 输出格式**

共 n 行,每行 m 个整数,表示所有操作进行完毕后的最终矩阵。

数据范围

1≤n,m≤1000,
1≤q≤100000,
1≤x1≤x2≤n,
1≤y1≤y2≤m,
−1000≤c≤1000,
−1000≤矩阵内元素的值≤1000

输入样例:

3 4 3
1 2 2 1
3 2 2 1
1 1 1 1
1 1 2 2 1
1 3 2 3 2
3 1 3 4 1

输出样例:

2 3 4 1
4 3 4 1
2 2 2 2

#include <stdio.h>

#define N 1010

int main() {
    int n, m, q;
    scanf("%d %d %d", &n, &m, &q);
    int a[N][N], b[N][N];
    // 读取原始数组 a[][]
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            scanf("%d", &a[i][j]);
        }
    }
    // 构造差分数组 b[][]
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            b[i][j] = a[i][j] - a[i - 1][j] - a[i][j - 1] + a[i - 1][j - 1];
        }
    }
    // 处理更新操作
    while (q--) {
        int x1, y1, x2, y2, c;
        scanf("%d %d %d %d %d", &x1, &y1, &x2, &y2, &c);
        b[x1][y1] += c;
        b[x1][y2 + 1] -= c;
        b[x2 + 1][y1] -= c;
        b[x2 + 1][y2 + 1] += c;
    }
    // 通过求差分数组b的前缀和来得到原数组a
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            b[i][j] = b[i][j] + b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1]; // 前缀和公式
        }
    }
    // 输出原数组a
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            printf("%d ", b[i][j]);
        }
        printf("\n");
    }
    return 0;
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值