最简单的线段树
这种线段树支持 \(O(logn)\) 的单点修改与区间查询
1. 基本的线段树建立
利用分治的思想,一个大的区间被分为两个区间,之后再分,直到分到每个点为止
可以用结构体存储线段树的每个节点,节点的最基础的属性就是区间的左右端点,表示了区间的范围
// 这里所有区间的个数严格小于 N * 4,因此开 N * 4 个节点
struct Node {
int l, r;
int sum;
}tr[N << 2];
int a[N];
void build(int p, int l, int r) {
tr[p] = {l, r};
if (l == r) {
tr[p].s = a[l];
return;
}
int mid = l + r >> 1;
build(p << 1, l, mid);
build(p << 1 | 1, mid + 1, r);
pushup(p);
}
2. \(pushup\)
在线段树中,每次儿子节点改变后,若这会影响父节点的值,就会用儿子节点的值,更新父节点的值,即为 \(pushup\) 的过程
void pushup(int p) {
tr[p].sum = tr[p << 1].sum + tr[p << 1 | 1].sum;
}
3.单点修改
想要进行单点修改,会先用函数递归的找到需要修改的节点,修改之后,再利用 \(pushup\),用修改好的儿子节点更新父节点,最后,线段树中包含这个点的所有区间都会被更新
void update(int p, int l, int r, int k) {
if (l == tr[p].l && r == tr[p].r) {
tr[p].sum += k;
return;
}
int mid = l + r >> 1;
if (x <= mid) update(p << 1, l, r, k);
else update(p << 1 | 1, l, r, k);
pushup(p);
}
4.区间查询
区间的查询实际上是将线段树上彼此相邻的节点拼起来讨论,这个拼起来的操作实际上和 \(pushup\) 是相同的,都是把小区间凑成大区间,那么有一种返回值为结构体的 \(query\) 函数可以得到整个的区间,这样可以将整个区间的属性一并输出,对于要求输出多种查询的问题,可以简化代码(具体在拓展中)
int query(int p, int l, int r) {
if (l <= tr[p].l && tr[p].r <= r) {
return tr[p].s;
}
int res = 0;
int mid = l + r >> 1;
if (l <= mid) res += query(p << 1, l, r);
if (r > mid) res += query(p << 1 | 1, l, r);
return res;
}
用法
- 单点修改 + 区间查询
- 区间修改 + 单点查询(与差分并用)
拓展
我们知道,在线段树上的操作,是对于区间的拆分与拼接,那么可以将结构体作为函数的返回值,传递更多的信息,以及提供一些复杂的操作
// pushup(int p) 的同名函数
Node pushup(Node L, Node R) {
return {L.l, R.r, L.sum + R.sum};
}
void pushup(int p) {
tr[p] = pushup(tr[p << 1], tr[p << 1 | 1]);
}
void build(...) {
...
}
void update(...) {
...
}
Node query(int p, int l, int r) {
if (l <= tr[p].l && tr[p].r <= r) {
return tr[p];
}
int mid = l + r >> 1;
if (y <= mid) return query(p << 1, l, r);
else if (x > mid) return query(p << 1 | 1, l, r);
else return pushup(tr[p << 1], tr[p << 1 | 1]);
}
那么,这样的操作有什么意义呢,答:可以增加一些对线段树的理解,以及一些题目需要这样写来传递区间的多个属性