树上主席树
树上主席树
主席树, 但是维护树上路径信息.
由于Defad今天忌离散化, 就不离散化了, 把值域开大点一般没啥问题.
上次的主席树有个朋友说没完全讲清楚, 这次先讲透了
主席树, 整体围绕的是前缀和, 用"批判的继承"维护前缀和, 然后在前缀和上二分.
为什么主席树不可修改, 就是因为这个前缀和思想, 所以主席树要修改需要借助树状数组完成 \(\log^{2}{N}\) 的修改.
但是树套树等Defad调出来再说, 下篇博客先讲莫队, 应该有几篇是一系列的莫队.
void update(int *p, int q, int l, int r, int x, int k) {
if (*p == 0) {
*p = ++cntt;
}
if (l == r) {
// 批判的继承
tr[*p].val = tr[q].val + k;
return;
}
int m = l + r >> 1;
if (x <= m) {
tr[*p].rs = tr[q].rs; // 继承
update(&tr[*p].ls, tr[q].ls, l, m);
} else {
tr[*p].ls = tr[q].ls; // 继承
update(&tr[*p].rs, tr[q].rs, m + 1, r);
}
push_up(*p);
}
显然, 每个批判的继承的结点都有一个儿子是继承的, 另一个儿子是批判的继承的, 这点我们看图就很显然.
\(cntt := cntt + 1\), \(rt_{2} := cntt\), 现在第二个权值线段树的根为 \(8\), 左儿子继承了 \(rt_{1}\) 的左儿子 \(2\), 右儿子 \(9\) 批判的继承了 \(rt_{1}\) 的右儿子 \(5\), \(9\) 的右儿子继承了 \(rt_{1}\) 的右儿子的右儿子 \(7\), 左儿子批判的继承了 \(rt_{1}\) 的右儿子的左儿子 \(6\).
现在 \(cntt := cntt + 1\), \(rt_{3} := cntt\), 可以手动模拟一下第三个权值线段树继承第二个权值线段树的操作.
那么查询就是在前缀和上二分了.
先考虑查询 \(1\) 到 \(x\), 也就是前缀 \(k\) th.
int kth(int p, int l, int r, int k) {
if (l == r) {
return l;
}
int m = l + r >> 1;
if (k <= tr[tr[p].ls].val) {
return kth(tr[p].ls, l, m, k);
} else if (k <= tr[p].val) {
k -= tr[tr[p].ls].val;
return kth(tr[p].rs, m + 1, r, k);
} else return -1;
}
就是在第 \(x\) 个权值线段树 \(rt_{x}\) 上找第 \(k\) 个.
那么就可以由维护前缀和之后求区间和是 \(s_{r} - s_{l - 1}\) 得到 区间 \(k\) th 就是二分第 \(l - 1\) 个权值线段树 \(rt_{l - 1}\) 和 第 \(r\) 个权值线段树 \(rt_{r}\) 即可.
int kth(int p, int q, int l, int r, int k) {
if (l == r) {
return l;
}
int m = l + r >> 1;
if (k <= tr[tr[q].ls].val - tr[tr[p].ls].val) {
return kth(tr[p].ls, tr[q].ls, l, m, k);
} else if (k <= tr[q].val - tr[p].val) {
k -= tr[tr[q].ls].val - tr[tr[p].ls].val;
return kth(tr[p].rs, tr[q].rs, m + 1, r, k);
} else return -1;
}
扩展到树上
刚才我们求的是区间 \(k\) th, 现在求树上的路径 \(k\) th.
首先, 树上前缀和是怎么求路径和的?
这篇博客只讨论从根往下的前缀和.
点权: \(s_{x} + s_{y} - s_{\operatorname{lca}(x, y)} - s_{fa_{\operatorname{lca}(x, y)}}\)
边权下放点权: \(s_{x} + s_{y} - 2 * s_{\operatorname{lca}(x, y)}\)
为什么是这样呢?
观察点权的路径和.
点权是本来这个点的权值, 所以 \(\operatorname{lca}(x, y)\) 也在 \(x\) 到 \(y\) 的路径上, 我们需要扣掉 \(s_{\operatorname{lca}(x, y)}\) 和 \(s_{fa_{\operatorname{lca}(x, y)}}\) 就是扣掉了 \(\operatorname{lca}(x, y)\) 上面的而且 \(val_{\operatorname{lca}(x, y)}\) 恰好只算了一次.
但是边权下放点权则不同.
每个点的权值是"头顶上"的边放下来的, 所以路径并不包括 \(\operatorname{lca}(x, y)\), 因为 \(val_{\operatorname{lca}(x, y)}\) 是 \(\operatorname{lca}(x, y)\) 头顶上的边, 所以直接扣掉两次 \(s_{\operatorname{lca}(x, y)}\) 即可.
那么根据前缀和的思想, 我们每个点的权值线段树都应该批判的继承父亲的权值线段树, 然后去二分路径上最重要的 \(4\) 或 \(3\) 个点所对应的权值线段树就好了.
例题
双倍经验有该死的 \(\operatorname{xor}\) 上一个答案, 不这样我肯定也在线做, 但这就纯恶心人了.
就是从根开始 DFS, 所有的点的权值线段树批判的继承父亲的权值线段树, 然后去二分 \(rt_{x}\) 和 \(rt_{y}\) 和 \(rt_{\operatorname{lca}(x, y)}\) 和 \(rt_{fa_{\operatorname{lca}(x, y)}}\) 这 \(4\) 个权值线段树即可.
最后瞎说一通
为什么我们称 Algorithm 为算法?
为什么我们称 Data Structure 为人类智慧?
因为数据结构就是人类智慧的闪耀!!!