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\) 的最长距离。则有转移:
这个转移的含义是讨论第一步走的方向,如果是往儿子 \(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;
}