首先考慮求全源最短路的幾種方法:
- Floyd:時間復雜度\(O(n^3)\),可以處理負權邊,但不能處理負環,而且速度很慢。
- Bellman-Ford:以每個點為源點做一次Bellman-Ford,時間復雜度\(O(n^2m)\),可以處理負權邊,可以處理負環,但好像比Floyd還慢?
- dijkstra:以每個點為源點做一次dijkstra,時間復雜度\(O(nmlogm)\),不能處理負權邊,但比前面兩個快多了。
好像……只有dijkstra還有希望?但負權邊處理不了真是很棘手啊。
一種方法是讓每條邊都加上一個數\(x\)使得邊權為正,但考慮下圖:
\(1\)到\(2\)的最短路應為:\(1 -> 3 -> 4 -> 2\),長度為\(-1\)。如果我們把每條邊的邊權都加上\(5\):
此時的最短路是:\(1 -> 5 -> 2\),就不是實際的最短路了,所以這種方法行不通
注:經本人研究,應該是兩條路徑進過的邊的數量不同而導致的
接下來,就該 Johnson 登場啦!Johnson 其實就是用另一種方法標記邊權啦。
首先來看看實現方法:我們新建一個虛擬結點(不妨設他的編號為0),由他向其他的所有結點都連一條邊權為\(0\)的邊,然后求0號節點為源點的單源最短路,存到一個\(h\)數組中。然后,讓每條邊的權值\(w\)變為\(w+h_u-h_v\),這里\(u\)和\(v\)分別為這條邊的起點和終點。然后再以每個點為源點做 dijkstra 就OK了。
Q:那這么說,Dijkstra 也可以求出負權圖(無負環)的單源最短路徑了?
A:沒錯。但是預處理要跑一遍 Bellman-Ford,還不如直接用 Bellman-Ford 呢。
如何證明這是正確的呢?
首先,從\(s\)到\(t\)的路徑中隨便取出一條:
則這條路徑的長度為:
簡化后得到:
可以發現,不管走哪條路徑,最后都是\(+h_s-h_t\),而\(h_s\)和\(h_t\)又是不變的,所以最終得到的最短路徑還是原來的最短路徑。
到這里已經證明一半了,接下來要證明得到的邊權非負,必須要無負權邊才能使 dijkstra 跑出來的結果正確。根據三角形不等式(就是那個三角形里任意兩條邊的長度之和大於等於另一條邊的長度),新圖上的任意一條邊\((u,v)\)上的兩點滿足:\(h_v \le w_{u,v}+h_u\),則新邊的邊權\(w_{u,v}+h_u-h_v \ge 0\)。所以新圖的邊權非負。
正確性證明就是這個亞子。
代碼實現(注意處理精度問題,該開ll的時候開ll):
#include<cstdio>
#include<queue>
#define MAXN 5005
#define MAXM 10005
#define INF 1e9
using namespace std;
int n,m;
int vis[MAXN];
long long h[MAXN],dis[MAXN];
bool f[MAXN];
struct graph
{
int tot;
int hd[MAXN];
int nxt[MAXM],to[MAXM],dt[MAXM];
void add(int x,int y,int w)
{
tot++;
nxt[tot]=hd[x];
hd[x]=tot;
to[tot]=y;
dt[tot]=w;
return ;
}
}g;//鏈式前向星
bool SPFA(int s)//這里用了Bellman-Ford的隊列優化
{
queue<int>q;
for(int i=1;i<=n;++i) h[i]=INF,f[i]=false;
h[s]=0;
f[s]=true;
q.push(s);
while(!q.empty())
{
int xx=q.front();
q.pop();
f[xx]=false;
for(int i=g.hd[xx];i;i=g.nxt[i])
if(h[g.to[i]]>h[xx]+g.dt[i])
{
h[g.to[i]]=h[xx]+g.dt[i];
if(!f[g.to[i]])
{
if(++vis[g.to[i]]>=n) return false;//注意在有重邊的情況下要記錄入隊次數而不是松弛次數
f[g.to[i]]=true,q.push(g.to[i]);
}
}
}
return true;
}
void dijkstra(int s)
{
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q;
for(int i=1;i<=n;i++) dis[i]=INF,f[i]=false;
q.push(make_pair(0,s));
dis[s]=0;
while(!q.empty())
{
int xx=q.top().second;
q.pop();
if(!f[xx])
{
f[xx]=true;
for(int i=g.hd[xx];i;i=g.nxt[i])
if(dis[g.to[i]]>dis[xx]+g.dt[i])
{
dis[g.to[i]]=dis[xx]+g.dt[i];
if(!f[g.to[i]])
q.push(make_pair(dis[g.to[i]],g.to[i]));
}
}
}
return ;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
g.add(u,v,w);
}
for(int i=1;i<=n;i++) g.add(0,i,0);//建虛擬節點0並且往其他的點都連一條邊權為0的邊
if(!SPFA(0))//求h的同時也判了負環
{
printf("-1");
return 0;
}
for(int u=1;u<=n;u++)
for(int i=g.hd[u];i;i=g.nxt[i])
g.dt[i]+=h[u]-h[g.to[i]];//求新邊的邊權
for(int i=1;i<=n;i++)
{
dijkstra(i);//以每個點為源點做一遍dijkstra
long long ans=0;
for(int j=1;j<=n;j++)//記錄答案
if(dis[j]==INF) ans+=1ll*j*INF;
else ans+=1ll*j*(dis[j]+(h[j]-h[i]));
printf("%lld\n",ans);
}
return 0;
}
最后安利一發博客