【学习总结】一、二维前缀和 && 一、二维差分

文章详细介绍了在准备蓝桥杯竞赛时遇到的矩阵相关问题,特别是前缀和与差分的概念,包括一维和二维的情况。通过图解和代码示例,阐述了如何利用前缀和快速计算序列和解决查询问题,以及如何运用差分进行序列修改。此外,还讨论了二维矩阵的前缀和与差分在处理子矩阵和操作中的应用。
专栏: 数据结构和算法
这不是马上要蓝桥杯了嘛,我就开始刷蓝桥杯的真题和模拟题( 点这(填空)| 点这(编程))
发现出现了很多涉及矩阵的的题,其中前缀和差分很重要,但每次做题又容易写错或理解不当。
因此我要在这认真的总结一下这部分知识点,并搭配图解和习题从本质上深刻的理解它们。

一维前缀

在高中的时候我们肯定都学了数列吧,还有数列求和。那这个公式就一点不陌生了。

数列的前n项和Sn 减去 数列的前n-1项和Sn-1等于它的通项公式,而将这种方式运用在编程中就能快速算出每一项对应的前缀和。

一般为了方便起见数组下标都从1开始,上面动态图掩饰主要是便于大家理解。

将上述图解用代码实现:

for (int i = 1 ; i <= n ; i++)
{
   cin>>nums[i];
   presum[i] = presum[i-1] + nums[i]; //求前缀和
}

光说,又不刷题……

大家看一下这道题(就知道这东西是有多么方便了)

输入一个长度为 n 的整数序列。
接下来再输入 m 个询问,每个询问输入一对 l , r。
对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数数列。
接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。
输出格式
共 m 行,每行输出一个询问的结果。
数据范围
1 ≤ l ≤ r ≤ n
1 ≤ n , m ≤ 100000
−1000 ≤ 数列中元素的值 ≤ 1000
输入样例:
5 3
2 1 3 6 4
1 2
1 3
2 4
输出样例
3
6
10

这道题如果你不会这个方法,你多半会选择对于每一次的查询然后去遍历一遍[l , r] 求和。

那多浪费时间啊,这里以后就用这个方法就行了~(超大声)

#include <iostream>
using namespace std;
const int N = 1e5+10;
int nums[N] , presum[N] , n , m , l , r; 
int main()
{
    cin>>n>>m;
    for (int i = 1 ; i <= n ; ++i)
    {
        cin>>nums[i];
        presum[i] = presum[i-1] + nums[i];
    }
    while(m--)
    {
        cin>>l>>r;
        cout<<presum[r] - presum[l-1]<<endl; //这就算出来了,多爽啊~
    }
    return 0;
}

上述presum[ l - 1 ] 处注意啊,别写成presum[ l ] 了。

二维前缀

二维前缀可谓是本节的重中之重,且一定要理解性的掌握~

这里我给出一张原二维数组a ,和二维前缀和数组b ,你能看出b它是怎么得来的嘛?(基于一维前缀)

这里以b[2][2] ==45 为例,它就是图中原二维数组a 涂色的部分的数值之和。

你会想这多容易啊,哈哈,但是如果我给你如图a[][]数组让你用代码去构造如图b[][]的二维前缀和数组,你能有什么快捷的方法嘛?

难道你要选择对于求每一个二位前缀和的值都用两个循环!!!!

天啊~,你可以先冷静下来,想一想,一维前缀和s[ i ] = s[ i-1 ] +a[ i ] ; 你会发现对于每一个前缀和都可以利用到它的前一个前缀和 ,然后只需要加上这一项的原数组值就完事了,一个循环就搞定了~ ,那这里的二维前缀和不也是异曲同工之妙嘛!!

这里我直接给出公式,然后你在结合图解理解一下~

b[i] [j] = b[i-1][j] + b[i][j-1 ] - b[i-1][ j-1] + a[i] [j]

上述图解是从数组0开始的,只是为了帮助大家理解,在以后的做题中,要从数组下标1开始赋值和运算,这会很方便的。

好了,既然都学到了二维前缀和了,我们再随其自然把求子矩阵和讲一下。

子矩阵和:给你二维数组中两个坐标 (x1 , y1)和 (x2 , y2),以它们作为一个子矩阵的左上角坐标和右下角坐标,求所围成的矩阵中所有元素之和。

对于新知识没有思路,先别慌!!(暴力肯定谁都会,这里强调的是快捷的方法

你可以先和已经学过的类似的知识做一下对比,说不定就有思路勒~ ,就像一维前缀推导二维前缀一样,这里也可以试着尝试推导一下公式。

指引:你会发现,之前我们就用一维前缀很快的求出来了,一维数组对应的区间之间的所有元素之和(只是这个比较容易理解),那这里也是不是可以用二维前缀和求子矩阵和呢?
我的答案是:Yes , 方向对了,你离成功就快了,这里的方法可以对比求二维前缀和的思想,很相似的~

这里我先把公式给出来,然后你再根据图解理解一下。

Sum(之矩阵和) = b[x2][y2] - b[x2][y1- 1] - b[x1 - 1][y2] + b[x1 - 1][y1 - 1]

好了,我这里给出一道模板题( 就是用我上面讲的方法 ),来练一下手吧~

输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。
对于每个询问输出子矩阵中所有数的和。
输入格式
第一行包含三个整数 n,m,q 。
接下来 n 行,每行包含 m 个整数,表示整数矩阵。
接下来 q 行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。
输出格式
共 q 行,每行输出一个询问的结果。
数据范围
1≤ n, m ≤ 1000
1 ≤ q ≤ 200000
1 ≤ x1 ≤ x2 ≤ n
1≤ y1 ≤ y2 ≤ m
−1000≤矩阵内元素的值≤1000
输入样例:
3 4 3
1 7 2 4
3 6 2 8
2 1 2 3
1 1 2 2
2 1 3 4
1 3 3 4
输出样例:
17
27
21
#include <iostream>
using namespace std;
const int N=1001;
int a[N][N], b[N][N], n, m, q, x1, x2, y1, y2;
int main()
{
    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]); 
            b[i][j] = b[i-1][j] + b[i][j-1] - b[i-1][j-1] + a[i][j];//求二维前缀和
        }
    }
    while(q--){
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", b[x2][y2] - b[x1 - 1][y2] - b[x2][y1 - 1] + b[x1 - 1][y1 - 1]);//直接就来了,多方便啊,哈哈
    }
    return 0;
}

一维差分

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

对比一维前缀:我们通过presum[i] = presum[i-1] + nums[i] 来求nums[i] 的一维前缀和数组presum[i] , 而我们要研究的一维差分就是通过presum[i]数组来重构它的nums[i]数组。

方法:这其实很简单,聪明的你们肯定已经发现,将presum[i] = presum[i-1] + nums[i] 这个公式变一下型,就是:nums[i] = presum[i] - presum[i-1] ,然后就和一维前缀的求法一样了~

那这又有什么实际用途呢?

这和高数的求导和差分一样, 用途可多了,而且非常实用~

这里看一道题,你就知道是有多么方便了。

输入一个长度为 n 的整数序列。
接下来输入 m 个操作,每个操作包含三个整数 l , r , c,表示将序列中 [l,r] 之间的每个数加上 c。
请你输出进行完所有操作后的序列。
输入格式
第一行包含两个整数 n 和 m。
第二行包含 n 个整数,表示整数序列。
接下来 m 行,每行包含三个整数l,r,c,表示一个操作。
输出格式
共一行,包含 n 个整数,表示最终序列。
数据范围
1≤ n , m ≤ 100000
1 ≤ l ≤ r ≤ n
−1000 ≤ c ≤ 1000
−1000 ≤ 整数序列中元素的值 ≤ 1000
输入样例:
6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1
输出样例:
3 4 5 3 4 2

如果你没学一维差分,那我初学时也和你们一样,对于每次操作,都用一个循环去给区间的每个元素加上一个c , 这显而易见,我们肯定会超时的,那怎样优化呢,这里就要采用这个妙招了~

解释:你想一下,假如现在你已经构造出来presum[i] 的差分数组 nums[i], 你会想到它的什么特点嘛?

原来,你对num[i] 的一个下标为r1的数组值加上一个 c ,那么将会对它前缀和数组presum[i] 产生很大的影响,即将presum[r1] ~ presum[n] 都加上了一个 c .

Amazing ! ! .这太妙了吧,对,算法就是这么的奇妙,同样的知识翻转一下,用途又大大提高了~

#include <iostream>

using namespace std;
const int N=100010;
int presum[N], nums[N], n, m, l, r, c;
int main(){
    scanf("%d%d", &n, &m);
    for (int i = 1 ; i <= n ; i++){
        scanf("%d", &presum[i]);
        nums[i] = presum[i] - presum[i-1]; //构造一维差分
    }
    while (m--)
    {
        scanf("%d%d%d", &l, &r, &c);
        nums[l] += c; 
        nums[r+1] -= c; //我们只想对[l , r]区间元素产生影响,需要对r ~ n 元素的影响消除掉。 
    }
    for (int i = 1 ; i <= n ; i++){
        presum[i] = presum[i-1] + nums[i];  //我们改变的是nums[i],还要采用一维前缀和求回presum[i]。
        printf("%d ", presum[i]); 
    }
    return 0;
}

二维差分

认真点哦 ,掉头发的难点来了~

二维差分就是通过a[][] 的二维前缀和数组b[][] 去求它的二维差分数组a[][],这听上去很简单,但如果将二维差分融入实际问题的求解中去,代码操作起来还是有点费劲的,不过你可以放一百个♥,我一定会用最通俗易懂的那种方法教会你的,哈哈,不吹了,我也没把握~

这里对比一维差分数组的求解,就是巧妙的将求二维前缀和的公式:

b[i] [j] = b[i-1][j] + b[i][j-1 ] - b[i-1][ j-1] + a[i] [j]

转换后求二维差分公式:a[i][j] = b[i][j] - b[i-1][j] - b[i][j-1] + b[i-1][j-1];

这里我就举例一道求子矩阵和的升级版,就是将求得的每个子矩阵全部元素加上一个 c ,然后输出操作完所有步骤后的二维数组。

你看不起谁啊,这题不就是和之前讲的那道一维差分一样的方法嘛!!

先别骄傲,二维差分要考虑的情况要比一维差分复杂多了,并且还是建立在差分的基础上,这题只是这类知识点最典型的实际问题,更为复杂多变的题型还要我们多练,多学习,多总结,这样才算真真掌握了这类知识点 !!

输入一个 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

这题难点不在如何构造二维差分数组,而在于如何在求出的二维差分数组上加上一个c ,使它的原二维前缀和数组的子矩阵里的所有元素都加上一个c ,这里的操作,很多同学要么写错,要么没考虑全面,这里我就将对此步操作,着重细讲一下,希望能帮助大家理解它~

假设我们已经构建好了二维数组b[][] 的二维差分数组 a[][] ,现在要处理的是如何在a[][] 上加c,使其二维前缀和数组b[][]在指定的子矩阵内的所有元素都加上一个c 。
void insert(int x1, int y1, int x2, int y2, int c)
{
    a[x1][y1] += c;
    a[x2 + 1][y1] -= c;
    a[x1][y2 + 1] -= c;
    a[x2 + 1][y2 + 1] += c;
}

上面给出的这个自定义函数就能实现上述功能。如果你能看懂那肯定更好啊(先思而后行),

如果没看懂,可以看我给出的图解加以理解~

注意:图中的a[][]、b[][]数组和题解是反着的,当时画完图才反应过来!

上述图可以形象比喻成:

这就像在一个区域内倒水:

  • 在左上角放入水源(+c)
  • 在边界设置挡板(-c、-c)
  • 在右下角补偿(+c)
  • 最终水只会留在我们想要的区域内!

明白上面这个自定义函数那么你就基本成功了,鼓励下自己吧~

#include <iostream>
using namespace std;
const int N = 1e3 + 10;
int b[N][N], a[N][N];
void insert(int x1, int y1, int x2, int y2, int c)
{
    a[x1][y1] += c;
    a[x2 + 1][y1] -= c;
    a[x1][y2 + 1] -= c;
    a[x2 + 1][y2 + 1] += c;
}
int main()
{
    int n, m, q;
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            cin >> b[i][j],
            a[i][j] = b[i][j] - b[i][j-1] - b[i-1][j] + b[i-1][j-1];//构建二维差分数组
    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++)
        {
            b[i][j] = b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1] + a[i][j];  //二维前缀和
        }
    }
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            printf("%d ", b[i][j]);//输出操作完所有步骤后的b[][]
        }
        printf("\n");
    }
    return 0;
}

看到这里,我如果告诉你这个insert()函数还可以用来构建二维差分数组a[][](不用那个二维差分公式),你相信嘛。哈哈~

答案是肯定的,只不过这需要你对这个函数

 for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= m; j++)
        {
            insert(i, j, i, j, b[i][j]);      //构建二维差分数组a[][]
        }
    }
一个很形象的栗子:这就像是在记账本上用特殊的方式记录,使得我们既能知道每天的哪个时间点具体存了多少钱(差分数组a),又能随时查询到每天任意时间点的总余额(前缀和数组b)。参考上面那道例题,那么insert(i, j, i, j, b[i][j]) 只影响 a[i][j],因为 x1 = x2 和 y1 = y2,所以只在 (i, j) 位置增加 b[i][j]。你可以想象一开始a[i][j]和b[i][j]都为空,那么a[i][j]一定是b[i][j]的差分数组,我们通过这种 单点更新的方式,确保差分数组 a[i][j] 被构建为可以通过累加得到原始的前缀和数组 b[i][j]。那么既然每个a[i][j]的点对应的前缀和都为每次单点更新的b[i][j],那么这个数组a[i][j]不就是前缀和数组b[i][j]对应的差分数组吗?还是不懂,直接过一遍样例对比一下 ~
  比如我这篇博客里面清理水草这道题: 【蓝桥杯】每日四道编程题(两道真题+两道模拟)| 第四天-优快云博客

听懂了嘛?,没听懂的话,你画个图分析下也挺简单的。

作者为啥要给出这个方法啊?哈哈,直接用公式多爽啊,我也想啊,但是我也是在别人的优秀题解中看到的这个方法,真是太秀了,而且理解这个方式,对于我们更为深入掌握二维差分和前缀有一定的帮助的,还有就是方法有很多,但是最好的永远不止于当下,因为它等待我们去探索!!

最后,我最近也在刷蓝桥杯,然后更新了蓝桥杯每日四题(填空编程),应该有部分矩阵相关的题,有时间可以去看看~

评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吹往北方的风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值