什么是线段树
线段树是一种二叉搜索树,借助分治算法思想,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。 -----------------改编自百度百科
线段树的作用
问题:给你 n n n个无序数字, q q q次操作,询问某段区间 [ l , r ] [l, r] [l,r]的和或是修改这段区间的值。假定 1 < = n , q < = 100 , 000 1 <= n, q <= 100,000 1<=n,q<=100,000,给你的时间限制是 1 s 1s 1s。
如果使用暴力?假设每次修改、查询的区间都是 [ 1 , n ] [1, n] [1,n],则最坏时间复杂度需要 O ( q ∗ n ) O(q*n) O(q∗n),显然会超时。
这个时候就需要借助神奇的线段树。
观察它的结构:
建树:
观察其节点下标规律,可得出当前节点p的两个儿子节点下标分别为
p
∗
2
p*2
p∗2和
p
∗
2
+
1
p*2 +1
p∗2+1。
利用递归建树,与归并排序类似。时间复杂度
O
(
n
∗
l
o
g
2
(
n
)
)
O (n*log2(n))
O(n∗log2(n))
void build(int l, int r, int p){
if(l ==r){
tree [p] = a[l]; //叶子节点信息
return ;
}
int mid = (l + r) >> 1; // 相当于mid = (l + r) / 2,
build(l, mid, p << 1); //往左区间
build(mid + i, r, p << 1|1); //往右区间, p << 1 | 1 相当于 p / 2 + 1
tree[p] = tree[p << 1] + tree[p << 1|1]; //将儿子节点信息上传
}
查询区间[L,R]的信息:
假设每次访问到节点
p
p
p存的是区间
[
l
,
r
]
[l, r]
[l,r]的信息,
m
i
d
=
(
l
+
r
)
/
2
mid = (l + r)/ 2
mid=(l+r)/2
如果
[
l
,
r
]
[l, r]
[l,r]刚好被
[
L
,
R
]
[L,R]
[L,R]包含(即
L
≤
l
L≤l
L≤l &&
r
≤
R
)
r ≤ R)
r≤R),则说明
p
p
p节点的信息我们都要。
在此之外的情况,则需要判断
m
i
d
mid
mid与
L
、
R
L、R
L、R的关系:
1.如果
L
≤
m
i
d
L ≤ mid
L≤mid,说明我们需要其左区间的部分信息。
2.如果
R
>
m
i
d
R > mid
R>mid,说明我们需要其右区间的部分信息。·最后返回以上几种情况所有信息合集。
int query(int l, int r, int p, int L, int R){
if (L <= l && r <= R) return tree[p]; //区间包含则返回该节点所有信息
int mid = (l + r) >> 1, ret = 0;// ret用来保存信息
if (L <= mid) ret += query(l, mid, p << 1, L, R);//取左区间的部分信息
if (R > mid) ret += query(mid + 1, r, p << 1|1,L, R);//取右区间的部分信息
return ret;//将以上结果返回
}
修改某个位置k的信息:
很简单,只要找到并修改对应叶子节点的信息即可。
void update(int l, int r, int p, int k, int s) {
if(l == r){
tree[p] = s;
return ;
}
int mid = (l + r) >> 1;
if(k <= mid) update(l, mid, p << 1, k, s); // k <= mid 说明k在左区间
else update(mid + 1, r, p << 1 | 1,k, s); // 反之则在右区间
tree[p] = tree[p << 1]+ tree[p << 1|1]; // 别忘了要更新 p节点的信息哦~
}
时间复杂度为 O ( l o g 2 ( n ) ) O(log2(n)) O(log2(n))
修改区间[L,R]
问题来了,如果区间[L,R]修改信息怎么办?
继续暴力的思想,[L,R]一个个单点修改。
则修改—次的时间复杂度为 O ( n ∗ l o g 2 ( n ) ) O(n*log2(n)) O(n∗log2(n)),修改 q q q次的话…
时间复杂度高达 O ( q ∗ n ∗ l o g 2 ( n ) ) O (q*n*log2(n)) O(q∗n∗log2(n)),反而不如朴素的暴力?
那么是否可以转化成查询的时间复杂度?
即我们只要修改区间查询可以查询到的节点。
比如总区间 [ 1 , 10 ] [1,10] [1,10],要修改 [ 4 , 9 ] [4,9] [4,9]区间,我们其实只需要修改 [ 4 , 5 ] [4,5] [4,5]、 [ 6 , 8 ] [6,8] [6,8]、 [ 9 , 9 ] [9,9] [9,9]所在的节点的信息即可。
那对于这些节点的子节点怎么办?
对于修改到的区间,打上lazy标记,如果下次访问到这个区间的子区间,则下放这个标记。
神奇的lazy标记,用以区间修改。时间复杂度优化至 O ( l o g 2 ( n ) ) O(log2(n)) O(log2(n))
void update(int l, int r,int p, int L, int R, int s) {
if(L <= l && r <= R) { //如果当前访问区间被要访问的区间所包含
tree[p] += (r - l + 1) * s; //首先更新当前节点的值
lazy[p] += s; //打上lazy标签
return ;
}
int mid = (l + r) >> 1;
if(lazy[p] != 0){ //如果当前区间有lazy标记,则需要下放
tree[p << 1] += (mid - l + 1) * lazy[p];
tree[p << 1 | 1] += (r - mid) * lazy[p];
//先更新两个儿子节点的值
lazy[p << 1] += lazy[p];
lazy[p << 1 | 1] += lazy[p];
//标记下放到两个儿子节点
lazy[p] = 0; //当前节点标记取消
}
if(L <= mid) update(l, mid, p << 1, L, R, s);
// L <= mid 说明需要更新左区间
if(R > mid)update(mid + 1, r, p << 1 | 1,L,R, s);
// R > mid 说明需要更新右区间
tree[p] = tree[p << 1] + tree[p << 1|1];
// 别忘了要更新 p 节点的信息哦
}
神奇的lazy标记,用以区间修改。时间复杂度优化至 O ( l o g 2 ( n ) ) O (log2(n)) O(log2(n))
int query (int l, int r, int p, int L, int R){
if(L <= l && r <= R) return tree[p];
// 区间包含则返回该节点所有信息
int mid = (l + r) >> 1, ret = 0; // ret用来保存信息
if(lazy [p] != 0) {
//如果当前区间有lazy标记,则需要下放
tree[p << 1] += (mid - l + 1) * lazy[p];
tree[p << 1|1] += (r - mid) * lazy[p];//先更新两个儿子节点的值
lazy[p << 1] += lazy[p];
lazy[p << 1|1] += lazy[p];//标记下放到两个儿子节点
lazy[p] = 0;//当前节点标记取消
}
if(L <= mid) ret += query(l, mid, p << 1, L, R);
//取左区间的部分信息
if(R > mid) ret += query(mid + 1, r, p << 1|1, L,R);
//取右区间的部分信息
return ret;//将以上结果返回
}
总结&注意事项:
建树时间复杂度
O
(
n
∗
l
o
g
2
(
n
)
)
O (n*log2(n))
O(n∗log2(n))
单次的单点/区间的更新/查询时间复杂度
O
(
l
o
g
2
(
n
)
)
O(log2(n))
O(log2(n))
时间复杂度极其优秀,空间需要开
4
∗
n
4* n
4∗n
用于处理一些需要可分治区间信息的区间询问、修改