【题解】P4654 [CEOI2017] Mousetrap
imsaileach · · 题解
前言
模拟赛之后被胁迫上去讲这题,没怎么准备,然后就在几个省的 OIer 面前当小丑。。倒是把我自己讲得很明白,但感觉对其他人不是很负责任,就来赎罪一下。。
更好的阅读体验。
题意
题目链接。
分析
-
以
t 为根,我们的目的是让老鼠走到根的操作数最小。 -
观察老鼠的动向,显然老鼠只要一往下走,那么除非管理员将它上方的道路擦干净,否则它就只能继续往下走。当然,老鼠可能也会往上走(待会儿再考虑)。
-
若老鼠已经往下走,但未走到叶子节点,那么我们不用立刻将其往上的一条路的分支堵住,因为当老鼠自己到叶子节点之后就动不了了,这时我们再把从这个叶子节点往上直到根的路径上所有的分支(不包括根的分支)全部堵上,最后再一一擦干净老鼠的边,使其被迫走向根节点。如果不堵分支,那么老鼠走入分支消耗的操作数一定大于等于
1 即直接堵的操作数。 -
因为是最优策略,所以每个点的最优操作数之类的信息唯一。
-
根据 4 提供的思路,考虑
f_u 表示老鼠从u 点出发往下折腾一通再被迫回到u 点在u 子树内消耗的操作数。考虑 DP,由于每次老鼠出发前管理员可以先进行一次操作。只观察u 点的儿子节点,发现如果不把向最大值(f_v)_{\max} 的路径堵住的话,老鼠一定选这条路往下走,折腾(f_v)_{\max} 次操作后,回到u 点;否则老鼠会向次大值(f_v)_{\max_2} 走,折腾(f_v)_{\max_2} 次操作后回到u 点。我们发现两种情况老鼠回到u 点后情况其他都一样(分支全部被堵住,只能往上走),所以选择堵(f_v)_{\max} 一定最优(我们还可能堵与它不相连的边,但贡献一定小于等于堵(f_v)_{\max} 的贡献,显然)。 -
考虑如何求
f_u ,发现实际上根据 3 可知管理员的固定策略,f_u 分为三部分:一、选择最大的f_v 堵住,使从u 点只能走向次大f_v ;二、在老鼠抵达叶子后,利用多出的时间堵上u 的其他分支;三、把老鼠往下走过的路径擦干净,让老鼠走回u 点。于是有转移方程:f_u=(f_v)_{\max_2}+deg_u-1 其中
deg_u-1 表示u 点的分支(度数)去掉连向father 节点的,去掉u \to v 走向次大子节点的(这个不堵),加上擦老鼠v \to u 走上来的边。再加上原本的次大子节点,就是f_u 。 -
考虑由
f_u 求出具体的操作次数。记录g_u 表示u 到t 路径上管理员需要堵的分支(不包括u 本身的分支),则可知g 的转移方程:g_u=g_{father}+\max(deg_{father} - 2, 0) 解释一下,
u 向上的需要堵的分支,等于其father 节点向上需要堵的分支加上father 节点自己的分支(度数,去掉连向father 的父节点的边,去掉连向u 的边)(特殊情况用\max 判断)(特别地,钦定deg_t=1 (实际上,从本题的数据上看,不影响))。然后我们记录一个
cnt_u 表示 从u 的父节点走入u 后 并最终结束游戏所需要的操作数。显然cnt_u 存在唯一值。知
cnt_u 的转移方程为:cnt_u=f_u+g_u+1-(father\neq m) 再解释一下,
f_u 是老鼠往下走最终回到u 点时u 子树内的操作数贡献,g_u 是管理员不得不堵的u 到t 路径上的分支,额外加的1 是擦干净father \to u 这条边的操作。最后的01 判断意思是:若父节点不是t ,则老鼠自己是从father 的另一个分支走入father 再走下u 的,老鼠已经自己弄脏了一条边,管理员就可以少堵一条。 -
然后我们会发现我们记录的值似乎没什么卵用。
-
但是先别重开。再次分析老鼠的固定策略。显然老鼠可以向上走,进入一条比直接从
u 往下走更优的分支,最大化答案。 -
考虑简化问题。考虑二分。对于一个待检查的最大操作数
T ,显然此时问题变为了:老鼠能不能使最大操作数大于T 。 -
考虑简化老鼠的策略,并分析整个游戏的固定流程分层。老鼠显然可以先往上走
k 次(k \geq 0 )。然后选择当前点的一个分支,从当前点走入这个分支,(根据 4 知)老鼠走到叶子节点,管理员堵分支,老鼠出来走入根节点t ,游戏结束。 -
考虑 11 中“从当前点走入这个分支”的隐含内容,显然走入分支后,剩下的操作数由
cnt 可以直接计算。 -
考虑二分检验时模拟过程。记
\mathrm{check}(u, p, q) 表示当前在u 点,最多还可以进行p 次操作,管理员提前保留了q 次操作,能否满足。这里需要说一下,因为两方都执行最优,所以管理员、老鼠互相可以知道对方的一定的策略。所以管理员会在每次可操作时堵上老鼠走入会使总操作次数大于p 的分支。因为管理员执行最优,且方案一定,所以管理员可以在不需要堵当前点的分支的时候提前堵后面的点,形象地说,管理员将操作数保留累积下来,在以后的点中使用。 -
考虑如何实现
\mathrm{check}(u, p, q) 。记v 为u 的任意儿子(当然,要求u 不是从v 走上来的)。首先为了使老鼠在当前点无法直接使操作数大于p ,我们要将所有cnt_v>p 的分支堵住。令这些分支的数目为x ,显然若x>p 或x>q 就返回\text{false} 。否则保留的操作数q 用去x ,同时老鼠往上走,时间+1 ,所以q 又多累积了1 ;同时最大的剩余操作数p 也用去x 。这时显然有:
\mathrm{check}(u,p,q)=\begin{cases} \mathrm{check}(father_u,p-x,q-x+1) & p \geq x , q \geq x \\ \text{false} & \textrm{otherwise} \\ \end{cases} 可以递归或循环实现。
特别地,
\mathrm{check} (t, i, j)=\text{true} 。注意记
u 是从last 走上来的,对于u 的儿子特殊判断v \neq last 。对于u=m ,last=0 。 -
所以每次检查答案
T 是否正确的返回值就是\mathrm{check}(m,T,1) 。
Code
#include <bits/stdc++.h>
using namespace std;
int n, t, m, a, b;
int fa[1000005], dg[1000005], f[1000005], g[1000005], cnt[1000005];
int firs[1000005], nex[2000005], to[2000005], tot;
void Add (int u, int v){
++ tot;
nex[tot] = firs[u];
firs[u] = tot;
to[tot] = v;
}
void init (int u, int father){
fa[u] = father;
g[u] = g[father] + max (dg[father] - 2, 0);
int max1 = 0, max2 = 0;
for (int e = firs[u];e;e = nex[e]){
int v = to[e];
if (v == father)
continue;
init (v, u);
if (max1 < f[v]){
max2 = max1;
max1 = f[v];
} else
if (max2 < f[v])
max2 = f[v];
}
f[u] = max2 + dg[u] - 1;
cnt[u] = f[u] + g[u] + 1 - (fa[u] != m);
}
bool Check (int p){
int q = 1, las = 0;
for (int u = m;u != t;u = fa[u]){
int x = 0;
for (int e = firs[u];e;e = nex[e]){
int v = to[e];
if (v == fa[u] || v == las)
continue;
if (cnt[v] > p)
++ x;
}
q -= x;
p -= x;
if (q < 0 || p < 0)
return false;
++ q;
las = u;
}
return true;
}
int main (){
scanf ("%d%d%d", &n, &t, &m);
for (int i = 1;i < n;++ i){
scanf ("%d%d", &a, &b);
Add (a, b);
Add (b, a);
++ dg[a];
++ dg[b];
}
dg[t] = 1;
init (t, 0);
int L = 0, R = n << 1;
while (L < R){
int mid = L + R >> 1;
if (Check (mid))
R = mid;
else
L = mid + 1;
}
printf ("%d\n", L);
return 0;
}