1 条题解
-
0
自动搬运
来自洛谷,原作者为

CarroT1212
chrono::system_clock::now().time_since_epoch().count()搬运于
2025-08-24 23:08:02,当前版本为作者最后更新于2025-02-06 18:59:09,作者可能在搬运后再次修改,您可在原文处查看最新版自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多
以下是正文
给我调秃了啊淦。细节真的多。本题解会尽量把我感到比较疑惑的地方,尤其是 DP 的优化细节写得详细一点。
令题面里原有的 变为 ,然后先手叫 A,后手叫 B。下文的“起点”“终点”全部是对于一个人的单次操作而言的。
先把 拎起来作根。A 不能走走过的路,所以 A 每次都只能往更深的地方走。然后 B 每次可以移动到大小为 的邻域(最后一次必须移到子树内)。
第一档分是 ,考虑它有什么性质。发现这时主动权完全在 B 手上,A 每次跳完,B 就一定可以跳到目前位置和点 的 LCA 上,跳着跳着就把 A 卡死了,所以答案就是 。那么现在只用考虑 的情况。
然后这里我们选择先二分一个答案 使求解的东西变得简单一点。(看完了下面的内容你可以想想如果这里不二分的话会发生什么。实际上 的 DP 还是可以做的,但是后续再往下砍复杂度似乎会变得非常困难。反正我自己做的时候就是推完 没想起来二分然后寄了。)
显然每次 A 行动的起点只会越来越深,这支持我们设计某种 DP。设 表示 A 从 点开始走,最后终点会不会 。 表示 B 从 点开始走最后终点会不会 。理论上按深度从大到小去更新就没有什么问题。进一步考虑这个 DP 的细节。
求 :预处理出每个点的所有 级儿子。
- 如果 没有 级儿子,那么 就是最后一次 B 操作的结果,即 子树中离 最近的 的点是否距离不超过 。容易维护。
- 否则,每次求 时把它的 级儿子 全部枚举一遍,有 。容易证明这个过程中每个 只会被用到至多一次,且由于 所以 要用到的信息肯定已经更新过了。所以我们可以在遍历的过程中把每个 都求出来。
求 :按理来说就是看 大小为 B 的邻域里的 有没有是 的就好了。但是这里会出现本题最恶心的一个细节:
如果 B 的一次操作终点是起点的祖先,那么 A 下一次操作就不能顺着这个起点方向的儿子再走下去,因为已经被走过了。
所以我们未必能直接拿邻域里的 去更新 。更具体地,当 是 祖先时,我们要去掉 中走回了 所在子树的情况。
这里精细实现一下已经可以获得一个 的 DP,不二分就没有 。然后考虑优化。
实际上唯一能超时限的只有 的更新。因为已经二分过答案了,这里 的更新可以当成:
- 对于不是 祖先的 ,求离 最近的满足 的 的距离是否 。
- 对于是 祖先的 ,抠掉往回走的情况再判。
这样就跟邻域没有什么关系了。考虑这样要怎么做。时刻记住我们的 DP 是在按深度从大到小更新 ,设现在的深度是 ,而现在是在确定所有深度为 的 用于转移 。
考虑某种做法:我们不倾向于对每个 去寻找可能的 进行转移,而是在每个 处去尝试更新所有 的答案。
你注意到这里所有和 距离不超过 的点 的深度都不小于 。而 ,说明这些点 的 已经被我们更新过了,我们可以放心地用。所以我们可以在每次到一个新的 的时候,把所有深度为 的点 计入考虑范围。
当一个点 被计入考虑范围时,我们需要做这些事:
- 设点 子树内最浅的 的 的深度为 。过程中容易维护。
- 处理第一种转移:
- 我们希望用 更新: 的兄弟子树里面的 在求 时用到的最小距离。因为 深度 ,这个时候可能被 影响到的 肯定还没有被确定取值。考虑怎么做。
- 整一棵在 dfn 上支持区间取 min,单点查询的线段树。可以简单地标记永久化实现。
- 对于 的所有兄弟节点,我们在线段树上将这些兄弟的子树内所有的值和 取 。
- 这样在以后确定 时,只要把 在线段树上对应位置的值拿出来加上 ,实际这个式子就是求树上距离,我们求得了 和子树外的 的最近距离。
- 对于 子树内的,直接用 即可。
- 处理第二种转移:
- 我们希望知道,如果 A 走进了 的某棵子树,然后 B 又从里面走出来,刚好停在了 ,再让 A 往另一个方向操作,这时是否最终会走到 的点。
- 把 的子树分为两种:A 从 开始还能走进去的;走不进去的。这里请自行想办法统计它们的一些信息。
- 然后枚举 B 是从哪棵子树里冒出来的,我们需要知道从这棵子树里出来的话,最后终点会不会 。考虑剩下的子树。
- 如果剩下的还有 A 能走进去的,那么所有能走进去的子树都要满足这样走进去的终点 。
- 如果剩下的 A 都走不进去,那说明 A 已经没得操作了,现在是 B 的最后一步。看一下剩下子树里是否存在一个距离 不超过 的 的点,或者 自己 也行。
- 如果满足条件,就用前面那棵线段树,把整棵子树全部赋为 。相当于里面还没确定的 全部变成 。
那么求 的时候只要把 和线段树里面的东西 拿出来取个 就大功告成了。按上面说的更新 上的所有 啥的就好。
至此做完了。。我这代码目前是 qoj 的第三短解,没卡常大概 2.5s,性价比挺高。
我自己想的进度是写对了无二分的 DP 和链。感觉最厉害的部分还是优化 转移那里想到去反向更新,然后进一步想到用逐层更新+线段树去维护这个过程。可能邻域相关套路我还是没见过多少。剩下的我认为思路其实比较自然,只要认真顺着这个做法去分析可能的情况就可以了。
而分析错了就会感受到调到头秃的酸爽。#include <bits/stdc++.h> #define pb push_back #define fi first #define se second using namespace std; bool MEM; using ll=long long; using ld=long double; using pii=pair<int,int>; using pll=pair<ll,ll>; const int I=1e9,N=3e5+7; const ll J=1e18; int n,rt,a,b; int dep[N],fa[N],dfn[N],cnn,dfm[N],co[N],st[N],tp; vector<int> e[N],e1[N],to[N],de[N]; void dfs(int p,int f) { dfn[p]=++cnn,co[cnn]=p,dep[p]=dep[f]+1,de[dep[p]].pb(p); fa[p]=f,st[++tp]=p; if (tp>a) to[st[tp-a]].pb(p); for (int i:e[p]) if (i!=f) e1[p].pb(i),dfs(i,p); tp--,dfm[p]=cnn; } struct sgt { int t[N<<2]; void ini() { memset(t,60,sizeof(t)); } void add(int x,int y,int z,int p=1,int l=1,int r=n) { if (x>y) return; if (x<=l&&r<=y) return t[p]=min(t[p],z),void(); int mid=(l+r)>>1; if (x<=mid) add(x,y,z,p<<1,l,mid); if (y>mid) add(x,y,z,p<<1|1,mid+1,r); } int que(int x,int p=1,int l=1,int r=n) { if (l==r) return t[p]; int mid=(l+r)>>1; if (x<=mid) return min(t[p],que(x,p<<1,l,mid)); else return min(t[p],que(x,p<<1|1,mid+1,r)); } } T; int mnn[N],f[N],g[N],t[N],tmp[N],is[N]; bool chk(int mid) { T.ini(); for (int i=1;i<=n;i++) t[i]=1e8,mnn[i]=i<=mid?dep[i]:I; for (int d=n;d;d--) { if (d+a-b<=n) { for (int p:de[d+a-b]) { if (f[p]) t[p]=dep[p]; else { t[p]=I; for (int i:e1[p]) t[p]=min(t[p],t[i]); } T.add(dfn[fa[p]]+1,dfn[p]-1,t[p]-dep[fa[p]]*2); T.add(dfm[p]+1,dfm[fa[p]],t[p]-dep[fa[p]]*2); // 以上是第一种转移 int len=e1[p].size(),sum=0,mus=0,le=0; for (int i=0,j=0;i<len;i++) { tmp[i]=1,is[i]=1; while (j<to[p].size()&&dfn[to[p][j]]<=dfm[e1[p][i]]) tmp[i]&=g[to[p][j++]],is[i]=0; if (is[i]) tmp[i]=mnn[e1[p][i]]-dep[p]<=b,mus+=tmp[i]; else sum+=tmp[i],le++; } for (int i=0;i<len;i++) { int w=0; if (!is[i]) { if (sum-tmp[i]!=le-1); else if (le-1) w=1; else w|=mus||p<=mid; } else { if (sum!=le); else if (le) w=1; else w|=mus-tmp[i]||p<=mid; } if (w) T.add(dfn[e1[p][i]],dfm[e1[p][i]],-I); } // 以上是第二种转移 } } for (int p:de[d]) { for (int i:e1[p]) mnn[p]=min(mnn[p],mnn[i]); if (to[p].size()) { f[p]=1; for (int u:to[p]) { int w=min(t[u]-dep[u],dep[u]+T.que(dfn[u])); if (w<=b) g[u]=1; else f[p]=0,g[u]=0; } } else f[p]=mnn[p]-dep[p]<=b; } } return f[rt]; } void mian() { scanf("%d%d%d%d",&n,&rt,&a,&b),cnn=0; for (int i=1;i<=n;i++) e[i].clear(),e1[i].clear(),to[i].clear(),de[i].clear(); for (int i=1,x,y;i<n;i++) scanf("%d%d",&x,&y),e[x].pb(y),e[y].pb(x); if (b>=a) return cout<<"1\n",void(); dfs(rt,0); int l=1,r=n,mid,res=0; while (l<=r) { mid=(l+r)>>1; if (chk(mid)) res=mid,r=mid-1; else l=mid+1; } cout<<res<<"\n"; } bool ORY; int main() { // while (1) // int t; for (scanf("%d",&t);t--;) mian(); cerr<<"\n"<<abs(&MEM-&ORY)/1048576<<"MB "<<clock()<<"ms\n"; return 0; }
- 1
信息
- ID
- 11068
- 时间
- 6000ms
- 内存
- 1024MiB
- 难度
- 7
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者