树上主席树
树上主席树
主席树, 但是维护树上路径信息.
由于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 为人类智慧?
因为数据结构就是人类智慧的闪耀!!!
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10亿数据,如何做迁移?
· 推荐几款开源且免费的 .NET MAUI 组件库
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· c# 半导体/led行业 晶圆片WaferMap实现 map图实现入门篇
· 易语言 —— 开山篇