线段树#
线段树是一棵完全二叉树,每个节点维护数组上一段区间的聚合信息(和、最大值等), 支持 \(O(\log n)\) 的单点更新与区间查询,配合懒标记还可 \(O(\log n)\) 完成区间更新。
与树状数组相比:线段树功能更全(可维护区间最值、可区间更新),代价是代码更长、常数更大。
属于”常用数据结构”题单(前缀和/栈/队列/堆/字典树/并查集/树状数组/线段树)的最后一块。
一、原理#
根节点
1代表整个区间[0, n-1];节点
p代表区间[l, r],其左右孩子2p、2p+1分别代表[l, mid]、[mid+1, r],其中mid = (l+r)/2;区间信息自底向上合并(
tree[p] = tree[2p] + tree[2p+1]);数组大小开 4 倍 n 保证不越界。
二、单点更新 + 区间求和#
struct SegTree {
int n;
vector<int> tree;
SegTree(vector<int>& a) : n(a.size()), tree(4 * n) { build(a, 1, 0, n - 1); }
void build(vector<int>& a, int p, int l, int r) {
if (l == r) {
tree[p] = a[l];
return;
}
int mid = (l + r) / 2;
build(a, p * 2, l, mid);
build(a, p * 2 + 1, mid + 1, r);
tree[p] = tree[p * 2] + tree[p * 2 + 1];
}
void update(int idx, int val, int p, int l, int r) { // 单点改为 val
if (l == r) {
tree[p] = val;
return;
}
int mid = (l + r) / 2;
if (idx <= mid)
update(idx, val, p * 2, l, mid);
else
update(idx, val, p * 2 + 1, mid + 1, r);
tree[p] = tree[p * 2] + tree[p * 2 + 1];
}
int query(int ql, int qr, int p, int l, int r) { // 区间 [ql, qr] 的和
if (ql <= l && r <= qr) return tree[p]; // 完全覆盖
int mid = (l + r) / 2, res = 0;
if (ql <= mid) res += query(ql, qr, p * 2, l, mid);
if (qr > mid) res += query(ql, qr, p * 2 + 1, mid + 1, r);
return res;
}
};
class SegTree {
int n;
int[] tree;
SegTree(int[] a) {
n = a.length;
tree = new int[4 * n];
build(a, 1, 0, n - 1);
}
void build(int[] a, int p, int l, int r) {
if (l == r) { tree[p] = a[l]; return; }
int mid = (l + r) / 2;
build(a, p * 2, l, mid);
build(a, p * 2 + 1, mid + 1, r);
tree[p] = tree[p * 2] + tree[p * 2 + 1];
}
void update(int idx, int val, int p, int l, int r) {
if (l == r) { tree[p] = val; return; }
int mid = (l + r) / 2;
if (idx <= mid) update(idx, val, p * 2, l, mid);
else update(idx, val, p * 2 + 1, mid + 1, r);
tree[p] = tree[p * 2] + tree[p * 2 + 1];
}
int query(int ql, int qr, int p, int l, int r) {
if (ql <= l && r <= qr) return tree[p];
int mid = (l + r) / 2, res = 0;
if (ql <= mid) res += query(ql, qr, p * 2, l, mid);
if (qr > mid) res += query(ql, qr, p * 2 + 1, mid + 1, r);
return res;
}
}
Note
区间查询的关键是分三类讨论:完全覆盖直接返回;否则下放到左右孩子;完全不相交的区间不会被递归到(由调用处条件保证)。
三、区间更新 + 懒标记(Lazy)#
区间整体加 v 时,若对每个叶子都更新则退化为 \(O(n)\)。懒标记让更新”停在完全覆盖的节点”:
先只改该节点并打上标记,等将来查询/更新下放到孩子时再”下推”标记。
struct SegTreeLazy {
int n;
vector<long long> tree, lazy; // lazy[p] 表示节点 p 待下推的区间增量
SegTreeLazy(vector<int>& a) : n(a.size()), tree(4 * n), lazy(4 * n) {
build(a, 1, 0, n - 1);
}
void build(vector<int>& a, int p, int l, int r) {
if (l == r) {
tree[p] = a[l];
return;
}
int mid = (l + r) / 2;
build(a, p * 2, l, mid);
build(a, p * 2 + 1, mid + 1, r);
tree[p] = tree[p * 2] + tree[p * 2 + 1];
}
void pushDown(int p, int l, int r) { // 下推懒标记
if (lazy[p] == 0 || l == r) return;
int mid = (l + r) / 2;
tree[p * 2] += lazy[p] * (mid - l + 1);
tree[p * 2 + 1] += lazy[p] * (r - mid);
lazy[p * 2] += lazy[p];
lazy[p * 2 + 1] += lazy[p];
lazy[p] = 0;
}
void rangeAdd(int ql, int qr, int v, int p, int l, int r) { // 区间 [ql, qr] 加 v
if (ql <= l && r <= qr) {
tree[p] += (long long)v * (r - l + 1);
lazy[p] += v;
return;
}
pushDown(p, l, r);
int mid = (l + r) / 2;
if (ql <= mid) rangeAdd(ql, qr, v, p * 2, l, mid);
if (qr > mid) rangeAdd(ql, qr, v, p * 2 + 1, mid + 1, r);
tree[p] = tree[p * 2] + tree[p * 2 + 1];
}
long long query(int ql, int qr, int p, int l, int r) {
if (ql <= l && r <= qr) return tree[p];
pushDown(p, l, r);
int mid = (l + r) / 2;
long long res = 0;
if (ql <= mid) res += query(ql, qr, p * 2, l, mid);
if (qr > mid) res += query(ql, qr, p * 2 + 1, mid + 1, r);
return res;
}
};
Note
更新/查询经过某个带标记的节点时,必须先
pushDown,否则孩子信息是过期的;懒标记的值要随子区间长度按比例计入
tree(区间加 v 时,长度为 len 的区间总和增加v * len);若维护的是区间最大值而非和,把”求和 + 贡献”改为”取 max”即可,思路不变。