[NOIP2015 提高组] 运输计划题解
题目链接:P2680 [NOIP2015 提高组] 运输计划 – 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
看了好长时间题解才终于懂的,有关lca和二分答案的题解解释的不详细,一时半会理解不过来,于是自己写一篇解释尽管解释主要在代码中,希望能对迷茫的小伙伴有帮助
解析(主要为二分答案的解析,lca只是求距离和找覆盖边时用得到,这里不多说):
由于m个运输计划是同时出发,所以所需要的时间取决于花费最长的时间,因为一个任务在y分钟内完成,那么另一个任务x(x<=y)也一定完成。
问题就转化成了求最长边最短,二分答案。
接下来,就是怎么二分了
想让时间大于答案的任务的时间减小,题目里说了,可以设一条边为虫洞,我们就找这个任务里哪条(些)边可以设为虫洞,这条(些)边就是被覆盖的边,我们将它们记录一下,所以我们只要找这些超时的任务的公共覆盖边就好了,即这条(些)边被记录的次数等于超时的任务数
代码有详细注释,不懂可以去看一下代码
#include<bits/stdc++.h> #define ll long long #define rll register long long using namespace std; const ll N=3e5+5; ll n,m,cnt; ll h[N],lg[N],sum[N],deep[N],st[N][20],vis[N],up[N]; struct edge//边 { ll v,w,nxt; } e[N<<1]; struct rw//任务 { ll u,v,lca,dis; bool operator <(const rw x)//重载运算符 { return dis<x.dis; } } g[N]; inline ll read() { ll x=0; bool flag=false; char ch=getchar(); while(ch<"0"||ch>"9") { if(ch=="-") flag=true; ch=getchar(); } while(ch>="0"&&ch<="9") { x=(x<<3)+(x<<1)+(ch^48); ch=getchar(); } return flag?~x+1:x; } void add(ll u,ll v,ll w) { e[++cnt].v=v; e[cnt].w=w; e[cnt].nxt=h[u]; h[u]=cnt; } void dfs(ll u,ll fat) { deep[u]=deep[fat]+1;//深度更新 st[u][0]=fat;//更新st表 for(rll i=1;i<=lg[deep[u]];++i) { st[u][i]=st[st[u][i-1]][i-1];//更新st表 } for(rll i=h[u];i;i=e[i].nxt)//遍历 { ll v=e[i].v,w=e[i].w; if(v!=fat)//如果不是父节点 { sum[v]=(sum[u]+w); //到根节点的距离就是父节点到根节点的距离加上这条边的边权 up[v]=w;//处理到父节点的距离 dfs(v,u);//继续搜索 } } } ll LCA(ll x,ll y)//正常求LCA { if(deep[x]<deep[y]) { x=x^y,y=x^y,x=x^y;//位运算版本的交换 } while(deep[x]>deep[y]) { x=st[x][lg[deep[x]-deep[y]]-1];//让两点在同一深度 } if(x==y) return x; for(rll i=lg[deep[x]]-1;i>=0;--i)//找交汇之前最后到达的点 { if(st[x][i]!=st[y][i])//如果不相交,向上跳 { x=st[x][i]; y=st[y][i]; } } return st[x][0];//返回lca } bool check(ll len) { if(g[m].dis<=len) return true; //如果路程最长的都比不过当前二分的答案,那么这个答案是可行的,直接返回 for(rll i=1;i<=n;++i) vis[i]=0;//每次操作前,数组清0 ll cont=0,maxn=0; //cont 记录比当前答案大的边,maxn记录被覆盖次数最多的边被覆盖的次数 //(我知道maxn这里我解释的很trouble,不好理解就把他看成每条边被覆盖的次数的最大值) //一定要时刻记住maxn的含义!!! for(rll i=m;i>=1;--i)//遍历 { if(g[i].dis<=len) break; //如果当前任务的路程已经小于答案,就没必要继续遍历查找了,退出即可 cont++;//当前任务路程比答案大,计数器+1 ll u=g[i].u,v=g[i].v,lca=g[i].lca,c=g[i].dis-len; //c 如果这条边大于或等于c,就说明删去它,这个任务的时间就小于或等于答案 //即这条边可以设为虫洞 while(u!=lca)//如果起点不是lca,那就向上找,查询被覆盖的边 { if(up[u]>=c)//大于c,说明在这个任务中,它可以设为虫洞,也就是上文说的覆盖 { vis[u]++; maxn=max(maxn,vis[u]);//更新maxn } u=st[u][0];//往上慢慢找,时间限制是1s~2s } while(v!=lca)//如果终点不是lca,向上找 { if(up[v]>=c)//和上面一样 { vis[v]++; maxn=max(maxn,vis[v]);//更新maxn } v=st[v][0];//慢慢跳(~ ̄▽ ̄)~ } if(maxn<cont)return false; //这里maxn只能小于等于cont,因为进行了cont次循环,一条边最多被覆盖cont次 //maxn是边被覆盖的次数的最大值 //如果小于cont,就说明找不到一条边可以被删去后使任务的时间小于等于答案 //所以这个答案不成立 //如果maxn==cont 就说明至少有一条边可以做到删去后使任务的时间小于等于答案 } return true; } int main() { n=read(),m=read(); for(rll i=1;i<n;++i) { ll x=read(),y=read(),z=read(); add(x,y,z); add(y,x,z); } for(rll i=1;i<=n;++i) { lg[i]=lg[i-1]+(1<<lg[i-1]==i);//预处理log,节约时间 } dfs(1,0);//搜索 for(rll i=1;i<=m;++i)//记录每一个计划的信息 { ll u=read(),v=read(),lca=LCA(u,v); g[i].u=u;//起点 g[i].v=v;//终点 g[i].lca=lca;//lca g[i].dis=sum[u]+sum[v]-(sum[lca]<<1);//这个计划的长度 } sort(g+1,g+m+1);//将任务从小到大排序 ll l=0,r=3e8,ans; while(l<r) //注意!!!这里二分的是最终答案,也就是最小花费时间,清楚概念,否则后边就和我一样容易混 { ll mid=(l+r)>>1;//二分操作 if(check(mid)) r=mid,ans=mid; else l=mid+1; } printf("%lld ",ans); return 0; }