线段树学习笔记
线段树的定义:一棵满二叉树。
假设一棵线段树中有 n n n 个节点,那么这一棵线段树存储需要使用 n + n 2 + n 4 + ⋯ + 2 + 1 ≈ 2 × n n + \frac{n}{2} + \frac{n}{4} + \cdots + 2 + 1 \approx 2\times n n+2n+4n+⋯+2+1≈2×n 个节点。
线段树的每一个节点记录的都是所有需要求/询问的信息。
节点部分:
struct Node {
int l, r;
int ???, ...;
void init(int p) { // 初始化
l = r = p;
??? = ???;
......;
}
} z[???];
假设现在要求 ∑ i = l r a i \sum_{i=l}^r a_i ∑i=lrai 的值。
那么最底下的单点区间记录的都是这个节点的值。
第二行的区间假设为 [ 1 , 2 ] [1,2] [1,2],那么这个区间记录的和就是 a 1 + a 2 a_1 + a_2 a1+a2。
考虑从下往上递归求解。
当左右儿子递归完成合并的时候,只需要将当前点的值更新为左儿子的值和右儿子的值的和即可。
如果求 max i = l r a i \max_{i=l}^r a_i maxi=lrai 的值,那么把求和改成取最大值即可。
但是如果要求询问区间和,区间最小值和区间最大值,那么开三个数组比较麻烦,开一个结构体即可。
建树部分:
// 将左儿子和右儿子合并
Node operator + (const Node &lhs, const Node &rhs) {
Node res;
res.l = lhs.l;
res.r = rhs.r;
res.??? = ???;
......;
return res;
}
// 其中 [l,r] 是当前维护的区间
// rt 是当前现在线段树节点的下标
void build(int l, int r, int rt) {
if (l == r)
// 最后一层节点 不能再分 更新当前节点
z[rt].init(l);
// init 成员函数是线性的
else {
int mid = l + r >> 1;
// 规定左儿子对应区间为 l~mid 右儿子对应区间为 mid~r
build(l, mid, rt << 1);
build(mid + 1, r, rt << 1 | 1);
z[rt] = z[rt << 1] + z[rt << 1 | 1];
// 更新上一层的节点
// 时间复杂度 O(1)
// 需要重载运算符+
}
}
线段树建树的整体复杂度为 O ( n ) O(n) O(n),因为每一个节点仅仅更新了一次。
假设现在只有一个求和操作:
struct Node {
int l, r, sum;
void init(int p) {
l = r = p;
sum = a[p];
}
} z[N << 2];
Node operator + (const Node &lhs, const Node &rhs) {
Node res;
res.l = lhs.l;
res.r = rhs.r;
res.sum = lhs.sum + rhs.sum;
return res;
}
void build(int l, int r, int rt) {
if (l == r)
z[rt].init(l);
else {
int mid = l + r >> 1;
build(l, mid, rt << 1);
build(mid + 1, r, rt << 1 | 1);
z[rt] = z[rt << 1] + z[rt << 1 | 1];
}
}
询问一段区间的和的时候,考虑将其拆分成一些段区间。
比如说头顶上的线段树,询问区间 [ 2 , 6 ] [2,6] [2,6] 的和,拆分出最小的段数,变为 [ 2 , 2 ] + [ 3 , 4 ] + [ 5 , 6 ] [2,2]+[3,4]+[5,6] [2,2]+[3,4]+[5,6] 这三个区间的和即可。
// 询问区间[nowl,nowr]的和
int query(int l, int r, int rt, int nowl, int nowr) {
if (nowl <= l && r <= nowr)
// 当前区间被完全包含 直接返回当前区间的和
return z[rt].sum;
int mid = l + r >> 1;
if (nowr <= mid)
// 右儿子没有 返回左儿子里递归出来的结果
return query(l, mid, rt << 1, nowl, nowr);
if (mid < nowl)
// 同理
return query(mid + 1, r, rt << 1 | 1, nowl, nowr);
return query(l, mid, rt << 1, nowl, nowr) + query(mid + 1, r, rt << 1 | 1, nowl, nowr); // 左右儿子都得递归
}