题解 P3238 【[HNOI2014]道路堵塞】
JohnJoeZhu · · 题解
由于题解很少加上讲解都好精炼,蒟蒻琢磨了好久,于是贡献一片题解
(前方多图预警)
题面传送门
博客体验应该不会更佳
1.化简题意
在有向正边权图中,给出
2.寻找解法
从题意上我们得不到直观的求解方式或思路(仅提供最短路这一思考方向),那我们就寻找题目性质
首先模拟题意,接下来是探索性质的思考过程,以图片形式呈现,各位大佬可以选择阅读或自行探究,结论探讨总结会在4幅图后呈现
探索开始
原图(最短路用红色表明,题目已知)
断开第一条边(紫色为答案,绿色为可行解,下面同理)
断开第二条边
断开第三条边
探索结束
结论开始
(由于图片展示有限,各位大佬可以模拟更长的路径,可以得到同样的结论)
咦,我们发现答案的边都是在最短路上跑一点,然后跳出来,又回到最短路也!
那是不是就是这个性质呢?
首先,我们先大胆猜想,然后再简单证明
(绿色为答案路径,紫色为非答案路径)
那不能够走紫色这条边吗?
显然,如果我们走路径k,那么对于最短路i到j,就会有更优解路径k来代替原来的路径,那路径k就成了最短路了
显然这与题意不符
可是真的只会绕一个弯吗?
显然题目要求我们只断一条边,所以即便是绕路,也只用绕一个弯,绕多个弯就不优了,这个可以结合上面的证明感性理解。
所以我们就得到性质:
对于答案路径,一定是在原最短路上走一段路,然后从最短路上一点绕开断路,走到最短路上另一点,再继续走最短路,回到终点。
如图所示
形式化的:
注意此时的大于和小于是指我们对最短路上的点编号后的结果
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;//完结撒花
}
看在这么长的题解+图片上,点个赞再走嘛。