网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
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
]
[
j
]
[
k
]
−
s
[
i
]
[
j
−
1
]
[
k
]
−
s
[
i
]
[
j
]
[
k
−
1
]
+
s
[
i
−
1
]
[
j
−
1
]
[
k
]
+
s
[
i
−
1
]
[
j
]
[
k
−
1
]
+
s
[
i
]
[
j
−
1
]
[
k
−
1
]
−
s
[
i
−
1
]
[
j
−
1
]
[
k
−
1
]
\color{Red}b[i][j][k] = s[i][j][k]-s[i - 1][j][k] - s[i][j - 1][k] - s[i][j][k - 1] + s[i - 1][j - 1][k] + s[i - 1][j][k - 1] + s[i][j - 1][k - 1] - s[i - 1][j - 1][k - 1]
b[i][j][k]=s[i][j][k]−s[i−1][j][k]−s[i][j−1][k]−s[i][j][k−1]+s[i−1][j−1][k]+s[i−1][j][k−1]+s[i][j−1][k−1]−s[i−1][j−1][k−1]
我们发现当维度为 t 的时候容斥的时间复杂度是
2
t
2^t
2t,而前缀和的总时间复杂度为 **O
(
n
t
2
t
)
O(n ^t2^t)
O(nt2t)**,即随着维度的升高,时间复杂度增大的很快,不过是可以优化到 **O
(
n
t
t
)
O(n^tt)
O(ntt)** 的,但在此不展开讨论,因为在算法竞赛中很少遇到3维以上的前缀和,而对于 `t≤3`时
O
(
n
t
2
t
)
O(n ^t2^t)
O(nt2t) 与
O
(
n
t
t
)
O(n^tt)
O(ntt)差别并不大,有兴趣的可自行查阅资料。
**(
2
)
三
维
区
间
修
改
\color{Purple}(2) 三维区间修改
(2)三维区间修改**
在三维情况下,我们修改的是一个立方体,有8个顶点,故我们只需要修改这8个顶点的 差分数组b[][][]
的值即可。给出坐标点
(
x
1
,
y
1
)
(x1 , ~y1)
(x1, y1) ~
(
x
2
,
y
2
)
(x2 ,~y2)
(x2, y2)定义的区间,如下图所示
三
维
差
分
空
间
图
示
三维差分空间图示
三维差分空间图示
那么对应的 8个 b[][][]
的修改如下:
// 前面
b[x1][y1][z1] += c; // 坐标起点
b[x2 + 1][y1][z1] -= c; // 右下顶点的右边一个点
b[x1][y1][z2 + 1] -= c; // 左上顶点的上面一个点
b[x2 + 1][y1][z2 + 1] += c; // 右上顶点的斜右上方一个点
// 后面
b[x1][y2 + 1][z1] -= c; // 左下顶点的后面一个点
b[x2 + 1][y2 + 1][z1] += c; // 右下顶点的斜右后方一个点
b[x1][y2 + 1][z2 + 1] += c; // 左上顶点的斜后上方一个点
b[x2 + 1][y2 + 1][z2 + 1] -= c; // 右上顶点的斜右上后方一个点,即区间终点的后一个点
可以发现坐标偏移加 1 都与终点相关,为了方便记忆,我们可以把它按照二进制的排列来写,若二进制中出现偶数个 正1
为正,奇数个正1
为负,与前缀和恰好相反(前缀和是偶数个 负1
为负,奇数个 负1
为正):
{0, 0, 0, 1} // x1, y1, z1
{0, 0, 1, -1}// x1, y1, z2 + 1
{0, 1, 0, -1}// x1, y2 + 1, z1
{0, 1, 1, 1}// x1, y2 + 1, z2 + 1
{1, 0, 0, -1}// x2 + 1, y1, z1
{1, 0, 1, 1}// x2 + 1, y1, z2 + 1
{1, 1, 0, 1}// x2 + 1, y2 + 1, z1
{1, 1, 1, -1}// x2 + 1, y2 + 1, z2 + 1
1.2 例题分析
【例题1】三体攻击(蓝桥杯2018年省赛 A组)
【题目描述】:
三体人将对地球发起攻击。为了抵御攻击,地球人派出了`n = A × B × C`艘战舰,在太空中排成一个 A 层 B 行 C 列的立方体。其中,第 i 层第 j 行第 k 列的战舰(记为战舰`(i, j, k)`)的生命值为 `s(i, j, k)`。
三体人将会对地球发起 m 轮“立方体攻击”,每次攻击会对一个小立方体中的所有战舰都造成相同的伤害。具体地,第 t 轮攻击用 7 个参数 x1, x2, y1, y2, z1, z2, d
描述; 所有满足i∈[x1, x2], j∈[y1, y2], k∈[z1, z2]
的战舰 (i, j, k)
会受到 d
的伤害。如果一个战舰累计受到的总伤害超过其防御力,那么这个战舰会爆炸。
地球指挥官希望你能告诉他,第一艘爆炸的战舰是在哪一轮攻击后爆炸的。
【**输入**】 :
第一行包括 4 个正整数 A, B, C, m
;
第二行包含`A × B × C`个整数,其中第 `((i − 1)×B + (j − 1)) × C + (k − 1)+1`个数为 `s(i, j, k)`;
第 3 到第 m + 2 行中,第 (t − 2) 行包含 7 个正整数 x1, x2, y1, y2, z1, z2, d
。
A
×
B
×
C
≤
1
0
6
,
m
≤
1
0
6
,
0
≤
s
(
i
,
j
,
k
)
,
d
≤
1
0
9
A × B × C ≤ 10^6, m ≤ 10^6, 0 ≤ s(i, j, k), d ≤ 10^9
A×B×C ≤ 106, m ≤ 106, 0 ≤ s(i, j, k), d ≤ 109。
【输出】:
输出第一个爆炸的战舰是在哪一轮攻击后爆炸的。保证一定存在这样的战舰。
分析:如果对于每次攻击我们都枚举每个点生命值,对于
n
=
1
0
6
n=10^6
n=106 个点,
m
=
1
0
6
m = 10 ^6
m=106次攻击来说,时间复杂度为 O(nm),肯定会TLE。
故此时我们需要在差分数组查询空间内每个元素是否小于0,但是计算空间上的每个元素的值是 O(n) 的,对于m次攻击,复杂度仍然是 O(nm)。可见单纯使用差分数组是不行的。为此我们需要利用二分法:题目保证一定存在这样一个战舰在某一轮攻击后会爆炸,即从 第 1 次到 m 次修改,必定有一次是临界值。在临界值以前,战舰的生命肯定是大于 0 的,在临界值后,若战舰出现了负值,则之后一直都会是负值。那么对 m 进行二分,就能在 O(logm) 次内找到这个临界值,这就是答案。总复杂度为 O(nlogm)。
细节处理:如果对于数组空间我们每一维度都开到
1
0
6
10^6
106,必定会MLE。为了避免这一问题,我们将三维数组拍平为一维数组,即采用压维的方式,对于 A × B × C =
1
0
6
![img](https://img-blog.csdnimg.cn/img_convert/3a04eb126e33a426268207c68784a2ac.png)
![img](https://img-blog.csdnimg.cn/img_convert/78a3875ffc610841c0b58125694c6131.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
1
0
6
,
m
≤
1
0
6
,
0
≤
s
(
i
,
j
,
k
)
,
d
≤
1
0
9
A × B × C ≤ 10^6, m ≤ 10^6, 0 ≤ s(i, j, k), d ≤ 10^9
A×B×C ≤ 106, m ≤ 106, 0 ≤ s(i, j, k), d ≤ 109。
【**输出**】:
输出第一个爆炸的战舰是在哪一轮攻击后爆炸的。保证一定存在这样的战舰。
分析:如果对于每次攻击我们都枚举每个点生命值,对于
n
=
1
0
6
n=10^6
n=106 个点,
m
=
1
0
6
m = 10 ^6
m=106次攻击来说,时间复杂度为 O(nm),肯定会TLE。
故此时我们需要在**差分数组**查询空间内每个元素是否小于0,但是计算空间上的每个元素的值是 O(n) 的,对于m次攻击,复杂度仍然是 O(nm)。可见单纯使用差分数组是不行的。为此我们需要利用**二分法**:题目保证一定存在这样一个战舰在某一轮攻击后会爆炸,即从 第 1 次到 m 次修改,必定有一次是临界值。在临界值以前,战舰的生命肯定是大于 0 的,在临界值后,若战舰出现了负值,则之后一直都会是负值。那么对 m 进行二分,就能在 O(logm) 次内找到这个临界值,这就是答案。总复杂度为 O(nlogm)。
~~~
细节处理:如果对于数组空间我们每一维度都开到
1
0
6
10^6
106,必定会MLE。为了避免这一问题,我们将三维数组拍平为一维数组,即采用压维的方式,对于 A × B × C =
1
0
6
[外链图片转存中...(img-0vDNGaAQ-1715427104412)]
[外链图片转存中...(img-aNpRdUu6-1715427104412)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**