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

专栏: 数据结构和算法
这不是马上要蓝桥杯了嘛,我就开始刷蓝桥杯的真题和模拟题( 点这(填空)| 点这(编程))
发现出现了很多涉及矩阵的的题,其中前缀和差分很重要,但每次做题又容易写错或理解不当。
因此我要在这认真的总结一下这部分知识点,并搭配图解和习题从本质上深刻的理解它们。

一维前缀

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

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

【学习总结】一、二维前缀和 && 一、二维差分_第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 
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[ l - 1 ] 处注意啊,别写成presum[ l ] 了。

二维前缀

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

【学习总结】一、二维前缀和 && 一、二维差分_第2张图片

这里我给出一张原二维数组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]

【学习总结】一、二维前缀和 && 一、二维差分_第3张图片

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

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

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

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

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

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

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

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

【学习总结】一、二维前缀和 && 一、二维差分_第4张图片

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

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

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;
}

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

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

【学习总结】一、二维前缀和 && 一、二维差分_第5张图片

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

#include 
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[][]
        }
    }
主观的解释:我们可以先假想b数组为空,那么a数组一开始也为空,但是实际上b数组并不为空,因此我们每次让a数组以(i,j)为左上角到以(i,j)为右下角面积内元素(其实就是一个小方格的面积)去插入 c=b[i][j],等价于原数组b中(i,j) 到(i,j)范围内 加上了 b[i][j] (既然你都通过二维前缀将a数组构建出了题目给出的b数组,那么是不是说明a[][]就是b[][]的二维差分数组呢!!),因此执行n*m次插入操作,就成功构建了差分a数组.

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

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

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

你可能感兴趣的:(数据结构与算法,学习,蓝桥杯,算法)