基于欧拉序的 O(n)-O(1) 静态在线 k 级祖先

source: https://arxiv.org/pdf/2005.11188 https://www.luogu.com.cn/article/ho7l34s2

在 \(\mathrm{O}(n\log n)-\mathrm{O}(1)\) 的的倍增-长剖方法中,问题一共被分为了两部分:

  1. 在 \(\mathrm{O}(n)-\mathrm{O}(1)\) 的复杂度内处理出每条长链及其往上常数倍链长的信息。
  2. 在 \(\mathrm{O}(n\log n)-\mathrm{O}(1)\) 的复杂度内把询问的 \((u, k)\) 转为另一对 \((v, k')\) 使得 \((v, k')\) 的答案可以查第 1 步的表得到。
    这个算法优化了第 2 步的处理。

倍增的问题是对于 \((u, k)\) 的询问,强制的要把 \(u\) 往上跳 \(2^{\lfloor \log k\rfloor}\) 步,这确实保证了剩下的距离(\(< 2^{\lfloor \log k\rfloor}\))与该长链链长(\(\ge 2^{\lfloor \log k \rfloor}\))是常数倍的。但实际上只要 \((v, k')\) 满足 \(v\) 是 \(u\) 的祖先,且 \(v\) 所在长链链长与 \(k\) 呈常数倍即可。

特判 \(dep_{u} \le k\)(认为根的深度为 \(1\),该情况不合法)和 \(k\le 1\) 的情况。

当 \(k \ge 2\) 时记 \(p = 2^{\lfloor \log k\rfloor - 1}\),有 \(2p\le k < 4p\),尝试让任意 \((u, k)\) 都能在 \(\mathrm{O}(n)-\mathrm{O}(1)\) 的复杂度内找到 \((v, k')\) 满足 \(v\) 是 \(u\) 的祖先且 \(v\) 所在的长链长度 \(\ge p\)。这样只需要每个长链维护往上 \(4\) 倍链长的信息就可以做到 \(\mathrm{O}(n)-\mathrm{O}(1)\) 了。

求出欧拉序 \(e_{1\sim 2n - 1}\)(此处欧拉序的定义为在 dfs 到该点时加入一次,每递归完一个儿子再加入一次)。欧拉序满足 \(dis(e_{i}, e_{i + 1}) = 1\),故有 \(dis(e_{i}, e_{j})\le |i - j|\)。

找到 \(u\) 的任意一次出现,记 \(e_{i} = u\)。则对于 \(|i - j|\le p\),有 \(dis(u, e_{j})\le |i - j|\le p\),那么这些 \(e_{j}\) 的 \(p\) 级祖先一定都是 \(u\) 的祖先(最坏情况下也只是 \(u\) 往下连走 \(p\) 步,\(p\) 级祖先向上走 \(p\) 步回到 \(u\)),且这些 \(p\) 级祖先所在的链长一定 \(\ge p\)。
所以,满足 \(|i - j|\le p\) 的 \(e_{j}\) 的 \(p\) 级祖先 \(v\) 都可以作为 \(u\) 的转移点。
(关于存在性的补充:因为 \(dis(\operatorname{root}, u) \ge k\),这说明 \(i - 1\ge dis(e_{1}, u) = dis(\operatorname{root}, u)\ge k\),同理有 \(2n - 1 - i\ge k\),而 \(k\ge 2p > p\),所以所有 \(j\) 都可以取到。且因为 \(dis(\operatorname{root}, u)\ge k\),\(e_{j}\) 的 \(p\) 级祖先最极端的情况就是向上走 \(2p\le k\) 步,所以满足 \(|i - j|\le p\) 的所有 \(e_{j}\) 的 \(p\) 级祖先也都存在。)

不过为了做到 \(\mathrm{O}(n)-\mathrm{O}(1)\),肯定不能对每个 \(j\) 维护每个 \(p\) 级祖先,这就是倍增。于是考虑让每个 \(j\) 只维护一个固定的 \(d(j)\) 级祖先,需要满足的就是对于任意 \(2\) 的次幂 \(p\),任意 \(2p + 1\) 的区间里都应存在 \(j\) 满足 \(d(j) = p\)。
于是可以构造出 \(d(j) = \operatorname{lowbit}(j)\),因为对于 \(2p + 1\) 的区间里至少有 \(2\) 个下标是 \(p\) 的倍数,那也就至少 \(1\) 个下标满足 \(\operatorname{lowbit}(j) = p\)。

总结一下:

  • 对欧拉序 \(e_{1\sim 2n - 1}\) 的每个 \(e_{i}\) 维护其 \(\operatorname{lowbit}(i)\) 级祖先 \(f_{i}\),可以离线在 dfs 时用栈处理。
  • 对每个长链维护该长链及其向上 \(4\) 倍链长的信息。
  • 对于询问 \((u, k)\),找到其在欧拉序任意一次出现位置 \(e_{i} = u\)。
  • 找到 \(|i - j|\le p\) 满足 \(\operatorname{lowbit}(j) = p\),把 \((u, k)\) 转成 \((f_{j}, k')\)。
  • \((f_{j}, k')\) 满足 \(f_{j}\) 是 \(u\) 祖先,且所在链长 \(\ge p\),\(k' \le k < 4p\),可以直接查表。

下附实现(题目 Luogu P5903 【模板】树上 K 级祖先),注意变量名和上文并不一致:

#include <bits/stdc++.h>
using uint = unsigned int;
uint s;
inline uint get(uint x) {
	x ^= x << 13;
	x ^= x >> 17;
	x ^= x << 5;
	return s = x; 
}
constexpr int N = 5e5 + 10, NN = 1e6 + 10;
int n, fa[N]; std::vector<int> son[N];
int dep[N], top[N], ht[N], hvson[N], fi[N], fal[NN], L, stk[N], tot;
inline void record(int u) {
  L++; int low = L & -L;
  fal[L] = tot >= low ? stk[tot - low] : 0;
}
void dfs1(int u) {
  stk[++tot] = u, dep[u] = tot, ht[u] = 1;
  record(u), fi[u] = L;
  for (int v : son[u]) {
    dfs1(v), record(u);
    if (ht[v] + 1 > ht[u])
      ht[u] = ht[v] + 1, hvson[u] = v;
  }
  tot--;
}
std::vector<int> up[N], down[N];
inline int query(int u, int k) {
  if (k >= dep[u]) return 0;
  if (k <= 1) return k ? fa[u] : u;
  int lg = std::__lg(k);
  int w = fi[u] - (fi[u] & ((1 << lg) - 1)) + (1 << lg - 1);
  int t = top[fal[w]], d = k - (dep[u] - dep[t]);
  return d >= 0 ? up[t][d] : down[t][-d];
}
int main() {
  int q;
  scanf("%d%d%u", &n, &q, &s);
  int rt = 0;
  for (int i = 1; i <= n; i++) {
    scanf("%d", &fa[i]);
    if (fa[i]) son[fa[i]].push_back(i);
    else rt = i;
  }
  dfs1(rt);
  for (int u = 1; u <= n; u++)
    if (u == rt || hvson[fa[u]] != u) {
      down[u].resize(ht[u]);
      for (int i = 0, v = u; i < ht[u]; i++)
        down[u][i] = v, top[v] = u, v = hvson[v];
      up[u].resize(std::min(4 * ht[u], dep[u]));
      for (int i = 0, v = u; i < up[u].size(); i++)
        up[u][i] = v, v = fa[v];
    }
  int lst = 0; long long ans = 0;
  for (int i = 1; i <= q; i++) {
    int u = (get(s) ^ lst) % n + 1, k = (get(s) ^ lst) % dep[u];
    lst = query(u, k), ans ^= 1ll * lst * i; 
  }
  printf("%lld\n", ans);
  return 0;
}

还有一份在 gpt 帮助下卡了点常的代码(在最优解第二页):

#include <bits/stdc++.h>
using uint = unsigned int;
uint s;
inline uint get() {
  s ^= s << 13, s ^= s >> 17, s ^= s << 5;
  return s; 
}
constexpr int N = 5e5 + 10;
int n, fa[N]; std::vector<int> son[N];
int dep[N], top[N], ht[N], hvson[N];
int fi[N], fal[N * 2], L, stk[N], tot;
inline void record(int u) {
  L++; int low = L & -L;
  fal[L] = tot >= low ? stk[tot - low] : 0;
}
void dfs1(int u) {
  stk[++tot] = u, dep[u] = tot, ht[u] = 1;
  record(u), fi[u] = L;
  for (int v : son[u]) {
    dfs1(v), record(u);
    if (ht[v] + 1 > ht[u])
      ht[u] = ht[v] + 1, hvson[u] = v;
  }
  tot--;
}
int id[N * 5], bas[N], idn;
inline int query(int u, int k) {
  if (k <= 1) return k ? fa[u] : u;
  int b = 30 - __builtin_clz((unsigned)k);
  int w = ((fi[u] >> b) | 1) << b;
  return id[fal[w] + dep[u] - k];
}
int main() {
  int q;
  scanf("%d%d%u", &n, &q, &s);
  int rt = 0;
  for (int i = 1; i <= n; i++) {
    scanf("%d", &fa[i]);
    if (fa[i]) son[fa[i]].push_back(i);
    else rt = i;
  }
  dfs1(rt);
  for (int u = 1; u <= n; u++)
    if (u == rt || hvson[fa[u]] != u) {
      int len = std::min(4 * ht[u], dep[u]);
      int *arr = id + idn + len, piv = idn + len - dep[u];
      for (int v = u, i = 0; i < len; i++, v = fa[v])
        arr[-i] = v;
      for (int v = u, i = 0; v; i++, v = hvson[v])
        arr[i] = v, bas[v] = piv;
      idn += len + ht[u];
    }
  for (int i = 1; i <= L; i++) fal[i] = bas[fal[i]];
  int lst = 0; long long ans = 0;
  for (int i = 1; i <= q; i++) {
    int u = (get() ^ lst) % n + 1, k = (get() ^ lst) % dep[u];
    lst = query(u, k), ans ^= 1ll * lst * i; 
  }
  printf("%lld\n", ans);
  return 0;
}

代码还可以继续卡常,但是我看了一眼最优解第一页都是 \(\mathrm{O}(\log \log n) - \mathrm{O}(\log \log n)\) 的倍增重剖感觉世界观被重塑了,所以就这样了。

posted @ 2026-10-16 18:24  rizynvu  阅读(12)  评论(0)    收藏  举报