【洛谷5905】【模板】Johnson 全源最短路
- 给定一张\(n\)个点\(m\)条边的有向图,可能存在负权边。
- 求从每个点出发到所有点的最短路。
- \(n\le3\times10^3,m\le6\times10^3\),点名卡\(n\)轮\(SPFA\)
单源最短路算法
考虑最常见的几个单源最短路算法:\(Floyd\),\(Dijkstra\),\(SPFA\)。
\(n\le3\times10^3\)跑\(Floyd\)简直就是天方夜谭。。。
有负权边的图没办法跑\(Dijkstra\)。。。
题目里点名卡\(SPFA\),想过除非有着极为惊人的卡常技巧。。。
所以说,我们就这样束手无策了?
考虑这三个算法中有一个的失败原因和另两个是不一样的——\(Dijkstra\)是因为有负权边可能会\(WA\),而不是会\(TLE\)。
因此,我们的目标就是要对原图进行一定转化,把所有边权变得非负。
边权转化
我们事先建立一个\(0\)号点,向所有点连一条边权为\(0\)的边,然后跑一遍\(SPFA\),求出了从\(0\)号点到每个点的最短路\(h_i\)。(这一过程顺便可以把负环判掉)
然后考虑对于一条有向边\(x_i\rightarrow y_i\)(边权为\(v_i\)),根据最短路的性质,满足\(h_{u_i}+w_i\ge h_{v_i}\),也就是\(w_i+h_{u_i}-h_{v_i}\ge 0\)。
于是,我们把每一条边的边权修改为\(w_i+h_{u_i}-h_{v_i}\),那么所有边权非负,在这张图上就可以开心地枚举每个起点跑\(Dijkstra\)了。
而此时想要转化回原图上\(s\)到\(t\)的距离,发现加上的\(h_{u_i}-h_{v_i}\)是一个类似于势能的玩意,作为中转节点的贡献会被抵消掉,因此只要给求出的距离减去\(h_s-h_t\)即可。
代码:\(O(n^2logn)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 3000
#define M 6000
#define add(x,y,z) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y,e[ee].v=z)
using namespace std;
int n,m,ee,lnk[N+5];struct edge {int to,nxt,v;}e[N+M+5];
int h[N+5],ct[N+5],IQ[N+5];queue<int> Q;I void SPFA()//一遍SPFA预处理
{
#define NA() (puts("-1"),exit(0),0)
RI i,k;for(i=1;i<=n;++i) h[i]=1e9;Q.push(n+1);W(!Q.empty())
for(i=lnk[k=Q.front()],Q.pop(),IQ[k]=0;i;i=e[i].nxt) h[k]+e[i].v<h[e[i].to]&&
(++ct[e[i].to]>2*n&&NA(),h[e[i].to]=h[k]+e[i].v,!IQ[e[i].to]&&(Q.push(e[i].to),IQ[e[i].to]=1));//顺便判负环
}
typedef pair<int,int> Pr;int dis[N+5],vis[N+5];priority_queue<Pr,vector<Pr>,greater<Pr> > q;I void Dij(CI s)//Dijkstra跑单源最短路
{
RI i,k,d;for(i=1;i<=n+1;++i) dis[i]=1e9,vis[i]=0;q.push(make_pair(dis[s]=0,s));//从s出发
W(!q.empty()) if(k=q.top().second,q.pop(),!vis[k]) for(vis[k]=1,i=lnk[k];i;i=e[i].nxt)
(d=dis[k]+e[i].v+h[k]-h[e[i].to])<dis[e[i].to]&&(q.push(make_pair(dis[e[i].to]=d,e[i].to)),0);//给边权加上h[k]-h[e[i].to]使非负
long long t=0;for(i=1;i<=n;++i) t+=1LL*i*(i^s?(vis[i]?dis[i]-h[s]+h[i]:1e9):0);printf("%lld\n",t);//枚举终点减去h[s]-h[i]统计答案
}
int main()
{
RI i,x,y,z;for(scanf("%d%d",&n,&m),i=1;i<=m;++i) scanf("%d%d%d",&x,&y,&z),add(x,y,z);
for(i=1;i<=n;++i) add(n+1,i,0);for(SPFA(),i=1;i<=n;++i) Dij(i);return 0;//先跑SPFA转化边权,然后枚举每个点出发跑Dijkstra
}
待到再迷茫时回头望,所有脚印会发出光芒