[关闭]
@Falsyta 2017-11-02T16:13:27.000000Z 字数 3285 阅读 1171

严格 log n Link-Cut Tree 正篇

未分类


为了保持严格 的复杂度,我们首先要舍弃原来简单暴力的 Expose 方法,然后再舍弃均摊复杂度的 splay ,换上两个严格 的策略。第一个问题是如何 Expose ,我们用已知的方法——轻重边树链剖分来处理。我们每次 expose(x) 的时候将路径上的轻边变为实边,进行操作过后再调用 conceal(x) 来把路径上的轻边恢复为虚边,由于一条路径上只有 条轻边,因此理想的复杂度应该可以做到

注:本文的边有四种:实边(solid),虚边(dashed),重边(heavy),轻边(light)(知道 link-cut tree 和树链剖分的大家应该都熟悉这些概念了),实/虚 是 link-cut tree 中的概念,是我们维护的对象,重/轻 是轻重边树链剖分中的概念,是我们分析的工具。
注2:为了区分树上的节点和 LCT 节点,树上的节点用 表示, LCT 中的节点用 表示(代码中除外)。

处理轻边

若一条边 满足 ,则称其为轻边(这一定义和轻重边树链剖分中是基本一致的)。为了判定轻边我们需要一个点的 ,而 link-cut tree 有一个重要的操作就是改变树的根,为了支持这个操作,我们定义 为:

总的来说就是 的虚儿子的子树大小之和加一,注意在把当前的根 切换到新根 的时候我们只需要先 expose(u) ,然后再把根换到 上,此时 上的点的 并不会改变,这样就可以维护 了。

通过 我们可以求出
于是对于有实儿子 的点 定义 ,若 说明 是轻边。

expose(x) 的实现和之前差不多,然后 conceal(x) 的时候只需要在实链中不断寻找最大的 ,如果不小于零就标为轻边并继续即可。于是我们需要支持链查询区间最大的

为了支持查询,我们对一个 LCT 节点 (所代表的是树上的节点 ) 维护:
- 子树的
(和 略有区别)

为了支持区间翻转还要维护 翻转后的版本 ,略去。

即可查询在平衡树的根节点查询到子树最大的 。(这里我的实现和论文有区别,如果有错误求指正)

除了寻找轻边以外,我们在断掉轻边的时候还需要找到是否有需要重新连接的重边,为了找到这些重边,我们可以用平衡树维护和点 相邻的所有实链(自己所在的那条除外)的 ,取出其中权值最大的一条即可。

其他部分的实现

所有的平衡树都使用之前介绍过的 Biased Search Tree ,取

为了避免麻烦用了C++的bind……凑合看个意思就行了(然而有的变量可能打错了qaq,如果看到的话求指正)

  1. void update(Node u) {
  2. //update the information from root to u
  3. }
  4. Node splice(Node v) {
  5. int x = fa[v.head];
  6. Node p, q, u = lct[x]; int a, b;
  7. bind(p, q, a, b) = split(u);
  8. u.wt -= v.wtsum; update(u);
  9. del(v, x);
  10. if (q != null) {
  11. fa[q.head] = x; fa[q.head] = b;
  12. u.wt += q.wtsum; update(u);
  13. ins(q, x);
  14. }
  15. u = concat(u, v, fc[v.head]);
  16. return p == null ? u : concat(p, u, a);
  17. }
  18. Node expose(int x) {
  19. Node p, q;
  20. int a, b;
  21. Node u = lct[x];
  22. bind(p, q, a, b) = split(x);
  23. if (q != null) {
  24. fa[q.head] = x; fc[q.head] = b;
  25. u.wt += q.wtsum; update(u);
  26. ins(q, x);
  27. }
  28. u = p == null ? u : concat(p, u, a);
  29. while (fa[u.head] != 0) u = splice(u);
  30. return p;
  31. }
  32. void slice(Node p) {
  33. int x = getLightEdge(u);
  34. Node q, u = lct[x]; int a, b;
  35. bind(p, q, a, b) = split(u);
  36. p = p == null ? u : concat(p, u, a);
  37. fa[q.head] = x; fc[q.head] = b;
  38. u.wt += q.wtsum; update(u);
  39. Node v = getHeavySon(x);
  40. if (2 * v.wtsum > u.wt) {
  41. u.wt -= v.wtsum; update(u);
  42. del(v, x);
  43. concat(p, v, fc[v.head]);
  44. }
  45. ins(q, x);
  46. return q;
  47. }
  48. void conceal(Node p) {
  49. while (getLightEdge(p) != null) {
  50. p = slice(p);
  51. }
  52. int x = u.tail; Node u = lct[x];
  53. Node v = getHeavySon(x);
  54. if (v != null && 2 * v.wtsum > p.wt) {
  55. bind(p, q, a, b) = split(x); //q == null
  56. u.wt -= v.wtsum; update(u);
  57. del(v, x);
  58. u = concat(u, v, fc[v.head]);
  59. if (p != null) concat(p, u, x);
  60. }
  61. }

复杂度证明

我觉得 的 bound 应该不需要证明了,对 条轻边进行了 次操作。

splice: split(u) 会产生 的代价( 一开始所在的实链),由于在一次操作后 ,所以总的复杂度是 的;concat(u,v) 和 concat(p,u) 会产生 (由于 是轻边所以 ),之后分析同 split ; ins 的复杂度由于 q.head 是 的重儿子所以 ; del 的复杂度是 ,分析同前。

expose: 没什么可分析的,只有 次操作。

slice: getLightEdge 和 split 的复杂度都是 ,一起分析,由于 的实儿子是它的轻儿子, ,因此复杂度就是 ,之后递归到 ,总复杂度就是 的;两次 concat 的复杂度都是 的,和 split 同理; del 的复杂度由于 的重儿子所以 ; ins 的复杂度是 ,分析同前。

conceal: 同 expose 。

综上,由于各处复杂度都是严格的,这里描述的算法的时间复杂度是严格 的。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注