QOJ #2550. Lion and Zebra 题解

Description

给定一棵包含 \(N\) 个顶点的树。
在这棵树上进行一个“捉迷藏”游戏。游戏由若干回合组成。

在每一回合中,有两名玩家:

  • 狮子 —— 追捕方;
  • 斑马 —— 逃跑方。

在回合开始时,狮子和斑马分别站在两个不同的顶点上。狮子始终知道斑马的位置,并且以 每秒 1 条边 的速度追赶。斑马不知道狮子的位置,但始终知道 与狮子的距离。基于这些信息,斑马在每一秒可以做出如下两种选择之一:

  • 用 1 秒钟走到任意相邻顶点;
  • 停留在当前顶点 1 秒。

当狮子与斑马在同一个顶点相遇,或者在同一条边上相遇时,本回合结束。特别地,如果他们从相邻的两个顶点同时沿同一条边相向而行,那么会在出发后的 0.5 秒 相遇。

斑马的目标是:在所有可能的狮子初始位置中,选择自己的行动方式,使得最短相遇时间尽可能大化。

你被给定 \(Q\) 个回合。在第 \(i\) 个回合中:

  • 斑马从顶点 \(v_i\) 出发;
  • 狮子与斑马的初始距离为 \(d_i\)

请你求出:在双方都采取最优策略时,第 \(i\) 个回合的最短结束时间

\(n,q\leq 10^5\)

Solution

首先这题如果知道狮子的位置,则走法一定是往狮子的方向走,直到距离不超过 \(2\),再反向走一个最长的路径跑路。

如果不知道狮子的位置,则每次就只能猜一个方向走,这个方向可能会很优也可能会很劣,考虑怎么最优化这个东西。

然后有个感觉是如果一次试探失败,即这个儿子 \(v\) 的方向不是狮子的方向,后面不会回头。证明就考虑下一步如果回头,且最终还会回到 \(v\) 的子树一定不优。如果最终不回 \(v\) 的子树,则假设下一步会走到 \(w\),那么一开始就走 \(w\) 一定更优。(感觉证明很感性,不保证严谨性)

所以策略一定是先一直往一个子树走,再根据情况往父亲方向跑路。

\(f_u\) 表示从根走到 \(u\),一路上都试探成功,且狮子在 \(u\) 子树里后面的最大步数;\(d_u\) 表示从根走到 \(u\) 时与狮子的距离;\(dis_u\) 表示 \(u\) 子树到 \(u\) 的最长距离。则有转移:

\[f_u=\max\left\{\max_{v\in son(u)}{\left\{\min(f_v+1,d_u+dis_v+1)\right\}},dis_{fa\setminus u}+d_u+1\right\} \]

这个转移的含义是讨论第一步走的方向,如果是往儿子 \(v\) 方向走且狮子在 \(v\) 的子树里,贡献为 \(f_v+1\);狮子不在 \(v\) 的子树里,则由于不会走回头路,后面一定是往子树的最长路径走,也就是 \(d_u+dis_v+1\),那么往 \(v\) 走的贡献即为两者取 min。

如果往父亲走,则开始跑路,贡献为 \(dis_{fa\setminus u}+d_u+1\),就是走去掉 \(u\) 子树的最长路径。

这么做是 \(O(nq)\) 的,需要优化。

注意到往儿子走的转移 \(\displaystyle\leq\max_{v\in son(u)}{\left\{\min(f_v+1,d_u+dis_v+1)\right\}}\leq d_u+\max_{v\in son(u)}{\left\{d_u+dis_v+1\right\}}=d_u+dis_u\),则如果 \(u\) 不是父亲 \(fa\) 的长儿子,\(u\) 往儿子方向走不如走回头路。这说明能往儿子走就一定会往长儿子方向走。

所以策略更新为每次往长儿子走,如果狮子不在长儿子子树里就一直走长儿子最优,否则继续走,直到与狮子距离不超过 \(2\) 时开始跑路。

容易发现这个策略会在狮子在根所在最长链上最劣,所以只需要考虑这种情况。

对于询问,容易发现根所在最长链的另一端一定是整棵树直径的一端点,预处理出直径的两端点。设 \(p=\left\lfloor\frac{d-1}{2}\right\rfloor\),则走法一定是往这个端点先走 \(p\) 步,再走反方向,且要求这么走的下一步子树里不能有狮子,搞个 set 维护即可。

时间复杂度:\(O((n+q)\log n)\)

Code

#include <bits/stdc++.h>

// #define int int64_t

const int kMaxN = 1e5 + 5;

int n, q;
std::vector<int> G[kMaxN];

struct Tree {
  int rt, mx[kMaxN], dep[kMaxN], fa[kMaxN][18];
  std::set<int> st[kMaxN];
  void dfs(int u, int _fa) {
    mx[u] = 0, fa[u][0] = _fa;
    for (int i = 1; i <= std::__lg(n); ++i)
      fa[u][i] = fa[fa[u][i - 1]][i - 1];
    for (auto v : G[u]) {
      if (v == _fa) continue;
      dep[v] = dep[u] + 1;
      dfs(v, u);
      mx[u] = std::max(mx[u], mx[v] + 1);
      st[u].emplace(mx[v] + 1);
    }
  }
  int getfa(int x, int len) {
    for (int i = std::__lg(n); ~i; --i)
      if (len >> i & 1)
        x = fa[x][i];
    return x;
  }
  int get(int x, int len) {
    auto it = st[x].lower_bound(len);
    if (it != st[x].begin()) return *prev(it);
    else return 0;
  }
  void prework(int _rt) { dfs(rt = _rt, 0); }
} t[2];

void dfs(int u, int fa, int *dis) {
  dis[u] = dis[fa] + 1;
  for (auto v : G[u]) {
    if (v == fa) continue;
    dfs(v, u, dis);
  }
}

std::pair<int, int> getseg() {
  static int dis[kMaxN] = {0};
  dfs(1, 0, dis);
  int s = std::max_element(dis + 1, dis + 1 + n) - dis;
  dfs(s, 0, dis);
  int t = std::max_element(dis + 1, dis + 1 + n) - dis;
  return {s, t};
}

int solve(int x, int d) {
  int o = (t[0].dep[x] < t[1].dep[x]), len = (d - 1) / 2;
  int ret = 0;
  if (len) {
    int y = t[o].getfa(x, len - 1);
    ret = t[o].mx[y] + 1;
  }
  x = t[o].getfa(x, len);
  ret = std::max(ret, t[o].get(x, d - len));
  return ret + d - len;
}

void dickdreamer() {
  std::cin >> n >> q;
  for (int i = 1; i < n; ++i) {
    int u, v;
    std::cin >> u >> v;
    G[u].emplace_back(v), G[v].emplace_back(u);
  }
  auto [a, b] = getseg();
  t[0].prework(a), t[1].prework(b);
  for (int i = 1; i <= q; ++i) {
    int x, d;
    std::cin >> x >> d;
    std::cout << solve(x, d) << '\n';
  }
}

int32_t main() {
#ifdef ORZXKR
  freopen("in.txt", "r", stdin);
  freopen("out.txt", "w", stdout);
#endif
  std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0);
  int T = 1;
  // std::cin >> T;
  while (T--) dickdreamer();
  // std::cerr << 1.0 * clock() / CLOCKS_PER_SEC << "s\n";
  return 0;
}
posted @ 2025-08-20 20:32  下蛋爷  阅读(36)  评论(0)    收藏  举报