Description给定一棵包含 N N N 个顶点的树。 在这棵树上进行一个“捉迷藏”游戏。游戏由若干回合组成。
在每一回合中,有两名玩家:
在回合开始时,狮子和斑马分别站在两个不同的顶点上。狮子始终知道斑马的位置,并且以 每秒 1 条边 的速度追赶。斑马不知道狮子的位置,但始终知道 与狮子的距离 。基于这些信息,斑马在每一秒可以做出如下两种选择之一:
用 1 秒钟走到任意相邻顶点; 停留在当前顶点 1 秒。 当狮子与斑马在同一个顶点相遇,或者在同一条边上相遇时,本回合结束。特别地,如果他们从相邻的两个顶点同时沿同一条边相向而行,那么会在出发后的 0.5 秒 相遇。
斑马的目标是:在所有可能的狮子初始位置 中,选择自己的行动方式,使得最短相遇时间 尽可能大化。
你被给定 Q Q Q 个回合。在第 i i i 个回合中:
斑马从顶点 v i v_i v i 出发; 狮子与斑马的初始距离为 d i d_i d i 。 请你求出:在双方都采取最优策略时,第 i i i 个回合的最短结束时间 。
n , q ≤ 1 0 5 n,q\leq 10^5 n , q ≤ 1 0 5 。
Solution首先这题如果知道狮子的位置,则走法一定是往狮子的方向走,直到距离不超过 2 2 2 ,再反向走一个最长的路径跑路。
如果不知道狮子的位置,则每次就只能猜一个方向走,这个方向可能会很优也可能会很劣,考虑怎么最优化这个东西。
然后有个感觉是如果一次试探失败,即这个儿子 v v v 的方向不是狮子的方向,后面不会回头。证明就考虑下一步如果回头,且最终还会回到 v v v 的子树一定不优。如果最终不回 v v v 的子树,则假设下一步会走到 w w w ,那么一开始就走 w w w 一定更优。(感觉证明很感性,不保证严谨性)
所以策略一定是先一直往一个子树走,再根据情况往父亲方向跑路。
设 f u f_u f u 表示从根走到 u u u ,一路上都试探成功,且狮子在 u u u 子树里后面的最大步数;d u d_u d u 表示从根走到 u u u 时与狮子的距离;d i s u dis_u d i s u 表示 u u u 子树到 u u u 的最长距离。则有转移:
f u = max { max v ∈ s o n ( u ) { min ( f v + 1 , d u + d i s v + 1 ) } , d i s f a ∖ u + d u + 1 } 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\} f u = max { v ∈ s o n ( u ) max { min ( f v + 1 , d u + d i s v + 1 ) } , d i s f a ∖ u + d u + 1 }
这个转移的含义是讨论第一步走的方向,如果是往儿子 v v v 方向走且狮子在 v v v 的子树里,贡献为 f v + 1 f_v+1 f v + 1 ;狮子不在 v v v 的子树里,则由于不会走回头路,后面一定是往子树的最长路径走,也就是 d u + d i s v + 1 d_u+dis_v+1 d u + d i s v + 1 ,那么往 v v v 走的贡献即为两者取 min。
如果往父亲走,则开始跑路,贡献为 d i s f a ∖ u + d u + 1 dis_{fa\setminus u}+d_u+1 d i s f a ∖ u + d u + 1 ,就是走去掉 u u u 子树的最长路径。
这么做是 O ( n q ) O(nq) O ( n q ) 的,需要优化。
注意到往儿子走的转移 ≤ max v ∈ s o n ( u ) { min ( f v + 1 , d u + d i s v + 1 ) } ≤ d u + max v ∈ s o n ( u ) { d u + d i s v + 1 } = d u + d i s u \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 ≤ v ∈ s o n ( u ) max { min ( f v + 1 , d u + d i s v + 1 ) } ≤ d u + v ∈ s o n ( u ) max { d u + d i s v + 1 } = d u + d i s u ,则如果 u u u 不是父亲 f a fa f a 的长儿子,u u u 往儿子方向走不如走回头路。这说明能往儿子走就一定会往长儿子方向走。
所以策略更新为每次往长儿子走,如果狮子不在长儿子子树里就一直走长儿子最优,否则继续走,直到与狮子距离不超过 2 2 2 时开始跑路。
容易发现这个策略会在狮子在根所在最长链上最劣,所以只需要考虑这种情况。
对于询问,容易发现根所在最长链的另一端一定是整棵树直径的一端点,预处理出直径的两端点。设 p = ⌊ d − 1 2 ⌋ p=\left\lfloor\frac{d-1}{2}\right\rfloor p = ⌊ 2 d − 1 ⌋ ,则走法一定是往这个端点先走 p p p 步,再走反方向,且要求这么走的下一步子树里不能有狮子,搞个 set 维护即可。
时间复杂度:O ( ( n + q ) log n ) O((n+q)\log n) O ( ( n + q ) log n ) 。
Code1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 #include <bits/stdc++.h> 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 ; while (T--) dickdreamer (); return 0 ; }