网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
1.1 基本概念
**问
题
描
述
\color{Turquoise}问题描述
问题描述**:给定一个长度为 n 的一维数组 a[ ],数组内的每个元素都有初值,并对其进行一下操作:
(1)修改操作:进行 m 次区间修改,每次修改对这段区间【L,R】内的所有元素做相同的加减操作。
(2)查询操作:查询某个元素的值是多少
如果简单的暴力遍历,那么对于每次的修改复杂度是 **O(n)** 的,有 m 次修改,此时复杂度为 **O(n\*m)** ,如果用差分法的话,复杂度将为 **O(n + m)**
**原理**
对于这个原数组 a[ ] = {a1,a2,a3,···,an},我们构造出这样一个数组 B[ ] = {b1,b2,b3,···,bn},使得 ai = b1 + b2 + ··· + bi,那么b[ ] 就称为 a[ ] 的差分,a[ ] 称为 b[ ] 的前缀和。可以发现,差分与前缀和是一组逆运算。
如何利用差分数组对区间进行修改呢?为什么利用差分数组能提升修改的效率呢?
**1.
区
间
修
改
\color{Purple}区间修改
区间修改,时间复杂度为 O(1)**
现在要将原数组 a[ ] 区间【L,R】上的每个数都加上一个 `c`,如下图所示:
![在这里插入图片描述](https://img-blog.csdnimg.cn/18d5f76d2894433a88b3c43e5b670683.png#pic_center)
* 第一个受到影响的差分数组中的元素为 bL],所以 `b[L] += c` ,对于 a[L] 后面的数都会受到 B[L] 的影响加上 c。
* 最后一个受影响的差分数组中的元素b[R],为了保证不影响到 R 后面的元素,所以我们需要 `b[R + 1] -= c`。
也就是说,对于 a[x] = b[1] + b[2] + ···+ b[x],利用差分数组能够精确地实现只修改指定区间内元素的目的,而不会修改区间外的a[ ] 的值,也就是:
* (1) 1 ≤ x < L, 前缀和 a[x] 不变。
* (2) L ≤ x ≤ R, 前缀和 a[x] 加上了 c 。
* (3) R < x ≤ N, 前缀和 a[x] 不变,因为被 b[R + 1] 中的c抵消了。
这样一来,就不必每次都对区间内所有的数进行处理,只需要修改区间【L,R】的两个端点 b[ ] 的值即可,复杂度是 **O(1)** 的。
```
void add(int l, int r, int c)
{
b[l] += c;
b[r + 1] -= c;
}
```
2. **单
点
查
询
\color{Purple}单点查询
单点查询,时间复杂度为 O(n)**
根据定义,差分数组 b[ x ]
的前缀和 b[1] + b[ 2 ] + ··· + b[ x ]
就是原数组 a[x]
的值
void get(int x)
{
for(int i = 1; i <= n; i ++ )
sum[i] = sum[i - 1] + b[i];
cout << sum[x];
}
初始化:我们其实不需要过分关注差分数组 b[]
是如何构造出来的,只需要知道差分与前缀和是互为逆运算即可。实际上,我们压根就不需要去构造差分数组 b[]
。
一开始,我们就可以把原数组 `a[]` 看成全是 `0`,即 `a[ ] = {0,0,0,···0}` ,此时相应的差分数组 `b[]` 也全是 `0`,即 `b[ ] = {0,0,0,···0}`,那么对于原数组 `a[]` 的初始值:
* a[ 1 ] 相当于区间 【1,1】的每个数都加上 a[ 1 ]
* a[ 2 ] 相当于区间 【2,2】的每个数都加上 a[ 2 ]
* …
* a[ n ] 相当于区间 【n,n】的每个数都加上 a[ n ]
这样,只需要用上面的区间修改操作 `add()` 即可完成初始化赋值了。
#### 1.2 例题分析
【例题1】 [区间(interval)](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)
题目描述:小明喜欢数组。他现在有一个 n 个元素的数组 a,而他要对 a[L] ~ a[R]进行M次操作:
操作一:将 a[L] ~ a[R]内的元素都加上 c
操作二:将 a[L] ~ a[R]内的元素都减去 c
最后询问 a[L] ~ a[R] 内的元素之和
**AcCode**
```
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1000010;
LL a[N];
void add(int l, int r, int c)
{
a[l] += c;
a[r + 1] -= c;
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++ ) // 将数组 a[] 看成自己的差分
{
int x;
scanf("%d", &x);
add(i, i, x);
}
for(int i = 1; i <= m; i ++ )// 修改操作
{
int k, l, r, c;
scanf("%d%d%d%d", &k, &l, &r, &c);
if(k == 1) add(l, r, -c);
else add(l, r, c);
}
for(int i = 1; i <= n; i ++ )// 求差分数组 a[]的前缀和(即修改后a[] 的值)
a[i] += a[i - 1];
int l, r;
scanf("%d%d", &l, &r);
LL ans = 0;
for(int i = l; i <= r; i ++ )
ans += a[i];
printf("%lld\n", ans);
return 0;
}
```
**差分的局限性**:我们可以注意到,利用差分数组 b[] 可以将原来
O
(
n
)
O(n)
O(n) 的区间修改,降为
O
(
1
)
O(1)
O(1)的端点修改,从而提高了修改操作的效率。
但是,对于一次的查询操作,我们必须计算前缀和 b[1] + b[2] + ··· + b[x]才能将原数组 a[x] 求出,计算量是
O
(
n
)
O(n)
O(n)的,即一次查询的复杂度是
O
(
n
)
O(n)
O(n)的。也就是说,如果查询操作发生多次,例如 m 次修改,k 次查询,且修改和查询的顺序是随机的,即可能边修改边查询。此时总复杂度为:m 次修改复杂度
O
(
m
)
O(m)
O(m),k次查询复杂度
O
(
k
n
)
O(kn)
O(kn),即
o
(
m
k
n
)
o(m + kn)
o(m+kn)。还不如直接暴力来的快
O
(
m
n
k
)
O(mn + k)
O(mn+k)。
可以看出,尽管差分数组对于 ”区间修改“很高效,但是对于”单点查询“来说略显吃力。此时有专门的数据结构来解决这一类问题:树状数组和线段树,详见本博客的树状数组和线段树专题。
### 2. 二维差分
#### 1.1 基本概念
有了一维差分的认识后,我们容易就能拓展到二维差分。一维是线性的,一段区间【L,R】有两个端点;二维是一个矩阵,一块区间由四个端点所围成。
**问
题
描
述
\color{Turquoise}问题描述
问题描述**: 在 n × n 的格子上有 m 个地毯。给出这些地毯的信息,问每个点被多少地毯覆盖。
输入: 第一行是两个整数n, m。接下来 m 行,每行 2 个坐标(x1, y1) 和 (x2, y2 ),代表一块地毯,左上角是 (x1, y1),右下角是(x2, y2)。
输出:输出n行,每行n个正整数。第i行第j列的正整数表示(i, j)这个格式被多少地毯覆盖。
可以发现,这是前面例题区间(interval)的二维拓展,其修改和查询操作完全一样。
我们知道存储矩阵往往需要很大的空间。如果题目有空间的限制,例如100M = 100 * 1024 *1024 个字节(byte),那么对于矩阵每个元素是 4 个字节的 int型 来说,可以计算出最大的 maxn = 5120。不过,也可以像前面例题一样,不定义差分矩阵 b[][]
,直接将原矩阵a[][]
看成自己的差分矩阵,这样一来就能剩下一半的空间了。
同前面一样,我们先考虑能不能直接暴力求解。可以看出,每次矩阵修改的复杂度是
O
(
n
2
)
O(n^2)
O(n2),共 m 次,总复杂度为
O
(
m
+
n
2
)
O(m+n^2)
O(m+n2),肯定会 TLE。
**(
1
)
二
维
差
分
的
定
义
\color{Purple}(1)二维差分的定义
(1)二维差分的定义**
在一维差分中,原数组a[ ]是从第1个b[1]开始的差分数组 b[ ]的前缀和:a[x]= b[1] + b[2] + ··· + b[x]。
在二维差分中,a[ ][ ]是差分数组b[ ][ ]的前缀和,即将原点坐标`(1,1)`和坐标`(i,j)`围成的矩阵中,所有的b[ ][ ]相加等于a[ i ][ j ]。我们可以把每个`b[][]`看成一个小格;在坐标`(1,1)`和`(i,j)`所围成的范围内,所有小格子加起来的总面积,等于 `a[i][j]`。如下图中,每个格子的面积是一个 b[ ][ ],例如阴影格子是b[ i ][ j ],它由4个坐标点组成:
(
i
,
j
)
\color{CadetBlue}(i, j)
(i,j)、
(
i
−
1
,
j
)
\color{CadetBlue}(i - 1, j)
(i−1,j)、
(
i
,
j
−
1
)
\color{CadetBlue}(i, j - 1)
(i,j−1)、
(
i
−
1
,
j
−
1
)
\color{CadetBlue}(i - 1, j - 1)
(i−1,j−1)。坐标点`(i, j)`的值是 a[ i ][ j ],它等于坐标`(1,1)`和`(i,j)`所围成的所有格子的总面积 。
![在这里插入图片描述](https://img-blog.csdnimg.cn/a5ec5e5af0e24da89c541fe6f77b2d0e.png#pic_center)
把
每
个
a
[
]
[
]
看
成
总
面
积
,
把
每
个
b
[
]
[
]
看
成
小
格
子
的
面
积
把每个a[][] 看成总面积,把每个b[][]看成小格子的面积
把每个a[][]看成总面积,把每个b[][]看成小格子的面积
由上图我们可以得到二维差分的定义:在二维情况下,差分就变成了相邻a[][]
的"面积差’’,计算公式是:
b
[
i
]
[
j
]
=
a
[
i
]
[
j
]
−
a
[
i
−
1
]
[
j
]
−
a
[
i
]
[
j
−
1
]
a
[
i
−
1
]
[
j
−
1
]
\color{Red}b[i][j] = a[i][j] - a[i - 1][j] - a[i][j - 1] + a[i - 1][j - 1]
b[i][j]=a[i][j]−a[i−1][j]−a[i][j−1]+a[i−1][j−1]
即利用上图红色大面积
a
[
i
]
[
j
]
\color{Maroon}a[i][j]
a[i][j]减去两个小面积
a
[
i
−
1
]
[
j
]
\color{Turquoise}a[i- 1][j]
a[i−1][j]、
a
[
i
]
[
j
]
\color{Green}a[i][j]
a[i][j],由于两个小面积公共的部分`a[i-1][j -1]`被减去了 2 次,故要加回来 1 次
a
[
i
−
1
]
[
j
−
1
]
\color{Yellow}a[i - 1][j - 1]
a[i−1][j−1]。
**(
2
)
二
维
区
间
修
改
\color{Purple}(2) 二维区间修改
(2)二维区间修改**
对于一维区间修改的操作,我们只需要修改区间的两个端点的b[]
值。那么相应地,在二维情况下,一块区间是一个矩阵,由4个端点,只需要修改这 4个 b[][]
值即可。如下图所示,
当我们对坐标点 (x1, y1) ~ (x2, y2)
所围成的区间进行修改时,对应的4个端点的操作应为:
b[x1][y1] += c; // 二维区间的起点
b[x1][y2 + 1] -= c; // 把 x看成常数,y从 y1 到 y2
b[x2 + 1][y1] -= c;// 把 y看成常熟,x从 x1 到 x2
b[x2 + 1][y2 + 1] += c;// 由于前面两式把 c 减去了 2 次,故要加回 1 次
1.2 例题分析
【例题1】Monitor
题意:Xiaoteng 有一个 n×m 的矩形庄稼地,为了抓到小偷,安装了 p 个监控,每个监控都有一个矩形的监视范围,左上角为 (x1,y1),右下角为 (x2,y2)。小偷们会来偷 q 次,每次小偷们的作案地点都是一个矩形区域,左上角为 (x1,y1),右下角为 (x2,y2)。问每次小偷们作案时,能否看到全部的小偷。
思路:将每个监控的矩形监视区域里的每个数都加上 1,都操作在差分数组上。求差分数组的前缀和得到原数组,如果原数组中的值大于 1,说明该点被多个监控覆盖,我们只需要记 1 个即可。对于小偷们每次作案的矩形区域,看监控区域是否全部覆盖(是否全是1),如果全部覆盖(作案矩形同监控矩形的值相等)则输出 YES,否则,输出NO。
AcCode
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int main()
{
int n, m;
while(~scanf("%d%d", &n, &m))
{
vector<vector<int>> a(n + 10, vector<int>( m + 10, 0));
int k;
scanf("%d", &k);
while(k -- )
{
int x1, y1, x2, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
a[x1][y1] += 1;
a[x2 + 1][y1] -= 1;
a[x1][y2 + 1] -= 1;
a[x2 + 1][y2 + 1] += 1;
}
// 求差分数组的前缀和,得到原数组的值
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];
// 如果被该区域被监控覆盖多次,则只记一次
for(int i = 1; i <= n; i ++ )
for(int j = 1; j <= m; j ++ )
if(a[i][j] > 1) a[i][j] = 1;
// 对于小偷们每次作案的矩形区域,看监控区域是否全部覆盖(是否全是1)
int p;
scanf("%d", &p);
while(p -- )
{
int x1, y1, x2, y2;
scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
int s1 = (x2 - x1 + 1) \* (y2 - y1 + 1);
int s2 = f[x2][y2] - f[x1 - 1][y2] - f[x2][y1 - 1] + f[x1 - 1][y1 - 1];
if(s1 == s2) puts("YES");
else puts("NO");
}
}
return 0;
}
3.三维差分
1.1 基本概念
三维已是人类空间想象的一个大跨度,其差分难度较为复杂,不过没关系,下面我们将利用空间立体图来逐一理解。
**(
1
)
三
维
差
分
的
定
义
\color{Purple}(1)三维差分的定义
(1)三维差分的定义**
元素值用三维数组 a[][][]
来定义,差分数组b[][][]
也是三维的。与之前低维度的差分类似,把三维差分想象成立体空间的操作。与之对应的小立方块有 8 个顶点,所以三维的区间需要修改 8 个b[][][]
的值。
在二维差分中,`a[][]` 是差分数组 `b[][]`的前缀和,即原点坐标 (1,1)和 坐标(i,j)围成的矩阵面积。
在三维差分中,a[][][]
是差分数组 b[][][]
的前缀和,即原点坐标 (1, 1, 1) 和 坐标(i, j, k)围成的立体体积。同样地,我们把每个b[][][]
看成一个小立方体,在坐标(1, 1, 1)
~ (i , j,k)
所围成的空间中,所有小立体加起来的总体积即为a[i][j][k]
。如下图所示,每个小立方体由 8 个端点定义。坐标点(i,j,k)的值是 a[i][j][k]
; 图中小立方体的体积是差分数组 b[i][j][k]
的值。
类似的,在三维情况下,差分就变成了相邻的`a[][][]`的 ”体积差“。那么如何来写出差分的递推计算公式呢?
观察前面一、二维的前缀和我们可以发现,其前缀和规律十分吻合容斥原理。
![在这里插入图片描述](https://img-blog.csdnimg.cn/a79059b5c1d045958ee2876f347bdd24.png#pic_center)
即对于
维
度
为
t
\color{Red}维度为 t
维度为t 的前缀和,记 **S(t)** 为其前缀和的递推式,则我们有:
S
(
t
)
=
a
[
t
]
+
∑
n
=
1
∞
(
−
1
)
(
n
−
1
)
S
(
[
t
−
1
]
的
组
合
形
式
)
,
n
为
−
1
的
个
数
S(t) = a[t]+ \sum\_{n = 1}^{∞}(-1)^{(n -1)}S( [t- 1]的组合形式),\color{CadetBlue}n~为 -1的个数
S(t)=a[t]+n=1∑∞(−1)(n−1)S([t−1]的组合形式),n 为−1的个数
所以对于三维的差分数组`b[][][]`,其递推式如下:
b
[
i
]
[
j
]
[
k
]
=
s
[
i
]
[
j
]
[
k
]
−
s
[
i
−
1
]
![img](https://img-blog.csdnimg.cn/img_convert/d0108e44cbf813768bb0242c475310d8.png)
![img](https://img-blog.csdnimg.cn/img_convert/c31269373ccf42e274ec8532c6f22afb.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
1dd24e1b14d3db4f9a81a5121566a.png#pic_center)
类似的,在三维情况下,差分就变成了相邻的a[][][]
的 ”体积差“。那么如何来写出差分的递推计算公式呢?
观察前面一、二维的前缀和我们可以发现,其前缀和规律十分吻合容斥原理。
即对于
维
度
为
t
\color{Red}维度为 t
维度为t 的前缀和,记 S(t) 为其前缀和的递推式,则我们有:
S
(
t
)
=
a
[
t
]
∑
n
=
1
∞
(
−
1
)
(
n
−
1
)
S
(
[
t
−
1
]
的
组
合
形
式
)
,
n
为
−
1
的
个
数
S(t) = a[t]+ \sum_{n = 1}{∞}(-1){(n -1)}S( [t- 1]的组合形式),\color{CadetBlue}n~为 -1的个数
S(t)=a[t]+n=1∑∞(−1)(n−1)S([t−1]的组合形式),n 为−1的个数
所以对于三维的差分数组b[][][]
,其递推式如下:
b
[
i
]
[
j
]
[
k
]
=
s
[
i
]
[
j
]
[
k
]
−
s
[
i
−
1
]
[外链图片转存中…(img-gqzySg4K-1715444668179)]
[外链图片转存中…(img-0h1Drw4l-1715444668179)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!