「CEOI2017」Mousetrap 题解
一般来说我们会考虑将树上的特殊点(比如重心之类的)当作根,这样的话问题就会好考虑一些。在这个题中,权衡老鼠一开始的点和陷阱房,显然陷阱房不变会好更好处理(毕竟起点没什么高妙性质)。实际上这个想法在样例中有迹可循,你可以发现陷阱房在 \(1\)。
并且,不难发现,假设某个时刻起,老鼠开始往下走了,那么这个老鼠的生命就差不多得了。因为就算管理员不操作,老鼠走到叶子了,也动不了了。此时管理员只需要保证这个点到根的路径上的所有点,只有向上的走法。用样例举个例子:假设老鼠走了 \(4 \to 7\) 的边,先封住 \(7 \to 10\) 的边,这样老鼠就动不了了;然后封住 \(4 \to 6,2 \to 3\),最后打开 \(4 \to 7\),老鼠只能从 \(7\) 向上走到 \(4,2,1\),进入陷阱房。
所以,不难发现老鼠被困住之后,陷阱房到老鼠初始点路径上老鼠没走过的点需要封住多余的子树,不然会产生额外的贡献(走进去之后还要走出来,并且里面的操作未知。但是这个过程完全可以规避),每个点 \(u\) 的贡献是 \(deg_u - 2\)。这显然是一个前缀和的形式,可以把这条链先爬出来,然后做个后缀和。
那么不难想到,整个过程是管理员一边堵住某些走廊,老鼠一边走。最终老鼠会在 \(m \to t\) 路径上的某一个点开始往下走到某一棵子树里面,然后遇到老鼠走不动的情况,剩下的工作在上面就已经说干净了。
但是不仅是上面的说法有漏洞(也就是,老鼠走入到子树,子树里面有些边还必须在老鼠困住无法操作时封掉),并且在子树中的过程中也没有想象中的这么好考虑——如何解决?
考虑 dp。定义 \(dp_u\) 为,老鼠刚好到 \(u\),并且 \(u\) 的子树的边完好;老鼠在里面随意操作,管理员让老鼠最终被困回 \(u\) 的最少操作是多少?显然 \(u\) 如果是陷阱房到老鼠出发点路径上的某个点,\(dp_u\) 没有意义(老鼠不可能走上去之后再走下来,这样显然不优,并且如果更优是老鼠的失误,管理员也不可能打开 \(u \to fa_u\) 这条边)
如何求得 \(dp_u\)?当老鼠走到 \(u\) 的时候,老鼠肯定想往 \(son_u\) 中 \(dp\) 值最大的 \(p\)。此时我们将 \(u \to p\) 的边封住,这样老鼠就只能退而求其次,选 \(son_u\) 中 \(dp\) 值第二大的 \(q\)……然后最终老鼠会被困在子树中的某一个点,我们令其回到 \(q\) 之后,先把 \(u\) 子树中其他的还没被封过的边封住,然后再把 \(u \to q\) 打开,这样老鼠最终会被困在 \(u\)。转移式可以阅读这一段话简单得出。
这样的话,我们求出所有结点的 \(dp\) 值。接下来就是老鼠选择一个子树往下钻的时候了。注意到,如果老鼠能够向下进入一个结点 \(u\),那么老鼠如果想向下钻入结点 \(v\) 使得 \(dp_v \leq dp_u\) 也是可以的,并且这属于老鼠的失误,老鼠肯定不会这么决策。但是这启发我们这个题可以用二分找到那个最大的 \(dp\) 值。并且这样就可以直接二分答案了:管理员先封住一条边,然后老鼠向上跳一条边;或者老鼠检测发现现在有一条边通向的点的 dp 值比当前二分的值要大,那么这个答案不合法。
具体实现可以研究代码,很清楚了。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
char buf[1<<18],*p1=buf,*p2=buf;
#define getchar() (p1==p2 && (p2=(p1=buf)+fread(buf,1,1<<18,stdin),p1==p2)?EOF:*p1++)
int read()
{
int x=0;
char c=getchar();
while(c<'0' || c>'9') c=getchar();
while(c>='0' && c<='9') x=(x<<1)+(x<<3)+(c^'0'),c=getchar();
return x;
}
void write(int x)
{
if(x>9) write(x/10);
putchar(x%10+'0');
}
vector<int> G[1000005];
int n,s,t,lnk[1000005],cnt,fa[1000005],dp[1000005],brc[1000005];
#define Deg(x) (int(G[x].size()))
void dfs(int now,int pre)
{
fa[now]=pre;
int maxn=0,maxc=0;
for(auto st:G[now])
{
if(st==pre) continue;
dfs(st,now);
if(dp[st]>=maxn) maxc=maxn,maxn=dp[st];
else if(dp[st]>=maxc) maxc=dp[st];
}
if(now!=s) dp[now]=maxc+Deg(now)-1;
}
bool check(int lmt)
{
int sum=0;
for(int i=1;i<=cnt;++i)
{
int p=lnk[i],cks=0;
for(auto st:G[p])
{
if(st==lnk[i-1] || st==lnk[i+1] || st==s) continue;
if(dp[st]+brc[i]+1-(i!=1)>lmt) ++cks;
}
sum+=cks,lmt-=cks;
if(sum>i || lmt<0) return false;
}
return true;
}
int main(){
n=read(),s=read(),t=read();
for(int i=1,u,v;i<n;++i) u=read(),G[u].push_back(v=read()),G[v].push_back(u);
dfs(s,0);
int p=t;
while(p^s) p=fa[lnk[++cnt]=p];
for(int i=cnt;i;--i) brc[i]=brc[i+1]+Deg(lnk[i])-2;
int l=0,r=2*n,ans=2*n;
while(l<=r)
{
int mid=(l+r)>>1;
if(check(mid)) r=(ans=mid)-1;
else l=mid+1;
}
write(ans);
return 0;
}
不是很会打标签,因为前面整理的思路看起来非常的 trivial 但是 dp 的过程非常的 naive……不知道怎么说好。