线段树
线段树是一种经常用来维护区间信息的数据结构,可以在 \(O(\log_2 n)\) 的时间复杂度之内完成单点修改、区间修改、区间查询等操作,底层是一个二叉树。
结构与建树
我们可以画出一颗树的形态,每往下一层就会将上一层管辖的范围分成两半,然后递归进行操作。我们可以递归建树,当前建树的区域为 \([l,r]\),所以我们进行递归建好左右子树,并累加上左右子树的区间值。代码如下:
void build(int l, int r, int p) {
if (l == r) { // 如果当前子树只有一个结点
t[p] = a[l]; // 直接赋值
return;
}
int m = l + r >> 1; // 找出分界点
build(l, m, 2 * p); // 建左子树
build(m + 1, r, 2 * p + 1); // 建右子树
t[p] = t[2 * p] + t[2 * p + 1]; // 累加上左右子树的和
}
区间查询
比如求 \([l,r]\) 当中的和 / 最大值可以使用线段树,达到 \(O(\log_2 n)\) 的效果。
当我们需要求出 \([l,r]\) 的和的时候,很显然并不一定有一个子树 \(t_i\) 可以直接满足要求,但是我们可以将它拆分成两个区间 \([l,r]\) 和 \([m,r]\),然后继续递归调用。
// 当前查询区间 [l, r],当前结点区间为 [s, t]。
int query(int l, int r, int s, int t, int p) {
if (l <= s && t <= r) { // 已经包含在了 [s, t] 里面
return t[p];
}
int m = s + t >> 1, s = 0;
if (l <= m) { // 如果 [s, t] 与 [l, r] 有交集
s += query(l, r, s, m, 2 * p); // 递归做儿子
}
if (r > m) {
s += query(l, r, m + 1, t, 2 * p + 1);
}
return s;
}
区间修改、惰懒标记
对于区间修改,我们可以直接递归子树相加,但是这是 \(O(n)\) 的,不能承受。所以我们需要一个叫做躲懒标记的东西。这个东西被标记了之后只会在下一次访问当前结点的时候再去进行修改,减少了不必要的麻烦。
- 区间修改:
// 修改 [l, r],当前子树是 [s, t],当前编号是 p,每个结点加上 c
void update(int l, int r, int c, int s, int t, int p) {
if (l <= s && t <= r) {
t[p] += (t - s + 1) * c, f[p] += c; // 直接相加,并加上标记
return;
}
int m = s + t >> 1;
if (s != t && f[p]) { // 如果有标记
t[2 * p] += t[p] * (m - s + 1); // 左子树相加
t[2 * p + 1] += t[p] * (t - m); // 右子树相加
f[p] = 0; // 标记清空
}
if (l <= r) { // 递归左子树
update(l, r, c, s, m, 2 * p);
}
if (r > m) { // 递归右子树
update(l, r, c, m + 1, t, 2 * p + 1);
}
t[p] = t[2 * p] + t[2 * p + 1];
}
- 区间查询:
void query(int l, int r, int s, int t, int p) {
if (l <= s && t <= r) {
return t[p];
}
int m = s + t >> 1, s = 0;
if (f[p]) { // 如果有标记
t[2 * p] += t[p] * (m - s + 1); // 左子树相加
t[2 * p + 1] += t[p] * (t - m); // 右子树相加
f[p] = 0; // 标记清空
}
if (l <= m) {
s += query(l, r, s, m, 2 * p);
}
if (r > m) {
s += query(l, r, m + 1, t, 2 * p + 1);
}
return s;
}
至此,简单的线段树就完成了!复杂的还在写。。。

浙公网安备 33010602011771号