二维差分
如果扩展到二维,我们需要让二维数组被选中的子矩阵中的每个元素的值加上c
,是否也可以达到O(1)
的时间复杂度。答案是可以的,考虑二维差分。
a[][]
数组是b[][]
数组的前缀和数组,那么b[][]
是a[][]
的差分数组
原数组: a[i][j]
我们去构造差分数组: b[i][j]
使得a
数组中a[i][j]
是b
数组左上角(1,1)
到右下角(i,j)
所包围矩形元素的和。
如何构造b
数组呢?
我们去逆向思考。
同一维差分,我们构造二维差分数组目的是为了 让原二维数组a
中所选中子矩阵中的每一个元素加上c
的操作,可以由O(n*n)
的时间复杂度优化成O(1)
已知原数组a
中被选中的子矩阵为 以(x1,y1)
为左上角,以(x2,y2)
为右下角所围成的矩形区域;
始终要记得,a数组是b数组的前缀和数组,比如对b
数组的b[i][j]
的修改,会影响到a
数组中从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
;
每次对b
数组执行以上操作,等价于:
for(int i=x1;i<=x2;i++)
for(int j=y1;j<=y2;j++)
a[i][j]+=c;
我们画个图去理解一下这个过程:
{:width=”57%”}
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
,才能使其恢复。
我们将上述操作封装成一个插入函数:
void insert(int x1,int y1,int x2,int y2,int c)
{ //对b数组执行插入操作,等价于对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;
}
我们可以先假想a
数组为空,那么b
数组一开始也为空,但是实际上a
数组并不为空,因此我们每次让b
数组以(i,j)
为左上角到以(i,j)
为右下角面积内元素(其实就是一个小方格的面积)去插入 c=a[i][j]
,等价于原数组a
中(i,j)
到(i,j)
范围内 加上了 a[i][j]
,因此执行n*m
次插入操作,就成功构建了差分b
数组.
这叫做曲线救国。
代码如下:
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
insert(i,j,i,j,a[i][j]); //构建差分数组
}
}
总结:
AC代码
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1e3 + 10;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2, int y2, int c)
{
b[x1][y1] += c;
b[x2 + 1][y1] -= c;
b[x1][y2 + 1] -= c;
b[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 >> 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;
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]; //二维前缀和
}
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
printf("%d ", b[i][j]);
}
printf("\n");
}
return 0;
}
其实我很纳闷的是,最后求前缀和不应该用a [i][j] = b[i][j]+ a[i-1][j] + a[i][j-1] - a[i-1][j-1];
cout << a[i][j]<<” “;这个式子吗,虽然最后是一样的结果,我也知道是转化的形式,但是Y总还有你们的题解都直接用差分数组直接得出结果,也不加以讲解说明,是不是对新手不友好呢。。卡了很久
我也这么觉得!!!想了好久qwq
大佬搞懂了吗?我也卡在这里了,一维的题解还是用的前缀和的公式,二维的题解最后求前缀和这里就看不懂了。。。
+1
只是一种方式,直接在b数组上做前缀和可以简化代码,引入额外的数组也没问题。
借用楼上“Barbed”的回复:
差分这样构建的原因:首先假设a,b都为空数组,也就是全部为0的情况,那么我们也可以说,b数组是a数组的差分数组!因为a[i][j] = b[1][1]+…b[i][j],因为都是0,所有情况满足!在这时a数组其实不为空,我们把a数组中的值看作(0+a[i][j])那么a数组的值变了,变成了a[i][j],这时如果b数组要想成为a数组的差分数组,值也要变为a[i][j],所以我们把b[i][j]变为a[i][j],这样就能满足b数组是a数组的差分数组。也就相当于想b数组中插入a[i][j].
个人理解:所以b数组其实对应的值就是a数组的值,前缀和可替换为b
问一下构建差分数组的代码,他的代码不是把a[][] copy 到 b[][]?
不是的,虽然我也没太明白
可以参考这个原始的
借个楼~
b[x1][ y1 ] +=c ;不因该是(x1,y1)到(x2+1,y2+1)都+c嘛
可以参考一维差分的最后计算,就是在不断的计算中,差分数组被不断更新,最后变为原数组的值吧
最后只要求输出变化后的值,所以不以a矩阵输出也可,即直接以b矩阵求自身前缀和的形式将b矩阵覆盖,和一般求前缀和矩阵方法一致。
从前往后求,可以覆盖的,在差分矩阵上直接操作就可以,不需要另开辟数组空间,就直接变成前缀和矩阵。
比如,另开辟专门的前缀和空间的话,假如
s
是前缀和,a
是差分,如果求前缀和s应该是s[i][j] = a[i][j] + s[i][j - 1] + s[i - 1][j] - s[i - 1][j - 1]
但是也可以直接在
a
上操作,不需要另辟空间s
,如下式:a[i][j] = a[i][j] + a[i][j - 1] + a[i - 1][j] - a[i - 1][j - 1]
右边旧的值
a[i][j]
是差分数组i,j
处的值,新的a[i][j]
的含义已经是前缀和了,直接用新的a[i][j]
覆盖掉旧的a[i][j]
再用
+=
简化一下,就成了a[i][j] += a[i][j - 1] + a[i - 1][j] - a[i - 1][j - 1]
前边求子矩阵前缀和那个题也是一样,可以不写s,直接把a原地覆盖为前缀和矩阵也可以。
你说的很对,我的理解:其实根本区别在于s是专门用来储存前缀和的,所以第一个式子很容易理解,至于第二个式子,a数组之所以难理解,是因为赋值过的a[i][j]储存的是前缀和,但后面没有修改没有操作的a[i][j]储存的还是差分数组的值,所以相当于省了空间而已
实际上就是用数组b保存了数组a的元素,也就是说通过b[i][j] = b[i][j - 1] + b[i - 1][j] + b[i][j] - b[i - 1][j - 1],b[i]被替换成a[i]
太牛了,通透
其实,我觉得前面使用初始化差分数组的时候就用这个等式,把b[i][j]放到左边就行,最后再把a[i][j]还原,这样一切都顺理成章,Y总他们就是追求这种极简导致新手根本看不懂,而且会自以为讲得很清楚(笑哭)
差分这样构建的原因:首先假设a,b都为空数组,也就是全部为0的情况,那么我们也可以说,b数组是a数组的差分数组!因为a[i][j] = b[1][1]+…b[i][j],因为都是0,所有情况满足!在这时a数组其实不为空,我们把a数组中的值看作(0+a[i][j])那么a数组的值变了,变成了a[i][j],这时如果b数组要想成为a数组的差分数组,值也要变为a[i][j],所以我们把b[i][j]变为a[i][j],这样就能满足b数组是a数组的差分数组。也就相当于想b数组中插入a[i][j].
👍🏻
兄弟这么晚还在淦吗!强!
秒啊兄弟
悟了!
懂了,这样b就可以保持为a的差分数组
不懂
多看几遍视频,多读几遍
nb
终于懂了!
转了好几圈,终于在老哥这里懂了
牛逼
b[1][1]到b[i][j]矩形所有元素求和,最终得到b[i][j](值等同于a[i][j])是因为在insert操作中,某一个数b[l][k]参与计算后,无论朝右边还是下边计算,都会有一个-b[l][k]与之抵消,最终除了b[i][j](即a[i][j])外,其余全部被抵消为0
insert(i, j, i, j, a[i][j])是为了让b[i][j]这个位置上的元素增加a[i][j],同时从(1, 1)到(i, j)这一段的前缀和也增加到了a[i][j],如果使用两层for循环遍历整个b数组,则有:(1, 1)到(1, 1)的前缀和从0增加到了a[1][1],(1, 1)到(1, 2)的前缀和增加到了a[1][2]......,也就是相当于(i, j)位置的元素的前缀和全都变成了a[i][j],那么b数组自然变成了a数组的差分数组(根据差分数组的定义)。
通透👍
学习总结了一、二维前缀和差分,希望对你有帮助~
https://blog.csdn.net/m0_74215326/article/details/129620912?spm=1001.2014.3001.5502
超级清楚orz
个人理解:insert的作用是使那一片区域内的值都加上c,而区域外面求和的话不受影响,那么当区域为一个点的时候,例如b[0][0]直接插入a[0][0]的值,那就使得这一个点的值从b[0][0]变成了a[0][0]的值,但是从b[0][1]处求和仍然是b[]0[1]=0,这样依次理解就行了
同一维差分的简化,简化掉一个数组
关于insert(i,j,i,j,A[i][j]):
假定A、B数组均为0,此时B为A的差分数组。
而A实际上是0向其中插入所有A[i][j]得到的。
并且:A数组中插入A[i][j]<=>B数组做insert(A[i][j])变换
当零数组A插入所有A[i][j]得到真正的A时,零数组B也做了所有对应的操作得到A的差分数组。
每次看到鹿鹿的题解都感觉很详细,并且看完觉得很通透!希望鹿鹿把所有题解更完! 太爱鹿鹿了!
这个题为啥要求前缀和阿
有没有其他方法构建差分数组啊,这个不理解呢
a数组是b数组的前缀和,我们可以通过b数组得到a数组,插入实际上改变的是虚拟的a数组的值,也就是b数组的值,当虚拟的a数组建好,作为工具的b数组自然也完善好了。
!!你这么一说我就明白了!!一直以为那一步是在构建b[],想半天不知道什么道理,看完你说的才明白是通过构建一个虚拟a数组的过程中,顺便构建出了b[]!!感谢!!
xd,经过insert后,b数组和a数组不是一样了吗
底下的insert理解吗?理解的话这里的应该也理解啊。插入c要用insert,那么把a看作全零以后,依次插入a[i][j]复原的过程不也是相当于一次次insert
a数组是b数组的前缀和,而b数组是a数组的差分,对b数组中insert的操作真正的目的是通过改变b数组,而改变对应的a数组。
如果还是不大明白,你可以这样理解:
b数组一开始是0,所以b数组它所对应的前缀和c数组也是0(避免和题中的a数组混淆,我这里用c数组表示,c数组也就是上面ldlike_0同学说的虚拟的a数组)。
insert的操作过程是根据已有的a数组,将c数组复刻出一个一样的a数组。为了将c数组变成和a数组一样的数组,b数组也需要一直变化,因为只有b数组变了,它的前缀和c数组才能变。
最终由于b数组一直是c数组的差分数组,且c数组成为了和a数组一样,所以b数组也就是a数组的差分数组。这样不就构建出了a数组的差分数组了么~
你可以再想想~(我一开始也是跟你一样误解总想不明白,后来看看别人的解释,自己画画图才理解的)
我还是有点懵,所以b数组是原先的a数组加上每一个格子上的值还是什么
你的理解听起来是有点偏差的,a数组是b数组前缀和,怎么b数组会是原先a数组加上格子的值呢?
b是a的差分数组,a是b的前缀和
牛,突然就通透了
偶遇群主,题解真详细,感谢
为什么是这样构建差分数组呀for (int i = 1; i <= n; i)
{
for (int j = 1; j <= m; j)
{
insert(i, j, i, j, a[i][j]); //构建差分数组
}
}
是把a的数传到b里面去吗
兄弟搞懂了吗,我这里也不太会
这样保证求和为就是原数组
我觉得二维差分前缀和那一部分的两种写法可以根据一维差分来进行推测:就比如一维差分的一种写法:
a[i]=a[i-1]+b[i];
就可以推测出二维差分的a[i][j]=a[i-1][j]+a[i][j-1]-a[i-1][j-1]+b[i][j];
b[i]+=b[i-1]则可以推测出b[i][j]+=b[i-1][j]+b[i][j-1]-b[i-1][j-1];
这样二维差分求前缀和的那一部分就会好理解一些
完整的过程可以总结为以下三个步骤:
### 1. 初始化差分数组
b
:- 初始时,差分数组
b
是一个全为 0 的数组。接下来,我们要通过遍历矩阵a
,把a
中的值体现在差分数组b
上。### 2. 用矩阵
a
初始化差分数组b
:- 我们通过以下操作将矩阵
a
中的每个元素值转化到差分数组b
中:cpp for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { insert(i, j, i, j, a[i][j]); // 把每个 a[i][j] 的值插入到差分数组 b 中 } }
通过这种方式,差分数组
b
就记录了原始矩阵a
的值。### 3. 针对每个子矩阵插入
c
:- 根据输入的操作信息(
x1, y1, x2, y2, c
),在差分数组b
中通过边界插入操作对指定的子矩阵进行增量修改。每次插入都在子矩阵的四个角进行增减操作,确保子矩阵区域的元素值增加c
,而其它部分不受影响。例如,通过如下操作更新差分数组:
cpp insert(x1, y1, x2, y2, c);
### 4. 通过二维前缀和恢复矩阵的最终状态:
- 在差分数组
b
中完成了所有的修改后,接下来我们需要通过二维前缀和来将差分信息累加到矩阵的每个位置,恢复出最终矩阵。前缀和的计算公式是:cpp b[i][j] += b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1];
通过这个公式,矩阵
b
中的每个位置会累积左、上和左上角的差分值,最终得到所有操作后的完整矩阵。### 5. 输出最终结果:
- 计算完二维前缀和后,差分数组
b
就包含了经过所有操作后的矩阵值,最后我们输出矩阵b
的结果。### 总结:
- 初始化差分数组
b
:差分数组初始为全 0。- 用
a
填充b
:将原始矩阵a
的值转化为差分数组b
。- 插入操作
c
:根据子矩阵边界在差分数组b
中进行插入操作,调整子矩阵区域的值。- 计算二维前缀和:通过前缀和公式累加差分,恢复最终矩阵的值。
- 输出结果:输出操作完成后的矩阵。
这样,经过上述过程,我们便可以高效地处理多个子矩阵区域的操作,并最终得到正确的结果。
太厉害了
#include[HTML_REMOVED]
#include[HTML_REMOVED]
#include[HTML_REMOVED]
#include[HTML_REMOVED]
using namespace std;
const int N=1010;
int n,m,q,c;
int s[N][N];
int main()
{
cin>>n>>m>>q;
for(int i=1;i<=n;i)
{
for(int j=1;j<=m;j)
{
scanf(“%d”,&s[i][j]);
}
}
while(q–)
{
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2>>c;
for(int i=x1;i<=x2;i)
{
for(int j=y1;j<=y2;j)
s[i][j]+=c;
}
}
baoli
神
#这个为什么是不对的?#
很奇怪的是5x5的测试矩阵它中间3x3的部分输出是正确的
我个人认为应该在矩阵+1和-1那里加上个空集的概念,这样方便理解,即[x2][y1]是我们这个矩阵数组还需要的部分,则它的下一个不需要的部分是[x2+1][y1]所以它为空集
AcWing《寒假每日一题2024》拼团优惠!https://www.acwing.com/activity/content/introduction/3684/group_buy/184900/