Appearance
zkw 线段树
普通线段树是一棵二叉树,zkw 线段树是一棵满二叉树,同样采用堆式建树。因为其满二叉树的性质,使得它能容易地获取叶子节点编号,也可以通过位运算简单地获取区间。具有代码短,常数小的优点。
了解即可,实用价值有,但是不大。
下面以区间加、区间和为例。接口统一使用从 sum 增加“增量乘区间长度”,查询或继续向下访问前再把祖先标记下传。
数组固定树形也不意味着不能持久化;可以持久化底层数组或记录每个版本的修改。只是竞赛中路径复制通常配合显式左右儿子的递归实现更方便。
区间加、区间和参考实现
cpp
struct zkw_segment_tree {
int size, height;
vector<long long> sum, tag;
vector<int> length;
void apply(int p, long long value) {
sum[p] += value * length[p];
if (p < size)
tag[p] += value;
}
void push_up(int p) {
// tag[p] 永久保留在 p 上时也不能丢掉它的贡献。
sum[p] = sum[p << 1] + sum[p << 1 | 1] + tag[p] * length[p];
}
void push_down(int p) {
if (!tag[p])
return;
apply(p << 1, tag[p]);
apply(p << 1 | 1, tag[p]);
tag[p] = 0;
}
void push_path(int p) {
for (int level = height; level >= 1; level--)
push_down(p >> level);
}
void init(const vector<long long> &a) {
size = 1;
height = 0;
while (size < (int)a.size()) {
size <<= 1;
height++;
}
sum.assign(size << 1, 0);
tag.assign(size, 0);
length.assign(size << 1, 1);
length[0] = 0;
for (int p = size - 1; p; p--)
length[p] = length[p << 1] + length[p << 1 | 1];
for (int i = 0; i < (int)a.size(); i++)
sum[size + i] = a[i];
for (int p = size - 1; p; p--)
push_up(p);
}
void range_add(int l, int r, long long value) {
if (l >= r)
return;
int left = l + size, right = r + size;
int left_leaf = left, right_leaf = right - 1;
push_path(left_leaf);
push_path(right_leaf);
while (left < right) {
if (left & 1)
apply(left++, value);
if (right & 1)
apply(--right, value);
left >>= 1;
right >>= 1;
}
for (int p = left_leaf >> 1; p; p >>= 1)
push_up(p);
for (int p = right_leaf >> 1; p; p >>= 1)
push_up(p);
}
long long range_sum(int l, int r) {
if (l >= r)
return 0;
int left = l + size, right = r + size;
push_path(left);
push_path(right - 1);
long long left_sum = 0, right_sum = 0;
while (left < right) {
if (left & 1)
left_sum += sum[left++];
if (right & 1)
right_sum += sum[--right];
left >>= 1;
right >>= 1;
}
return left_sum + right_sum;
}
void point_add(int position, long long value) {
range_add(position, position + 1, value);
}
long long point_query(int position) {
return range_sum(position, position + 1);
}
};建树为
猫树
名字由来似乎是首次在国内 OI 届引入此数据结构的选手的网名。
猫树通常指 Disjoint Sparse Table,用于静态区间询问。它只要求合并运算满足结合律,不要求幂等性;预处理时间和空间均为
对于两个叶子节点
若每一层都维护块内的后缀积和前缀积,那么查询 op(suffix[l], prefix[r]),不能颠倒。
对于 LCA,因为 zkw 是满二叉树,所以 LCA = l >> (log[l ^ r] + 1)。
点击展开代码
cpp
template <class T, class Op>
struct disjoint_sparse_table {
int n, levels;
vector<T> value;
vector<vector<T>> table;
Op op;
// a 使用 0 下标。
void init(const vector<T> &a, Op operation = Op()) {
value = a;
op = operation;
n = (int)a.size();
levels = 0;
while ((1 << levels) < n)
levels++;
table.assign(levels, vector<T>(n));
for (int k = 0; k < levels; k++) {
int half = 1 << k;
int block_length = half << 1;
for (int left = 0; left < n; left += block_length) {
int middle = min(left + half, n);
int right = min(left + block_length, n);
if (left < middle) {
table[k][middle - 1] = value[middle - 1];
for (int i = middle - 2; i >= left; i--)
table[k][i] = op(value[i], table[k][i + 1]);
}
if (middle < right) {
table[k][middle] = value[middle];
for (int i = middle + 1; i < right; i++)
table[k][i] = op(table[k][i - 1], value[i]);
}
}
}
}
// 查询 0 下标闭区间 [left, right],要求 left <= right。
T query(int left, int right) const {
if (left == right)
return value[left];
int level = 31 - __builtin_clz((unsigned)(left ^ right));
return op(table[level][left], table[level][right]);
}
};经典的重叠 Sparse Table 依赖幂等性,而猫树与 Sqrt Tree 都能维护任意结合运算。它们的功能不存在严格包含关系,空间常数也取决于布局、是否补齐到二次幂以及实现方式,不能笼统断言“至少是 ST 表四倍”或“Sqrt Tree 功能严格更强”。
李超线段树
李超线段树用于动态插入直线或限定横坐标范围的线段,并查询给定横坐标处的最优直线。下文维护最大值;若纵坐标相同,规定编号更小的直线更优。
下面的实现维护离散横坐标数组 x。树上区间 x[l] 到 x[r],所有插入端点和查询横坐标都必须先映射为数组下标。节点的 id == 0 明确表示“当前节点没有直线”,不会访问 line[0],因此区间插入时允许出现已经创建、但尚未保存主导直线的祖先节点。
询问:
查询时沿根到叶子的路径,把途中所有非空主导直线按同一个比较规则合并即可,时间复杂度为
整数直线、离散横坐标参考实现
cpp
using i128 = __int128_t;
struct li_chao_tree {
struct line_type {
long long slope, intercept;
i128 value(long long x) const {
return (i128)slope * x + intercept;
}
};
struct node_type {
int left = 0, right = 0, id = 0;
};
vector<long long> x;
vector<line_type> line{line_type{}}; // 下标 0 不存放直线
vector<node_type> tree{node_type{}}; // 下标 0 表示空节点
int root = 0;
void init(vector<long long> coordinates) {
sort(coordinates.begin(), coordinates.end());
coordinates.erase(unique(coordinates.begin(), coordinates.end()), coordinates.end());
assert(!coordinates.empty());
x = move(coordinates);
line.assign(1, line_type{});
tree.assign(1, node_type{});
root = 0;
}
int add_line(long long slope, long long intercept) {
line.push_back({slope, intercept});
return (int)line.size() - 1;
}
int new_node() {
tree.push_back({});
return (int)tree.size() - 1;
}
bool better(int first, int second, int position) const {
if (!first)
return false;
if (!second)
return true;
i128 first_value = line[first].value(x[position]);
i128 second_value = line[second].value(x[position]);
if (first_value != second_value)
return first_value > second_value;
return first < second;
}
int insert_line(int p, int left, int right, int id) {
if (!p)
p = new_node();
if (!tree[p].id) {
tree[p].id = id;
return p;
}
int middle = (left + right) >> 1;
if (better(id, tree[p].id, middle))
swap(id, tree[p].id);
if (left == right)
return p;
if (better(id, tree[p].id, left))
tree[p].left = insert_line(tree[p].left, left, middle, id);
else if (better(id, tree[p].id, right))
tree[p].right = insert_line(tree[p].right, middle + 1, right, id);
return p;
}
void insert_line(int id) {
root = insert_line(root, 0, (int)x.size() - 1, id);
}
int insert_segment(int p, int left, int right,
int query_left, int query_right, int id) {
if (query_right < left || right < query_left)
return p;
if (!p)
p = new_node();
if (query_left <= left && right <= query_right)
return insert_line(p, left, right, id);
int middle = (left + right) >> 1;
tree[p].left = insert_segment(tree[p].left, left, middle,
query_left, query_right, id);
tree[p].right = insert_segment(tree[p].right, middle + 1, right,
query_left, query_right, id);
return p;
}
void insert_segment(int left, int right, int id) {
root = insert_segment(root, 0, (int)x.size() - 1, left, right, id);
}
int query(int p, int left, int right, int position) const {
if (!p)
return 0;
int answer = tree[p].id;
if (left == right)
return answer;
int middle = (left + right) >> 1;
int child_answer;
if (position <= middle)
child_answer = query(tree[p].left, left, middle, position);
else
child_answer = query(tree[p].right, middle + 1, right, position);
if (better(child_answer, answer, position))
answer = child_answer;
return answer;
}
// 返回 0 表示该横坐标没有任何已插入线段。
int query(int position) const {
return query(root, 0, (int)x.size() - 1, position);
}
};整条直线插入调用 insert_line(id),为 __int128 比较整数斜率、截距和横坐标,避免 long long 乘法先溢出。若数据是浮点数,则相等判断、近交点误差和无穷值协议必须按题目精度要求重新设计,不能直接套用整数比较。
可持久化线段树
动态开点线段树:
因为是可持久化线段树的前置知识(严格需要),略提一下。
一般线段树建树时,将所有区间都在节点上表示了出来。但是事实上,对于一个节点
即:只有在访问到某个区间时,才创建对应的节点编号。如果只是将朴素线段树,以单点加、区间和为例替换成动态开点线段树,只需在函数体中,把节点编号改为引用,并把节点的左右儿子显式地存储在节点的结构体中,而不是沿用堆式建树的隐式儿子表示。当当前递归区间的节点不存在时,将其赋予新的编号即可。询问时若遇到空节点则表示没有内容,返回空即可。
点击展开代码
cpp
void update(int &p, int l, int r, int x, int v) {
int mid = l + r >> 1;
if (!p)
p = ++idx;
if (l == r) {
tr[p].sum += v;
return;
}
if (x <= mid)
update(ls(p), l, mid, x, v);
else
update(rs(p), mid + 1, r, x, v);
push_up(p);
}
int query(int p, int l, int r, int x, int y) {
int mid = l + r >> 1, res = 0;
if (!p)
return 0;
if (x <= l && r <= y) {
return tr[p].sum;
}
if (x <= mid)
res += query(ls(p), l, mid, x, y);
if (y > mid)
res += query(rs(p), mid + 1, r, x, y);
return res;
}因为动态开点只涉及到需要的节点,所以可以支持维护序列长度很大的情况,每一次操作最多涉及线段树上
因为动态开点每次最劣会创建访问到的区间数量的节点数,所以动态开点的空间复杂度是
标记永久化:
因为是可持久化线段树的前置知识(不严格需要),略提一下。
一个区间在线段树上的规范分解只包含
标记永久化的实现选择不下传,而是在访问路径上累计祖先标记。与懒标记相比,对于不能直接累计的标记,通常更难处理。
可持久化数据结构意味可以维护历史版本,即第
因为线段树是自上而下的数据结构,对于一个确定的子树,其维护的信息是确定的。所以若节点
主席树:
可持久化权值(值域)线段树一般被称作主席树。下面维护前缀出现次数:root[i] 表示前 kth_in_subarray(left, right, k) 返回子数组
点击展开代码
cpp
struct node {
int left = 0, right = 0, sum = 0;
} tr[N << 5];
int idx, root[N];
int version_count, compressed_size;
int clone_node(int previous) {
int current = ++idx;
tr[current] = tr[previous]; // 叶子和内部节点的旧信息全部复制
return current;
}
int update(int previous, int left, int right, int position) {
int current = clone_node(previous);
tr[current].sum++;
if (left == right)
return current;
int middle = (left + right) >> 1;
if (position <= middle)
tr[current].left = update(tr[previous].left, left, middle, position);
else
tr[current].right = update(tr[previous].right, middle + 1, right, position);
return current;
}
// rank[i] 是第 i 个元素在 [1, value_count] 中的离散排名。
void build_prefix_versions(int n, int value_count, const vector<int> &rank) {
idx = 0;
version_count = n;
compressed_size = value_count;
tr[0] = {};
root[0] = 0;
for (int i = 1; i <= n; i++)
root[i] = update(root[i - 1], 1, value_count, rank[i]);
}
int kth(int left_root, int right_root,
int left, int right, int k) {
if (left == right)
return left;
int middle = (left + right) >> 1;
int left_count = tr[tr[right_root].left].sum
- tr[tr[left_root].left].sum;
if (k <= left_count)
return kth(tr[left_root].left, tr[right_root].left,
left, middle, k);
return kth(tr[left_root].right, tr[right_root].right,
middle + 1, right, k - left_count);
}
// 返回 -1 表示参数越界;否则返回离散后的值域下标。
int kth_in_subarray(int left, int right, int k) {
if (left < 1 || right > version_count || left > right || k <= 0)
return -1;
int count = tr[root[right]].sum - tr[root[left - 1]].sum;
if (k > count)
return -1;
return kth(root[left - 1], root[right], 1, compressed_size, k);
}每个新版本只复制根到一个叶子的
区间修改:
若第 push_down,常数略大,容易爆空间。 使用标记永久化,会有更好的效果。
Trick:
若第
吉司机线段树
历史最值线段树,经典的“势能线段树”,暂略。
线段树合并
线段树合并就是将两棵线段树上表示相同区间的信息合并起来,所以时间复杂度显然与节点数相关,所以线段树合并通常针对动态开点线段树,只合并需要用到的点。
实现不难理解,对于合并 push_up 更新祖先节点即可。
以区间和为例:
点击展开代码
cpp
int merge(int p, int q, int l, int r) {
int mid = l + r >> 1;
if (!p)
return q;
if (!q)
return p;
if (l == r) {
tr[p].sum += tr[q].sum;
return p;
}
ls(p) = merge(ls(p), ls(q), l, mid);
rs(p) = merge(rs(p), rs(q), mid + 1, r);
push_up(p);
return p;
}上述代码是破坏式合并:调用后必须丢弃根
线段树合并从应用角度而言,通常用于合并权值线段树,并且更多地应用在树上,用于合并子树信息。
线段树分裂
线段树分裂是线段树合并的逆过程。若节点只保存一个不可逆聚合值,例如只保存两个集合合并后的最大值,就不能仅凭这个聚合值反推出两部分;通常需要保留子树、叶子信息或修改历史。这不等于“最大值操作不能撤销”:若记录修改前的值或维护修改栈,最大值同样可以回滚。
以权值线段树统计数字出现次数为例:保留前
点击展开代码
cpp
void split(int p, int &q, int k) {
if (!p)
return;
if (!q)
q = ++idx;
if (k > tr[ls(p)].sum)
split(rs(p), rs(q), k - tr[ls(p)].sum);
else
swap(rs(p), rs(q));
if (k < tr[ls(p)].sum)
split(ls(p), ls(q), k);
tr[q].sum = tr[p].sum - k;
tr[p].sum = k;
}线段树分裂的时间复杂度容易发现是单次
线段树优化建图
线段树优化建图用于将一个点和一个区间内的所有点连边的问题,例如将
根据任意一个区间
若
若
但是容易发现,这不能在同一棵线段树上进行。所以建两棵线段树,一棵向儿子节点连边,一棵向父亲节点连边。两棵树表示同一个原点的叶子必须双向连接权值为 id1[i] 与 id2[i] 成为原点 id2[u] -> id2[v]。
以最短路为例:
点击展开代码
cpp
struct segment {
int idx;
int id1[N], id2[N];
void build1(int t, int l, int r) {
idx = max(idx, t);
int mid = l + r >> 1;
if (l == r) {
id1[l] = t;
return;
}
add(t, t << 1, 0);
add(t, t << 1 | 1, 0);
build1(t << 1, l, mid);
build1(t << 1 | 1, mid + 1, r);
}
void build2(int t, int l, int r) {
int mid = l + r >> 1;
if (l == r) {
id2[l] = t + idx;
return;
}
add((t << 1) + idx, t + idx, 0);
add((t << 1 | 1) + idx, t + idx, 0);
build2(t << 1, l, mid);
build2(t << 1 | 1, mid + 1, r);
}
void link1(int t, int l, int r, int x, int y, int u, int w) {
int mid = l + r >> 1;
if (x <= l && r <= y) {
add(id2[u], t, w);
return;
}
if (x <= mid)
link1(t << 1, l, mid, x, y, u, w);
if (y > mid)
link1(t << 1 | 1, mid + 1, r, x, y, u, w);
}
void link2(int t, int l, int r, int x, int y, int u, int w) {
int mid = l + r >> 1;
if (x <= l && r <= y) {
add(t + idx, id1[u], w);
return;
}
if (x <= mid)
link2(t << 1, l, mid, x, y, u, w);
if (y > mid)
link2(t << 1 | 1, mid + 1, r, x, y, u, w);
}
} tree;
signed main() {
int n, q, s;
cin >> n >> q >> s;
tree.build1(1, 1, n);
tree.build2(1, 1, n);
int i;
For(i, 1, n) add(tree.id1[i], tree.id2[i], 0),
add(tree.id2[i], tree.id1[i], 0);
while (q--) {
int op;
cin >> op;
if (op == 1) {
int u, v, w;
cin >> u >> v >> w;
add(tree.id1[u], tree.id1[v], w);
add(tree.id2[u], tree.id2[v], w);
add(tree.id1[u], tree.id2[v], w);
add(tree.id2[u], tree.id1[v], w);
}
if (op == 2) {
int u, l, r, w;
cin >> u >> l >> r >> w;
tree.link1(1, 1, n, l, r, u, w);
}
if (op == 3) {
int u, l, r, w;
cin >> u >> l >> r >> w;
tree.link2(1, 1, n, l, r, u, w);
}
}
return 0;
}线段树优化建图本质上只是一个建图的工具,结合到具体题目,还是需要一定的图论知识。
线段树分治
线段树分治一定程度上类似线段树优化建图,本质还是利用线段树的性质:一个区间
对于这一段时间,可以拆分成
把一段时间
一个线段树分治的一个经典应用是和可撤销并查集的结合使用,用来维护点的连通性。
点击展开代码
cpp
struct sege {
int n;
vector<int> tr[N << 2];
function<void(int)> answer_query;
void update(int t, int l, int r, int x, int y, int id) {
int mid = l + r >> 1;
if (x <= l && r <= y) {
tr[t].pb(id);
return;
}
if (x <= mid)
update(t << 1, l, mid, x, y, id);
if (y > mid)
update(t << 1 | 1, mid + 1, r, x, y, id);
}
void solve(int t, int l, int r) {
int his = dsu.History();
for (auto u : tr[t]) {
dsu.merge(edge[u].x, edge[u].y);
}
if (l == r) {
answer_query(l); // 根据题意回答第 l 个时刻的询问
} else {
int mid = l + r >> 1;
solve(t << 1, l, mid);
solve(t << 1 | 1, mid + 1, r);
}
while (dsu.History() != his)
dsu.roll(); // 可撤销并查集部分
}
} tree;线段树上二分
对于一个单调的区间询问,例如正权区间内第一个前缀和大于
如果询问不是全局的,同样地,将询问区间拆分成线段树上的
时间复杂度:
