树上主席树

树上主席树

主席树, 但是维护树上路径信息.

由于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\) 个点所对应的权值线段树就好了.

例题

VJudge SPOJ LuoGu 双倍经验

双倍经验有该死的 \(\operatorname{xor}\) 上一个答案, 不这样我肯定也在线做, 但这就纯恶心人了.

就是从根开始 DFS, 所有的点的权值线段树批判的继承父亲的权值线段树, 然后去二分 \(rt_{x}\)\(rt_{y}\)\(rt_{\operatorname{lca}(x, y)}\)\(rt_{fa_{\operatorname{lca}(x, y)}}\)\(4\) 个权值线段树即可.

最后瞎说一通

为什么我们称 Algorithm 为算法?

为什么我们称 Data Structure 为人类智慧?

因为数据结构就是人类智慧的闪耀!!!

posted @ 2024-12-06 01:25  指针神教教主Defad  阅读(13)  评论(0编辑  收藏  举报