图论:Johnson全源最短路
背景:
Johnson 和 Floyd 一样,是一种能求出无负环图上任意两点间最短路径的算法。该算法在 1977 年由 Donald B. Johnson 提出。
任意两点间的最短路可以通过枚举起点,跑 n次 Bellman-Ford 算法解决,时间复杂度是 O(\(n^2m\))的,也可以直接用 Floyd 算法解决,时间复杂度为 O(\(n^3\))。
注意到堆优化的 Dijkstra 算法求单源最短路径的时间复杂度比 Bellman-Ford 更优,如果枚举起点,跑 n 次 Dijkstra 堆优化算法,就可以在 O(\(nmlogm\))的时间复杂度内解决本问题,
比上述跑 n 次 Bellman-Ford 算法的时间复杂度更优秀,在稀疏图上也比 Floyd 算法的时间复杂度更加优秀。
但 Dijkstra 算法不能正确求解带负权边的最短路,因此我们需要对原图上的边进行预处理,确保所有边的边权均非负。
点击查看代码块
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll,int> pii;
const int inf = 0x7f7f7f7f;
const int maxn = 1e4+10;
int head[maxn],cnt=0;
struct edge{
int v,next;
ll w;
}e[maxn<<1];
void add(int u,int v,ll w){
e[cnt].v=v;e[cnt].w=w;e[cnt].next=head[u];head[u]=cnt++;
}
int n,m,S;//S为超级源点
ll h[maxn];//spfa先跑一次出来超级源点到所有点的单源最短路
int num[maxn];
bool vis[maxn];
bool spfa(int s){
memset(num,0,sizeof(num));
memset(vis,0,sizeof(vis));
for (int i=0;i<=n;i++) h[i] = inf;
h[s] = 0;
vis[s] = 1;
num[s] = 1;
queue<int> q;
q.push(s);
while(!q.empty()){
int u=q.front();
q.pop();
vis[u] = 0;
for (int i=head[u];~i;i=e[i].next){
int v=e[i].v;
ll w=e[i].w;
if(h[v] > h[u] + w){
h[v] = h[u] + w;
if(!vis[v]){
vis[v] = 1;
num[v]++;
q.push(v);
if(num[v]>n) return 1;
}
}
}
}
return 0;
}
ll dis[maxn];
void dijkstra(int s){
for (int i=1;i<=n;i++) dis[i] = inf;
memset(vis,0,sizeof(vis));
dis[s] = 0;
priority_queue<pii,vector<pii>,greater<pii> >q;
q.push({dis[s],s});
while(!q.empty()){
int u=q.top().second;
q.pop();
if(vis[u]) continue;
vis[u] = 1;
for (int i=head[u];~i;i=e[i].next){
int v=e[i].v;
ll w=e[i].w;
if(dis[v]>dis[u]+w){
dis[v]=dis[u]+w;
q.push({dis[v],v});
}
}
}
}
int main(){
memset(head,-1,sizeof(head));
cnt = 0;
cin>>n>>m;
for (int i=1;i<=m;i++){
int u,v;
ll w;
scanf("%d%d%lld",&u,&v,&w);
add(u,v,w);
}
S = 0;
for (int i=1;i<=n;i++){//超级源点和所有点连一条边权为0的边
add(S,i,0);
}
if(spfa(S)){
puts("-1");return 0;
}
//更改边权为w+h[u]-h[v]
for (int u=1;u<=n;u++){
for (int i=head[u];~i;i=e[i].next){
int v=e[i].v;
ll w=e[i].w;
e[i].w = w + h[u] - h[v];
}
}
//对每个点再跑一次dijkstra
for (int i=1;i<=n;i++){
dijkstra(i);
ll ans = 0;
for (int j=1;j<=n;j++){
if(dis[j] == inf) ans += 1ll*j*1e9;
else ans += 1ll*j*(dis[j]+h[j]-h[i]);//这里注意:原本加上来的现在要减回去
}
printf("%lld\n",ans);
}
return 0;
}
/*
3 2
1 2 -2
1 3 -3
*/