线段树的进阶
在前一篇博客中线段树(Segment Tree)使用记录,介绍了基础的线段树形式。
这一篇讨论以下进阶的使用,主要针对lazy标志
。
上一篇博客中对于一个区间的修改只是单点修改,但是有时候会碰到区间修改的情况,这时基本的线段树可能就不适用了。
这时就需要针对区间修改来对区间树的更新方式进行变化。
下面先假设问题为,初始给定一个数组:
- 修改操作包括:区间加上一个数,或者区间减去一个数。
- 查询操作包括:区间的求和。
建树
上一篇博客中使用的是至底向上的建树方式,这样的建树方法可以最大化空间利用率(2n空间即可),但是这样会造成一些处理上的困难,如图:
图中点1、2其实在整颗树的最右边,而3、4、5、6却在左边,这样的节点排列方式对于区间修改是不利的(会造成逻辑上的混乱)。
所以可以使用至顶向下的建树方式,也就是递归建树:
1 | private void buildTree(int l, int r, int node, int[] arr) { |
将区间的左边放到左子树,右边放到右子树来进行递归建树,注意在建立完毕左右子树之后更新本节点信息(pushUp(node)
)。
可以看到上面就是至顶向下所建立的树的结构,节点序号从左向右排列,但是这样带来的问题就是增加了空间占用(上图中数组大小14)。
1 | public SegmentTreeLazySum(int n, int[] arr) { |
可以使用上面的方式来确定所需数组大小,也就是假如最后一层需要能放下n个元素,最小的i使得$2^i > n$。
lazy标记
在进行区间修改时,我们不可能像单点修改一样,将所有节点的值都修改,因为在查询时,可能只需要上层节点的信息就可以完成查询。
例如将整个数组所有点都增加1,然后询问整个数组的求和,这时我们只需要在根节点上之前所记录的求和加上整个数组的长度即可。
这样的思想就是为了降低算法复杂度,对于一个区间的修改,我们先欠着,当必要的时候才进行修改。
update
方法的代码如下:
1 | // [L, R]为原始更新区间,[x, y]为当前节点node所包含的区间 |
这里lazy标记就代表需要加上的数(使用正负来代表原始的加减)。
可以看到:
- 当发现整个子区间都包含在更新区间中时,就可以停止更新下传,更新节点值与lazy标记即可。
- 当子区间部分包含在更新区间中时,就需要下传更新,那么此时就需要先将之前的lazy标记给下传了。
- 完成左右儿子的更新后,记得更新本节点(
pushUp(node)
)。
那么这里就涉及到了lazyDown
函数,这个函数根据不同的情况会有很大的变化,这里因为只涉及加减法,所以比较简单:
1 | private void lazyDown(int node, int len) { |
因为只是加减法,所以子节点的lazy标记单纯加上父节点的lazy标记即可,当然同时也要记得更新子节点的值。
查询操作
查询操作就递归向下即可,当然记得需要下传lazy标记:
1
2
3
4
5
6
7
8
9
10
11
12
13
public int query(int L, int R, int x, int y, int node) {
if ( y < L || x > R ) {
return 0;
}
if (L <= x && y <= R) {
return table[node];
}
lazyDown(node, y - x + 1);
int res = 0, mid = (x + y) >> 1;
res += query(L, R, x, mid, left(node));
res += query(L, R,mid+1, y, right(node));
return res;
}
1 | public int query(int L, int R, int x, int y, int node) { |
完整代码
注意这里所针对的问题,初始给定一个数组:
- 修改操作包括:区间加上一个数,或者区间减去一个数。
- 查询操作包括:区间的求和。
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
public class SegmentTreeLazySum {
private int[] table;
private int[] lazy;
private int n;
public SegmentTreeLazySum(int n, int[] arr) {
this.n = n;
int i = 1;
while (i < n) {
i = i << 1;
}
table = new int[2*i];
lazy = new int[2*i];
buildTree(0, n-1, 1, arr);
}
private void buildTree(int l, int r, int node, int[] arr) {
if(l == r) {
table[node] = arr[l];
return;
}
int mid = (l + r) >> 1;
buildTree(l, mid, left(node), arr);
buildTree(mid+1, r, right(node), arr);
pushUp(node);
}
private void pushUp(int node) {
int l = left(node), r = right(node);
table[node] = table[l] + table[r];
}
private void lazyDown(int node, int len) {
if (lazy[node] == 0) {
return;
}
int l = left(node), r = right(node);
lazy[l] += lazy[node];
table[l] += lazy[node] * (len - (len >> 1));
lazy[r] += lazy[node];
table[r] += lazy[node] * (len >> 1);
lazy[node] = 0;
}
public void update(int t, int L, int R, int c, int x, int y, int node) {
if ( y < L || x > R ) {
return;
}
if (L <= x && y <= R) {
c = t == 0 ? c : -c;
table[node] += (y - x + 1) * c;
lazy[node] += c;
return;
}
lazyDown(node, y - x + 1);
int mid = (x + y) >> 1;
update(t, L, R, c, x, mid, left(node));
update(t, L, R, c, mid+1, y, right(node));
pushUp(node);
}
public int query(int L, int R, int x, int y, int node) {
if ( y < L || x > R ) {
return 0;
}
if (L <= x && y <= R) {
return table[node];
}
lazyDown(node, y - x + 1);
int res = 0, mid = (x + y) >> 1;
res += query(L, R, x, mid, left(node));
res += query(L, R,mid+1, y, right(node));
return res;
}
private int left(int idx) {
return idx << 1;
}
private int right(int idx) {
return (idx << 1) + 1;
}
// 测试用例:
// 输入:
// 1
// 5 5
// 1 1 1 1 1
// 2 2 4 7
// 1 1 3 4
// 0 0 4 2
// 1 1 4 8
// 2 2 4 3
//
// 输出:
// 3
// -23
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
for (int k = 0; k < T; k++) {
int n, m, t, x, y, c;
n = sc.nextInt();
m = sc.nextInt();
int[] arr = new int[n];
for (int i = 0; i < n; i++) {
arr[i] = sc.nextInt();
}
SegmentTreeLazySum hdu = new SegmentTreeLazySum(n, arr);
for (int i = 0; i < m; i++) {
t = sc.nextInt();
x = sc.nextInt();
y = sc.nextInt();
c = sc.nextInt();
if (t == 2) {
System.out.println(hdu.query(x, y, 0, n - 1, 1));
} else {
hdu.update(t, x, y, c,0, n - 1, 1);
}
}
}
}
}
1 | public class SegmentTreeLazySum { |
HDU3397
这个题目是一个比较典型的线段树题,初始给定一个数组,操作包括:
- 0:将区间[x, y]全部置为0;
- 1:将区间[x, y]全部置为1;
2:将区间[x, y]中的1变为0,0变为1;
3:查询区间[x, y]中1的数量;
- 4:查询区间[x, y]中连续出现1的最多的次数。
这道题的难点一在于查询4
,因为在一个节点上,我们需要通过它的两个子节点的信息来得到连续1的数量。
对于这个问题,可以考虑这样来解决,在一个节点上,我们保存如下信息:
- 贴着区间左边的连续1的数量
LLen
。 - 贴着区间右边的连续1的数量
RLen
。 - 区间中的最大连续1的数量
MLen
。
那么对于一个节点,它的相关信息可以这样计算得到:
LLen
:等于左儿子的LLen
。但是需要注意,如果左儿子的LLen
等于整个区间的长度,那么就为左儿子的LLen
加上右儿子的LLen
。RLen
:同上。MLen
:等于 左儿子的MLen
,右儿子的MLen
,左儿子的RLen
加上右儿子的LLen
的最大值。
这道题的难点二在于操作2
,如何在一个节点上完成信息的更新,主要是LLen
、RLen
、MLen
信息的变化?
为了完成这个件事,这里对称的将连续0的数量保存下来ZLLen
、ZRLen
、ZMLen
,这样在进行操作2时,
就可以将LLen
、RLen
、MLen
和ZLLen
、ZRLen
、ZMLen
的信息交换即可。
整个代码如下:
1 | import java.util.Arrays; |
代码比较基础,速度与使用空间都不怎么样,但是至少AC了:
总结
线段树有很多不同的形式,而且很多时候根据题目的不同会有很多的小变化。
最重要的是代码一般较长,逻辑一般较乱,所有很容易出BUG,建议保持好心态。