P3629 巡逻 LCA题解
原题:洛谷P3629
问题转化#
首先,给定的图是一个有 个点, 条边的无向连通图,这个图就等价于一棵树。
不建立新的道路时,从 号节点出发,把整棵树上的每条边遍历至少一次,再回到 号节点,会恰好经过每条边两次,路线总长度为 ,如下图最左边的部分所示。根据树的深度优先遍历的思想,很容易证明这个结论,因为每条边必然被递归一次、回溯一次。
建立 条新道路之后,因为新道路必须经过恰好一次( 次、 次都不可以),所以在沿着新道路 巡逻之后,要返回 ,就必须沿着树上从 到 的路径巡逻一遍,最终形成一个环。与不建立新道路的情况相结合,相当于树上 与 之间的路径就只需经过一次了,如上图所示。
因此,当 时,我们找到树的最长链,在两个端点之间加一条新道路,就能让总的巡逻距离最小。,答案就是 。
当 时,建立第 条新道路 之后,又会形成一个环,若两条新道路形成的环不重叠,则树上 之间的路径只需经过一次,答案继续减小。否则,在两个环重叠的情况下,如果我们还按照刚才的方法把第 个环与建立 条新道路的情况相结合,两个换重叠的部分就不会被巡逻到,如下图所示。因为题目要求每条道路必须被巡逻,两个环重叠的部分就不会被巡逻到,并且返回。最终的结果应是两个环重叠的部分由 “只需经过一次” 变回了 “需要经过两次”。
综上所述,我们得到了如下算法。
在最初的树上求直径,设直径为 。然后把直径上的边权取反(从 改为 )。
在最长链边权取反后的树上再次求直径,设直径为 。
答案就是 。如果这条直径包含 取反的部分,就相当于两个环重叠。减掉 后,重叠的部分变成了 “只需经过一次”,减掉 后,相当于把重叠的部分加回来,变为 “需要经过两次”,与我们之前的讨论相符。时间复杂度为 。
以上摘编自李煜东《算法竞赛进阶指南》
处理 l1 的路径#
书中处理 的路径时用的是两次 ,而我们也可以用 来处理,原理如下:
我们在处理 时可以同时处理出 的起点 和终点 。由于树上两点间的路径是唯一的,且这个路径一定经过这两点的 。所以用树形 处理完 后,求出 和 的 ,记为 。然后先从 开始向父亲节点跳,跳到 ,将期间经过的点顺序存进数组 。再从 开始跳,跳到 ,将经过的点逆序存进数组 ,数组 就是 的路径。
如何处理起点和终点呢?很简单,在树形 处理 (从节点 出发走向以 为根的子树,能够到达的最远节点的距离)时,用 记录 从节点 出发走向以 为根的子树,能够到达的最远节点。那么对于由 组成的链,它的起点和终点就是 和 。
注意,对 的初值的处理应为:对于每个叶节点 ,。
还有一点要注意的,正常的存边方式无法通过起点和终点确定一条边,但是我们处理出的是路径上的点,只能通过起点和终点给边权取反,所以我们要改一下边权的存储方式:map<int,int>v[N]
, 表示从 到 有一条边权为 的边。
vector<int>e[N];
map<int,int>v[N];
inline void work()
{
while(st!=x) v[st][f[st][0]]=v[f[st][0]][st]=-1,st=f[st][0];
while(ed!=x) v[ed][f[ed][0]]=v[f[ed][0]][ed]=-1,ed=f[ed][0];
//这里图省事,不把路径存数组里,在跳的时候直接把边权取反
}
void DP(int u,int num)
{
vis[u]=1;
if(du[u]==1) to[u]=u;//判断叶节点
for(reg int i=0;i<e[u].size();i=-~i)
{
if(vis[e[u][i]]) continue;
DP(e[u][i],num);
if(l[num]<d[u]+d[e[u][i]]+v[u][e[u][i]]) l[num]=d[u]+d[e[u][i]]+v[u][e[u][i]],st=to[u],ed=to[e[u][i]];
if(d[u]<d[e[u][i]]+v[u][e[u][i]]) d[u]=d[e[u][i]]+v[u][e[u][i]],to[u]=to[e[u][i]];
}
}
signed main()
{
n=read();k=read();
for(reg int i=1;i<n;i=-~i)
{
x=read();y=read();
e[x].push_back(y);e[y].push_back(x);
du[x]++;du[y]++;v[x][y]=v[y][x]=1;
}
DP(1,1);
if(k==1) return printf("%lld",2*(n-1)-l[1]+1),0;
pre();dfs(1,0);x=lca(st,ed);//求lca,pre是预处理log
work();
memset(d,0,sizeof(d));
memset(vis,0,sizeof(vis));//注意清零
DP(1,2);
return printf("%lld",2*n-l[1]-l[2]),0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】