数据结构与算法—-线段树
什么是线段树:
一种支持范围整体修改和范围整体查询的数据结构
解决问题的范畴:
大范围信息可以只由左、右两侧信息加工出,而不必遍历左右两个子范围的具体状况
线段树实例:
实例一:SegmentTree
题目描述:
给定一个数组arr,用户希望你实现如下三个方法
1)void add(int L, int R, int V) : 让数组arr[L…R]上每个数都加上V
2)void update(int L, int R, int V) : 让数组arr[L…R]上每个数都变成V
3)int sum(int L, int R) :让返回arr[L…R]这个范围整体的累加和
怎么让这三个方法,时间复杂度都是O(logN)
思路:
将原始数组
origin
第0
位弃用(为了使用位运算),从1
开始,放入数组arr
中1
2
3
4
5int MAXN = origin.length + 1;
arr = new int[MAXN]; // arr[0] 不用 从1开始使用
for (int i = 1; i < MAXN; i++) {
arr[i] = origin[i - 1];
}计算累加和。将数组
arr
完全二分,每一段区间的和放入数组sum
中,sum
为满二叉树结构,长度为4*MAXN
(arr
长度满足2^n
时,满二叉树长度为2*MAXN
,不满足时需要补满)例如:
arr[x, 3, 2, 5, 7],sum[x, 17, 5, 12, 3, 2, 5, 7]
arr(1-4)
的和放入sum[1],
arr(1-2)
的和放入sum[2]
,arr(3-4)
的和放入sum[3]
,arr(1-1)
的和放入sum[4]
,arr(2-2)
的和放入sum[5]
,arr(3-3)
的和放入sum[6]
,arr(4-4)
的和放入sum[7]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23int[] sum = new int[MAXN << 2];
// 递归填充某一区间的累加和信息
// 在arr[l~r]范围上,去build,1~N,(l: 1, r: origin.length)
// rt : 这个范围在sum中的下标(root)
public void build(int l, int r, int rt) {
if (l == r) {
sum[rt] = arr[l];
return;
}
int mid = (l + r) >> 1;
build(l, mid, rt << 1); // 左孩子 2*rt
build(mid + 1, r, rt << 1 | 1); // 右孩子 2*rt + 1
pushUp(rt);
}
/**
* 当前节点值=左孩子值+右孩子值
* @param rt
*/
private void pushUp(int rt) {
sum[rt] = sum[rt << 1] + sum[rt << 1 | 1];
}懒增加。实现在
l~r
范围内所有的元素都加C
,引入int[] lazy
数组, 标记每个节点需要下发的增加数量,当l~r
范围覆盖了当前表达的范围时直接在当前层累加,当覆盖不了时,往下级下发攒下的lazy add任务,根据任务范围决定左右孩子是否都需要下发,下发完毕后汇总父节点sum
值。下发时将左右子树的lazy
值累加上父节点值(lazy[rt << 1] += lazy[rt];
),sum
值累加上子节点个数当前lazy值(`sum[rt << 1] += lazy[rt] ln;),并将父节点
lazy值置为
0`。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40// L..R -> 任务范围 ,所有的值累加上C
// l,r -> 表达的范围(l: 1, r: origin.length)
// rt 去哪找l,r范围上的信息(root)
public void add(int L, int R, int C,
int l, int r,
int rt) {
// 任务的范围彻底覆盖了,当前表达的范围
if (L <= l && r <= R) {
sum[rt] += C * (r - l + 1);
lazy[rt] += C;
return;
}
// 要把任务往下发
// 任务 L, R 没有把本身表达范围 l,r 彻底包住
int mid = (l + r) >> 1;
// 下发之前所有的懒任务
pushDown(rt, mid - l + 1, r - mid);
// 左孩子是否需要接到任务
if (L <= mid) {
add(L, R, C, l, mid, rt << 1); // 递归增加左孩子
}
// 右孩子是否需要接到任务
if (R > mid) {
add(L, R, C, mid + 1, r, rt << 1 | 1); // 递归增加右孩子
}
// 左右孩子做完任务后,我更新我的sum信息
pushUp(rt); // 见2.
}
// 之前的所有懒增加,从父范围发给左右两个子范围
// ln表示左子树元素结点个数,rn表示右子树结点个数
private void pushDown(int rt, int ln, int rn) {
if (lazy[rt] != 0) {
lazy[rt << 1] += lazy[rt];
sum[rt << 1] += lazy[rt] * ln;
lazy[rt << 1 | 1] += lazy[rt];
sum[rt << 1 | 1] += lazy[rt] * rn;
lazy[rt] = 0;
}
}懒更新。实现在
l~r
范围内所有的元素更新为C
,引入int[] change
和boolean[] update
数组,配合使用标记是否需要更新及更新的值。当l~r
范围覆盖了当前表达的范围时直接在当前层更新,当覆盖不了时,往下级下发攒下的lazy update任务,根据任务范围决定左右孩子是否都需要下发,下发完毕后汇总父节点sum
值。任务下发时,将左右子树的update值置为父节点值(update[rt << 1] = true;change[rt << 1] = change[rt];
),sum
值置为子节点个数当前update
值(`sum[rt << 1] = change[rt] ln;),并将左右子节点
lazy值置为0,父节点
update置为
false`。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38// L..R -> 任务范围 ,所有的值累加上C
// l,r -> 表达的范围(l: 1, r: origin.length)
// rt 去哪找l,r范围上的信息(root)
public void update(int L, int R, int C, int l, int r, int rt) {
if (L <= l && r <= R) {
update[rt] = true;
change[rt] = C;
sum[rt] = C * (r - l + 1);
lazy[rt] = 0;
return;
}
// 当前任务躲不掉,无法懒更新,要往下发
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid);
if (L <= mid) {
update(L, R, C, l, mid, rt << 1);
}
if (R > mid) {
update(L, R, C, mid + 1, r, rt << 1 | 1);
}
pushUp(rt);
}
// 之前的所有懒更新,从父范围发给左右两个子范围
// ln表示左子树元素结点个数,rn表示右子树结点个数
private void pushDown(int rt, int ln, int rn) {
if (update[rt]) {
update[rt << 1] = true;
update[rt << 1 | 1] = true;
change[rt << 1] = change[rt];
change[rt << 1 | 1] = change[rt];
lazy[rt << 1] = 0;
lazy[rt << 1 | 1] = 0;
sum[rt << 1] = change[rt] * ln;
sum[rt << 1 | 1] = change[rt] * rn;
update[rt] = false;
}
}任务下发时先执行懒更新任务,再执行懒增加。因为懒更新任务直接将节点值修改为更新值,如果后执行懒更新,更新操作后可能存在积攒的增加任务,无法正常执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 之前的所有懒增加和懒更新,从父范围发给左右两个子范围
// ln表示左子树元素结点个数,rn表示右子树结点个数
private void pushDown(int rt, int ln, int rn) {
if (update[rt]) {
update[rt << 1] = true;
update[rt << 1 | 1] = true;
change[rt << 1] = change[rt];
change[rt << 1 | 1] = change[rt];
lazy[rt << 1] = 0;
lazy[rt << 1 | 1] = 0;
sum[rt << 1] = change[rt] * ln;
sum[rt << 1 | 1] = change[rt] * rn;
update[rt] = false;
}
if (lazy[rt] != 0) {
lazy[rt << 1] += lazy[rt];
sum[rt << 1] += lazy[rt] * ln;
lazy[rt << 1 | 1] += lazy[rt];
sum[rt << 1 | 1] += lazy[rt] * rn;
lazy[rt] = 0;
}
}懒查询。返回在
l~r
范围内所有元素的累加和。当l~r
范围覆盖了当前表达的范围时直接返回sum
,当覆盖不了时,往下级下发攒下的lazy add及lazy update任务,根据任务范围决定左右孩子是否都需要下发,下发完毕后汇总父节点sum
值。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// l,r -> 表达的范围(l: 1, r: origin.length)
// rt 去哪找l,r范围上的信息(root)
public long query(int L, int R, int l, int r, int rt) {
if (L <= l && r <= R) {
return sum[rt]; // 任务范围覆盖了可表达范围,直接返回当前sum
}
int mid = (l + r) >> 1;
pushDown(rt, mid - l + 1, r - mid); // 执行懒更新及懒增加任务
long ans = 0;
if (L <= mid) {
ans += query(L, R, l, mid, rt << 1);
}
if (R > mid) {
ans += query(L, R, mid + 1, r, rt << 1 | 1);
}
return ans;
}
完整题解:
1 | public class Code01_SegmentTree { |
实例二:Falling Squares
题目描述:
leetcode 699. Falling Squares 想象一下标准的俄罗斯方块游戏,X轴是积木最终下落到底的轴线。
下面是这个游戏的简化版:
1)只会下落正方形积木
2)[a,b] -> 代表一个边长为b的正方形积木,积木左边缘沿着X = a这条线从上方掉落
3)认为整个X轴都可能接住积木,也就是说简化版游戏是没有整体的左右边界的
4)没有整体的左右边界,所以简化版游戏不会消除积木,因为不会有哪一层被填满。
给定一个N*2的二维数组matrix,可以代表N个积木依次掉落,返回每一次掉落之后的最大高度。
思路:
- 将给定的二维数组转化为方块在X轴左右的边界(
[a, b]
从x=a
位置掉落长为b
的方块,转化为[a, a+b-1]
表示方块在X轴的左右边界) - 离散化所有边界,从小到大放入
map
,将所有的边界从1
开始编号 - 后续步骤参考实例一思路,将累加和的计算修改为计算最大高度
完整题解:
1 | import java.util.ArrayList; |
If the images or anything used in the blog infringe your copyright, please contact me to delete them. Thank you!