淺談 Johnson 算法


前言

Johnson 和 Floyd 一樣是用來解決無負環圖上的全源最短路。

在稀疏圖上的表現遠遠超過 Floyd,時間復雜度 \(O(nm\log m)\)

算法本身一點都不復雜(前提是你已經掌握了多種最短路算法),而且正確性很容易證明。

注意:全文多處引自SF dalao 的文章

再次注意:模板題貼在這里,請熟讀題面再看代碼

引入

想想求一個有 \(\leq 3000\) 個點和 \(\leq 6000\) 條邊的有負權圖的全源最短路,你發現 Floyd \(O(n^3)\) 爆體而亡

那怎么辦辦呢...,我們先假設沒有負權,然后可以想到一下很暴力的做法:

既然沒有負權我們可以用 \(n\) 遍 dijkstra 來解決問題,復雜的 \(O(nm\log m)\),可以接受,20pts 到手。

但是它有負權啊,你這做法有個毛線用啊

所以有一個奇怪的想法滋生在我們的腦海中:如果我們將每一個邊權都加上一個定值使之為正呢?

這個想法很直接,乍一想好像還挺有道理,正在你暗暗佩服自己的睿智時,我會告訴你:這是錯的

例如對於下圖:\(1\)\(2\) 的最短路為 \(1\to 5\to 3\to 2\)

Johnson 算法演示1.png

但是在所有邊權加上 \(5\) 之后呢?

Johnson 算法演示2.png

觀察上圖,發現最短路變成了 \(1\to 4\to 2\),這顯然是不對的。

除了有圖有真相之外,我們還可以用柯學的方法來解釋為什么這是錯的:

因為這里的每一條路徑增加的並不是一個定值。

例如這里 \(1\to 4\to 2\) 只增加了 \(10\),而 \(1\to 5\to 3\to 2\) 卻增加了 \(15\)

顯然在這種情況下,邊數少的一條路徑更有利,從而導致錯誤

那么怎樣才能使每一條路徑都增加一個定值,每條邊權又都變成正數呢?

這就是 Johnson 算法的核心了。

算法概述

Johnson 算法分為兩步:

  1. 預處理勢能函數 \(h\)。(人話:跑一遍 spfa)
  2. 根據勢能函數求解全源最短路。(人話:跑 \(n\) 遍 dijkscal)

算法流程

這里先介紹算法流程再介紹正確性,第一遍閱讀時能讀懂多少就讀懂多少,看完下面的證明后再回頭看一眼即可。

我們新建一個虛擬節點(在這里我們就設它的編號為 \(0\)),從這個點向其他所有點連一條邊權為 \(0\) 的邊。

接下來用 Bellman-Ford 算法(spfa 的弱化版)求出從 \(0\) 號點到其他所有點的最短路,記為 \(h_i\)

假如存在一條從 \(u\) 點到 \(v\) 點,邊權為 \(w\) 的邊,則我們將該邊的邊權重新設置為 \(w+h_u-h_v\)

接下來以每個點為起點,跑 \(n\) 輪 dijkstra 算法即可求出任意兩點間的最短路了。

容易看出,該算法的時間復雜度是 \(O(nm\log m)\)

正確性證明

為什么這樣重新標注邊權的方式是正確的呢?

提醒一下,本文采用通俗易懂的寓言來描述證明過程,勢能什么的還是參考其他人的文章吧。

咳咳,回歸正題,為什么這樣重新標注邊權的方式是正確的呢?

假設重新標記后,\(s\)\(t\) 的路徑為 \(s\to p_1\to p_2\to ...\to p_k\to t\)

那么加權之后的長度表達式為:

\((w(s,p_1)+h_s−h_{p1})+(w(p_1,p_2)+h_{p1}−h_{p2})+⋯+(w(p_k,t)+h_{pk}−h_t)\)

化簡后得到:

\(w(s,p_1)+w(p_1,p_2)+ \dots +w(p_k,t)+h_s-h_t\)

那么顯然,無論從那個方向走來,\(h_s-h_t\) 始終不變,變化的僅僅是路徑上的邊權。

即我們引言中的第一個要求:每一條路徑都增加一個定值

接下來我們需要證明新圖中所有邊的邊權非負,因為在非負權圖上,dijkstra 算法能夠保證得出正確的結果。

根據三角形不等式,新圖上任意一邊 \((u,v)\) 上兩點滿足: \(h_v \leq h_u + w(u,v)\)

這條邊重新標記后的邊權為 \(w'(u,v)=w(u,v)+h_u-h_v \geq 0\)。這樣我們證明了新圖上的邊權均非負

至此,我們就證明了 Johnson 算法的正確性。

代碼實現

代碼實現還是挺簡單的,就不做過多介紹了。

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#include<queue>
#include<cstdlib>
#define N 30010
#define M 60010
#define INF 1000000000
using namespace std;

int n,m,head[N],cnt=0,sum[N];
long long h[N],dis[N];
bool vis[N];
struct Edge{
	int nxt,to,val;
}ed[M];

int read(){
	int x=0,f=1;char c=getchar();
	while(c<'0' || c>'9') f=(c=='-')?-1:1,c=getchar();
	while(c>='0' && c<='9') x=x*10+c-48,c=getchar();
	return x*f;
}

void add(int u,int v,int w){
	ed[++cnt].nxt=head[u];
	ed[cnt].to=v,ed[cnt].val=w;
	head[u]=cnt;
	return;
}

void spfa(){
	queue<int>q;
	memset(h,63,sizeof(h));
	memset(vis,false,sizeof(vis));
	h[0]=0,vis[0]=true;q.push(0);
	while(!q.empty()){
		int u=q.front();q.pop();
		if(++sum[u]>=n+1){
			printf("-1\n");exit(0);
		}
		vis[u]=false;
		for(int i=head[u];i;i=ed[i].nxt){
			int v=ed[i].to,w=ed[i].val;
			if(h[v]>h[u]+w){
				h[v]=h[u]+w;
				if(!vis[v]) q.push(v),vis[v]=true;
			}
		}
	}
	return;
}

void dijkstra(int s){
	priority_queue<pair<long long,int> >q;
	for(int i=1;i<=n;i++) 
		dis[i]=INF;
	memset(vis,false,sizeof(vis));
	dis[s]=0;
	q.push(make_pair(0,s));
	while(!q.empty()){
		int u=q.top().second;q.pop();
		if(vis[u]) continue;
		vis[u]=true;
		for(int i=head[u];i;i=ed[i].nxt){
			int v=ed[i].to,w=ed[i].val;
			if(dis[v]>dis[u]+w){
				dis[v]=dis[u]+w;
				if(!vis[v]) q.push(make_pair(-dis[v],v));
			}
		}
	}
	return;
}

int main(){
	n=read(),m=read();
	int u,v,w;
	for(int i=1;i<=m;i++)
		u=read(),v=read(),w=read(),add(u,v,w);
	for(int i=1;i<=n;i++)
		add(0,i,0);
	spfa();
	for(int u=1;u<=n;u++)
		for(int j=head[u];j;j=ed[j].nxt)
			ed[j].val+=h[u]-h[ed[j].to];
	for(int i=1;i<=n;i++){
		dijkstra(i);
		long long ans=0;
		for(int j=1;j<=n;j++){
			if(dis[j]==INF) ans+=(long long)j*INF;
			else ans+=(long long)j*(dis[j]+h[j]-h[i]);
		}
		printf("%lld\n",ans);
	}
	return 0;
}

結語

例題比較難找,之后遇到了會繼續 Update 的,同時希望同學們遇到 Johnson 的題目及時私信告訴我。

注意:全文多處引自SF dalao 的文章

完結撒花


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM