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

JohnJoeZhu
我要认真学习whk!!!搬运于
2025-08-24 21:46:49,当前版本为作者最后更新于2020-09-09 21:35:25,作者可能在搬运后再次修改,您可在原文处查看最新版自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多
以下是正文
由于题解很少加上讲解都好精炼,蒟蒻琢磨了好久,于是贡献一片题解
(前方多图预警)1.化简题意
在有向正边权图中,给出 到 最短路径经过的边,求每删除有且只有一条边后的最短路
2.寻找解法
从题意上我们得不到直观的求解方式或思路(仅提供最短路这一思考方向),那我们就寻找题目性质
首先模拟题意,接下来是探索性质的思考过程,以图片形式呈现,各位大佬可以选择阅读或自行探究,结论探讨总结会在4幅图后呈现
探索开始
原图(最短路用红色表明,题目已知)

断开第一条边(紫色为答案,绿色为可行解,下面同理)

断开第二条边

断开第三条边

探索结束
结论开始
(由于图片展示有限,各位大佬可以模拟更长的路径,可以得到同样的结论)
咦,我们发现答案的边都是在最短路上跑一点,然后跳出来,又回到最短路也!
那是不是就是这个性质呢?
首先,我们先大胆猜想,然后再简单证明
(绿色为答案路径,紫色为非答案路径)

那不能够走紫色这条边吗?
显然,如果我们走路径k,那么对于最短路i到j,就会有更优解路径k来代替原来的路径,那路径k就成了最短路了
显然这与题意不符
可是真的只会绕一个弯吗?
显然题目要求我们只断一条边,所以即便是绕路,也只用绕一个弯,绕多个弯就不优了,这个可以结合上面的证明感性理解。
所以我们就得到性质:
对于答案路径,一定是在原最短路上走一段路,然后从最短路上一点绕开断路,走到最短路上另一点,再继续走最短路,回到终点。
如图所示

形式化的:
$$\text{其中}x_uj,\text{且}1-x_u,x_v-n\text{属于原最短路径},x_u-x_v\text{不属于原最短路径且使答案最优} $$注意此时的大于和小于是指我们对最短路上的点编号后的结果
3.解法实现
说了这么多性质,到底如何实现呢???
显然我们不可能对每一条边断开后单独求解
那我们就考虑是不是答案有传递性,相互之间的贡献,或者任何共同性可以减少复杂度的
那我们的性质就要派上用场了
(废话我们发现,我们的绕路都是从一个地方跳出来,然后再回去
从的顺序遍历,如果前面的路径也绕开了这条边,我们是不是就可以使用呢?
显然,之前的答案对于现在是有贡献的,但是不一定是最优的
所以我们是不是还要从这条断边的起点SPFA一次,获得这个点对答案(也就是绕开路径)的贡献
那答案是否有有限的贡献范围呢?
显然,如果这一条远路没有绕开断边,对后面的答案都没有贡献了
所以绕远路的终点,也就是回到最短路上的点,需要比现在的断边的终点大
然后我们又发现,断边的枚举是递增的
每次插入一个比现在大的点和权值,然后寻找最小的权值且满足要求
动态维护一个最小值,支持删除插入,时间复杂度在左右,我们就可以突发奇想,想到小根堆!
接下来整理解法
-
存图+对最短路上的点顺序编号+求到终点的最短路值(后面会用到)
-
顺序枚举最短路上的点
-
对于当前的点,SPFA寻找后继节点
由于这里找到最短路上的点后需要快速得到到终点的长度,并插入堆
所以预处理一个到终点的最短路值,同时减少没有必要的尝试,减少时间
-
从堆顶取出元素,如果路径没有绕过断边,踢出堆
-
如果堆顶无元素,输出-1,否则输出解
-
更新到当前点的最短路(之前走的都是绕远路来的)
-
如果这一条边的终点是n,结束
其实就是这幅图啦

4.时间复杂度简析
蒟蒻只会简单分析,有错请指正
设最短路上有个点,不在最短路上有个点
枚举断边
每一个断边最坏需要
那复杂度就需要
还有一个堆的复杂度
貌似很大,但是每一次拓展,堆理论的大小都会减小,SPFA的拓展也越来越有限,而且这理论上是一个稀疏图
综上,复杂度为
只要常数好,就卡得过
所以这是一段瞎扯5.代码
还是这个最重要
#include<queue> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; int read()//蒟蒻不太会快读(雾) { int ans=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') f*=-1; ch=getchar(); } ans=ch-'0'; ch=getchar(); while(ch>='0'&&ch<='9') ans*=10,ans+=ch-'0',ch=getchar(); return ans; } const int N=2e5+5; int n,m,l; int num[N],rode[N],dis1[N],disn[N],along[N]; struct edge{ int u,v,nex,w; }edge[N<<1]; struct node{ int u,dis; node(int uu,int diss){u=uu,dis=diss;} bool operator <(const node &x)const{ return dis>x.dis;//注意符号,我们要小根堆 } }; priority_queue<node>s; int head[N],top=0; void add(int u,int v,int w)//日常加边 { edge[++top].v=v; edge[top].u=u; edge[top].w=w; edge[top].nex=head[u]; head[u]=top; } bool vis[N],viss[N<<1]; void SPFA(int S)//日常SPFA { queue<int>q; for(int i=1;i<=n;i++) vis[i]=0; q.push(S); vis[S]=1; while(!q.empty()) { int u=q.front(); q.pop(); vis[u]=0; for(int i=head[u];i;i=edge[i].nex) { int v=edge[i].v; if(!viss[i]&&dis1[v]>dis1[u]+edge[i].w)//不能过原最短路上的边,保证只走远路 { dis1[v]=dis1[u]+edge[i].w; if(num[v]) s.push(node(num[v],dis1[v]+disn[num[v]]));//跑回最短路就插入堆 else if(!vis[v]) { vis[v]=1; q.push(v); } } } } } int main() { n=read(),m=read(),l=read(); for(int i=1,u,v,w;i<=m;i++) u=read(),v=read(),w=read(),add(u,v,w); for(int i=1;i<=l;i++)//标记最短路 { rode[i]=read(); along[i+1]=edge[rode[i]].v;//记录点 num[along[i+1]]=i+1;//对最短路上的点编号 viss[rode[i]]=1;//记录最短路的边 } for(int i=l;i;i--) disn[i]=disn[i+1]+edge[rode[i]].w;//求出到n点的最短距离 memset(dis1,0x3f,sizeof(dis1));//到1点的最短路,服务于SPFA dis1[1]=0; along[1]=1; SPFA(1); for(int i=1;i<=l;i++) { while(!s.empty()&&s.top().u<=i) s.pop();//没有绕开断边 printf("%d\n",s.empty()?-1:s.top().dis);//用堆求解 dis1[edge[rode[i]].v]=dis1[edge[rode[i]].u]+edge[rode[i]].w;//更新到1点的最短路,服务于SPFA SPFA(edge[rode[i]].v); } return 0;//完结撒花 }看在这么长的题解+图片上,点个赞再走嘛。
-
- 1
信息
- ID
- 2311
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 7
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者