快速搞定算法之线段树问题
线段树主要是用于区间问题,比如求区间和、区间修改、区间最值等。
树状数组其实就是阉割版的线段树,但线段树能做到的事比树状数组要大。
这里讲解两道使用线段树解决的问题:
区间求和问题 :
题目描述
如题,已知一个数列,你需要进行下面两种操作:
将某区间每一个数加上 k。
求出某区间每一个数的和。
输入格式
第一行包含两个整数 n,m,分别表示该数列数字的个数和操作的总个数。
第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 m 行每行包含 3 或 4 个整数,表示一个操作,具体如下:
1 x y k:将区间 [x,y] 内每个数加上 k。
2 x y:输出区间 [x,y] 内每个数的和。
输出格式
输出包含若干行整数,即为所有操作 2 的结果。
输入输出样例
-
输入 #1
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
-
输出
111
8
20
说明/提示
对于 30% 的数据:n≤8,m≤10。
对于 70% 的数据:n≤103,m≤104。
对于 100% 的数据:1≤n,m≤105。
保证任意时刻数列中任意元素的和在 [263,263) 内。
【样例解释】
这道题是模板题,直接使用线段树方法,求区间和,则在线段树结点里面存放包含区间的和。
线段树的基本概念上节课已经讲了,需要建立一颗区间树,数结点里面存放我们需要的信息,然后维护这棵树。
思路先是需要建树,可以选择数组建树,区间划分就采用二分。线段树根节点为1,所以每一个编号为i的结点,其左儿子编号为i2,右儿子编号为i2+1。构建树可以使用递归的方法。当区间只有一个值时,这个结点的值就是这一个值。
建树方法:
void build(int s, int t, int p) {
// 对 [s,t] 区间建立线段树,当前根的编号为 p
if (s == t) {
d[p] = a[s];
return;
}
int m = (s + t) / 2;
build(s, m, p * 2), build(m + 1, t, p * 2 + 1);
// 递归对左右区间建树
d[p] = d[p * 2] + d[(p * 2) + 1];
}
线段树构建完了就是该怎么通过它进行区间查询了。
int getsum(int l, int r, int s, int t, int p) {
// [l,r] 为查询区间,[s,t] 为当前节点包含的区间,p 为当前节点的编号
if (l <= s && t <= r)
return d[p]; // 当前区间为询问区间的子集时直接返回当前区间的和
int m = (s + t) / 2, sum = 0;
if (l <= m) sum += getsum(l, r, s, m, p * 2);
// 如果左儿子代表的区间 [l,m] 与询问区间有交集,则递归查询左儿子
if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
// 如果右儿子代表的区间 [m+1,r] 与询问区间有交集,则递归查询右儿子
return sum;
这里发现,建树和查询都没有看到更新的操作,当某一区间的值都加上一个值以后还需要对区间进行更新。因为当把区间中每一个值都遍历一次、修改一次,那耗费的时间会很多,而且就不能体现线段树的优势了。使用这里使用一个叫做 懒惰标记 的东西。
在进行区间增减时,可以把区间的增减值先放在结点懒惰标记里面,也就是它子节点需要更新的值先放在父节点里。
当没有查到子节点头上时,父节点就一直欠着子节点的值,因为反正还没有找到子节点,没人会关系他是多少,但是当找到子节点时,父节点就必须把欠的值发下去,不然会引起子节点的不满。这就需要传递懒惰标记。
把欠的值还给子节点,就是下传懒惰标记。
区间修改(区间加上某个值):
void update(int l, int r, int c, int s, int t, int p) {
// [l,r] 为修改区间,c 为被修改的元素的变化量,[s,t] 为当前节点包含的区间,p
// 为当前节点的编号
if (l <= s && t <= r) {
d[p] += (t - s + 1) * c, b[p] += c;
} // 当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改
if (b[p] && s != t) {
// 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值
d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
b[p] = 0; // 清空当前节点的标记
if (l <= m) update(l, r, c, s, m, p * 2);
if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
d[p] = d[p * 2] + d[p * 2 + 1];
同时查询也要变化:
if (l <= s && t <= r) return d[p];
// 当前区间为询问区间的子集时直接返回当前区间的和
if (b[p]) {
d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m),
b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点
b[p] = 0; // 清空当前节点的标记
int sum = 0;
if (l <= m) sum = getsum(l, r, s, m, p * 2);
如果你是要实现区间修改为某一个值而不是加上某一个值的话
d[p] = (t - s + 1) * c, b[p] = c;
d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m),
b[p * 2] = b[p * 2 + 1] = b[p];
b[p] = 0;