Johnson全源最短路

例題:P5905 【模板】Johnson 全源最短路數組

首先考慮求全源最短路的幾種方法:優化

  • Floyd:時間複雜度\(O(n^3)\),能夠處理負權邊,但不能處理負環,並且速度很慢。
  • Bellman-Ford:以每一個點爲源點作一次Bellman-Ford,時間複雜度\(O(n^2m)\),能夠處理負權邊,能夠處理負環,但好像比Floyd還慢?
  • dijkstra:以每一個點爲源點作一次dijkstra,時間複雜度\(O(nmlogm)\),不能處理負權邊,但比前面兩個快多了。

好像……只有dijkstra還有但願?但負權邊處理不了真是很棘手啊。spa

一種方法是讓每條邊都加上一個數\(x\)使得邊權爲正,但考慮下圖:

\(1\)\(2\)的最短路應爲:\(1 -> 3 -> 4 -> 2\),長度爲\(-1\)。若是咱們把每條邊的邊權都加上\(5\)

此時的最短路是:\(1 -> 5 -> 2\),就不是實際的最短路了,因此這種方法行不通code

注:經本人研究,應該是兩條路徑進過的邊的數量不一樣而致使的blog

接下來,就該 Johnson 登場啦!Johnson 其實就是用另外一種方法標記邊權啦。隊列

首先來看看實現方法:咱們新建一個虛擬結點(不妨設他的編號爲0),由他向其餘的全部結點都連一條邊權爲\(0\)的邊,而後求0號節點爲源點的單源最短路,存到一個\(h\)數組中。而後,讓每條邊的權值\(w\)變爲\(w+h_u-h_v\),這裏\(u\)\(v\)分別爲這條邊的起點和終點。而後再以每一個點爲源點作 dijkstra 就OK了。get

Q:那這麼說,Dijkstra 也能夠求出負權圖(無負環)的單源最短路徑了?
A:沒錯。可是預處理要跑一遍 Bellman-Ford,還不如直接用 Bellman-Ford 呢。博客

如何證實這是正確的呢?it

首先,從\(s\)\(t\)的路徑中隨便取出一條:io

\[s -> p_1 -> p_2 -> \cdots -> p_k -> t \]

則這條路徑的長度爲:

\[(w_{s,p_1}+h_s-h_{p_1})+(w_{p_1,p_2}+h_{p_1}-h_{p_2})+\dots+(w_{p_k,t}+h_{p_k}-h_t) \]

簡化後獲得:

\[w_{s,p_1}+w_{p_1,p_2}+\cdots+w_{p_k,t}+h_s-h_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;
}

最後安利一發博客

相關文章
相關標籤/搜索