最短路算法總結(超詳細~)

最短路算法框架

最短路有五種算法,分別適用不同的情況。
在這裏插入圖片描述
單源最短路: 求一個點到其他點的最短路
多源最短路: 求任意兩個點的最短路

稠密圖用鄰接矩陣存,稀疏圖用鄰接表存儲。

稠密圖: m 和 n2 一個級別
稀疏圖: m 和 n 一個級別


樸素Dijkstra算法

在這裏插入圖片描述
集合s:所有已經確定最短路的點
①的意思就是在集合s外找一個距離起點最近的點
②的意思就是讓這個最近點放到集合 s 中

Dijkstra算法是通過 n 次循環來確定 n 個點到起點的最短路的。

首先找到一個沒有確定最短路且距離起點最近的點,並通過這個點將其他點的最短距離進行更新。每做一次這個步驟,都能確定一個點的最短路,所以需要重複此步驟 n 次,找出 n 個點的最短路。

核心代碼

for(int i=0; i<n; i++)
	{
		int t = -1;
		for(int j=1; j<=n; j++)   // 在沒有確定最短路中的所有點找出距離最短的那個點 t 
		   if(!st[j] && (t == -1 || dist[t] > dist[j]))
		    t=j;                  
		    
		st[t]=true; // 代表 t 這個點已經確定最短路了
		
		for(int j=1; j<=n; j++) // 用 t 更新其他點的最短距離 
		 dist[j] = min(dist[j],dist[t]+g[t][j]);
	 } 

代碼講解:

① 找到 t 後,t 是剩餘未確定最短路中的距離起點最小的結點,那麼它的最短距離我們就可以確定了,標記時讓 st[t] = true 即可。

② t 是怎樣更新其他點的最短距離呢?看下圖

在這裏插入圖片描述
剛開始,t = 1,因爲結點 1 距離起點距離爲0,是距離最小的,我們拿1這個結點來更新其他結點的最短路,結點 2 的最短路可以更新爲 1,因爲 t 到起點的距離爲 0,而1——>2的距離爲 1,0+1=1,同理,結點 3 的最短距離更新爲 4 。
這第一個結點的最短距離我們已經確定了,此時,再找下一個 t ,很明顯,t = 2,我們再來用 2 這個結點來更新其他點的最短距離,因爲 dist[2]+g[2][3] = 1 + 2,很明顯 1 + 2 < 4,我們可以把結點 3 的最短距離更新成 3, 對應代碼裏的 dist[j] = min(dist[j],dist[t]+g[t][j]) 。

代碼裏每次更新都是循環1~n個結點,其實已經確定最短路的點是不用更新的了,還有 t 這個點可能與 j 這個點間是沒有路的,也是不用更新的,不過這不影響答案,但你也可以更新時加個 if(!st[j] && g[t][j]!=0x3f3f3f3f)。

例題:
給定一個n個點m條邊的有向圖,圖中可能存在重邊和自環,所有邊權均爲正值。

請你求出1號點到n號點的最短距離,如果無法從1號點走到n號點,則輸出-1。

輸入格式
第一行包含整數n和m。

接下來m行每行包含三個整數x,y,z,表示存在一條從點x到點y的有向邊,邊長爲z。

輸出格式
輸出一個整數,表示1號點到n號點的最短距離。

如果路徑不存在,則輸出-1。

數據範圍
1≤n≤500,
1≤m≤105,
圖中涉及邊長均不超過10000。

輸入樣例:
3 3
1 2 2
2 3 1
1 3 4

輸出樣例:
3

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 510;
int n,m;
int g[N][N]; // 鄰接矩陣
int dist[N]; // 第 i 個點到起點的最短距離
bool st[N]; // 代表第 i 個點的最短路是否確定、是否需要更新 

int dijkstra()
{
	memset(dist,0x3f,sizeof(dist)); // 將所有距離初始化正無窮
	dist[1] = 0;  // 第一個點到起點的距離爲 0  
	
	for(int i=0; i<n; i++)
	{
		int t = -1;
		for(int j=1; j<=n; j++)  // 在沒有確定最短路中的所有點找出距離最短的那個點 
		   if(!st[j] && (t == -1 || dist[t] > dist[j]))
		    t=j;
		    
		st[t]=true; //代表 t 這個點已經確定最短路了
		
		for(int j=1; j<=n; j++) // 用 t 更新其他點的最短距離 
		 dist[j] = min(dist[j],dist[t]+g[t][j]);
	 } 
	 if(dist[n] == 0x3f3f3f3f) return -1;  // 說明 1 和 n 是不連通的,不存在最短路 
	 return dist[n];
}
int main()
{
	cin >> n >> m;
	
	memset(g,0x3f,sizeof(g));
	while(m--)
	{
		int a,b,c;
		scanf("%d%d%d",&a,&b,&c);
		g[a][b] = min(g[a][b],c); // 保留長度最短的重邊 
	}
	cout << dijkstra(); 
	return 0;
}

堆優化版Dijkstra算法

優化版的Dijkstra算法是通過小根堆來找到當前堆中距離起點最短且沒有確定最短路的那個點。

因爲是稀疏圖,所以需要用鄰接表來存儲
還是上到題的題目,只不過數據範圍變成1≤n,m≤1.5×105,這個數據範圍如果還用樸素算法的話,O(n2)是1010級別,必定超時的,所以我們採用堆優化版的算法。

#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef pair<int,int> PII; //first存距離,second存結點編號 
const int N = 2e5+10;
int n,m;
int h[N],w[N],e[N],ne[N],idx; // 鄰接表的存儲 
int dist[N]; // 第 i 個點到起點的最短距離
bool st[N]; // 代表第 i 個點的最短路是否確定、是否需要更新 

void add(int a,int b,int c)
{
	e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
}

int dijkstra()
{
	memset(dist,0x3f,sizeof(dist)); // 將所有距離初始化正無窮
	dist[1] = 0;  // 第一個點到起點的距離爲 0  
	
	priority_queue<PII, vector<PII>, greater<PII>> heap; // 小根堆
	heap.push({0,1}); //把 1 號點放入堆中 
	
	while(heap.size()) // 堆不空
	{
	   	PII t = heap.top(); //找到當前距離最小的點
		heap.pop();
		
		int ver = t.second,distance = t.first; // ver爲編號,distance爲距離
		if(st[ver]) continue;   // 重邊的話不用更新其他點了
		st[ver] = true;   //標記 t 已經確定最短路
		
		for(int i = h[ver]; i!=-1; i=ne[i])  // 用 t 更新其他點的最短距離
		{
			int j = e[i];
			if(dist[j] > distance + w[i])
			{
				dist[j] = distance + w[i];
				heap.push({dist[j],j}); //入堆
			}
		}
	}  
	 
	if(dist[n] == 0x3f3f3f3f) return -1;  // 說明 1 和 n 是不連通的,不存在最短路 
	return dist[n];
}
int main()
{
	memset(h,-1,sizeof(h));
	cin >> n >> m;
	while(m--)
	{
		int a,b,c;
		scanf("%d%d%d",&a,&b,&c);
		add(a,b,c);
	}
	cout << dijkstra(); 
	return 0;
}

因爲題目上是有重邊的情況的,假設1——>2是有權重爲2和3的重邊,我們在用1號結點更新2號結點時,2號結點會兩次入堆,這樣我們發現堆中會有很多冗餘的點,當堆中彈出下一個 t 時,t 是爲{2,2}的,而不是{2、3},因爲 2<3 ,這個時候我們就可以標記 2 結點爲true了,等到下次堆中彈出{2、3}時,我們不需要用{2、3}來更新其他點了,因爲我們已經用{2、2}這個距離更小的點更新過了,所以堆中當彈出{2、3}時直接continue即可

解釋一下Dijkstra算法爲什麼不能用於有負權的邊的圖:
在這裏插入圖片描述
因爲Dijkstra算法是通過當前離起點最近的點來更新其他的點的距離,例如上圖中的 4 號結點會被 2 號結點更新爲2+1=3,但實際上4號結點的最短路徑是3+(-2)=1,這樣你就知道爲什麼Dijkstra算法不能用於有負權的邊的圖吧。


Bellman-Ford算法

Bellman-Ford算法是通過循環 n 次,每次循環都遍歷每條邊,進而更新結點的距離,每一次的循環至少可以確定一個點的最短路,所以循環 n次,就可以求出 n 個點的最短路。
在這裏插入圖片描述

for(int i=0; i<n; i++)
  for(int j=0; j<m; j++)
  {
  	 if(dist[a]+w<dist[b])
  	   dist[b] = dist[a] + w; //w是a->b的權重 
  }

在這個算法上延申,如果我們想求有邊數限制的最短路怎麼求呢,假如讓求從1號點到n號點的最多經過k條邊的最短距離怎麼求呢???

需要注意的是我們需要一個備份,先來看看爲什麼…
在這裏插入圖片描述
還是這張圖,假設我們限制邊數 k 爲 1,那麼外層循環只需要進行一次,肉眼可以看出,我們只能求出 1 和 2 和 3號結點的最短路,4號結點最短路是不存在的,可是當在枚舉所有條邊時,假如我們先枚舉的1——>2邊時,那麼2號結點最短路被更新爲2,這是沒問題的,可是,當我們再枚舉到2——>4邊時,4號結點最短路會被更新爲2+1=3,如果4後面還有結點的話,後面的所有結點都會被更新,可實際上,4結點是不存在最短路的,因爲我們限制了 k。那麼怎麼解決呢,其實很簡單,我們只需要用上一次的dist來更新即可,而我們把上一次更新後的dist放到備份裏存起來以備下一次更新用

有了備份,當枚舉1——>2到時,2結點被更新爲2,而枚舉到2——>4時,4號結點是用2號結點上一次的dist來更新的,而2號結點上一次dist是 +∞,而 +∞ + 1 > +∞,所以說4號結點是不會被更新的。
但是你可能有疑問,2——>4的權值如果是-1,那3號結點是可以更新成 +∞ - 1的,那它不還是更新了?它確實更新了,但是,我們在最後判斷時,還是會判斷4號沒有最短路的,這也就是爲什麼下道例題最後判斷條件寫成if(dist[n] > 0x3f3f3f3f/2) return -1。

例題:
853. 有邊數限制的最短路
給定一個n個點m條邊的有向圖,圖中可能存在重邊和自環, 邊權可能爲負數。

請你求出從1號點到n號點的最多經過k條邊的最短距離,如果無法從1號點走到n號點,輸出impossible。

注意:圖中可能 存在負權迴路 。

輸入格式
第一行包含三個整數n,m,k。

接下來m行,每行包含三個整數x,y,z,表示存在一條從點x到點y的有向邊,邊長爲z。

輸出格式
輸出一個整數,表示從1號點到n號點的最多經過k條邊的最短距離。

如果不存在滿足條件的路徑,則輸出“impossible”。

數據範圍
1≤n,k≤500,
1≤m≤10000 ,
任意邊長的絕對值不超過10000。

輸入樣例:
3 3 1
1 2 1
2 3 1
1 3 3

輸出樣例:
3

#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 510,M = 10010;

int n,m,k;
int dist[N],backup[N]; //backup數組爲上次
struct edges
{
   	int a,b,w; // a->b權值爲w的邊 
}edge[M];

int bellman_ford()
{
	memset(dist,0x3f,sizeof(dist));
	dist[1] = 0;
	
	for(int i=0; i<k; i++) 
	{
		memcpy(backup,dist,sizeof(dist)); //備份
		for(int j=0; j<m; j++)   // 枚舉所有邊 
		{
		   int a = edge[j].a, b = edge[j].b, w=edge[j].w;	
		   dist[b] = min(dist[b],backup[a]+w); // 用備份更新 
		}
	}
	if(dist[n] > 0x3f3f3f3f/2) return -1;
	return dist[n];
}
int main()
{
    cin >> n >> m >> k;
	for(int i=0; i<m; i++)
	{
	   int a,b,w;
	   cin >> a >> b >> w;
	   edge[i] = {a,b,w}; 	
	}	
	int t = bellman_ford();
	
	if(t == -1) cout << "impossible";
	else cout << t;
	return 0;
} 

SPFA算法

SPFA算法需要圖中沒有負環才能使用。其實大部分正權圖也是可以用SPFA算法做的,例如最上面的那到題就可以用SPFA做,效率還高於Dijkstra算法。
SPFA算法是在Bellman-Ford的基礎上優化後的算法。在Bellman-Ford算法中,如果某個點未被更新過,我們還是會用這個點去更新其他點,其實,該操作是不必要的,我們只需要拿更新過後的點去更新其他的點,因爲只有用被更新過的點更新其他結點x,x的距離纔可能變小

在這裏插入圖片描述
核心代碼:

int spfa()
{// bool st[N]: 存第 i 個點是不是在隊列中,防止存重複的點 
	memset(dist,0x3f,sizeof(dist));
	dist[1] = 0;
	
	queue<int> q; //存儲所有待更新的點
	q.push(1);  // 1號點入隊 
	st[1] = true;
	while(q.size()) // 隊列不空
	{
	   int t = q.front(); //取隊頭 
	   q.pop();
	   st[t] = false; // 代表這個點已經不在隊列了
	   
	   for(int i = h[t]; i!=-1; i=ne[i]) // 更新 t 的所有臨邊結點的最短路 
	   {
	   	 int j = e[i];
	   	 if(dist[j] > dist[t]+w[i])
	   	 {
	   	    dist[j] = dist[t] + w[i];
			if(!st[j])  //如果 j 不在隊列,讓 j 入隊 
			{
				q.push(j); 
				st[j] = true;  // 標記 j 在隊中 
			} 	    	
		 }
	   }	
	} 
	 if(dist[n] == 0x3f3f3f3f) return -1; // 不存在最短路 
	 return dist[n]; 
}

SPFA判斷負環

什麼是負環呢? 下圖左邊的2——>3——>4就是一個負環,因爲轉一圈後的距離是負的,右圖的 1 結點是應該自環,也屬於負環。
在這裏插入圖片描述
相比上一個代碼,多了一個cnt數組,cnt[x] 代表起點到x最短路所經的邊數,當 cnt[x] ≥ n 時,則說明 1——>x 這條路徑上至少經過 n 條邊 ,那麼也就是 1——>x 這條路徑上至少經過 n+1 個點,而我們知道總共只有 n 個點,說明至少存在兩個點是重複經過的,那麼這個點構成的環一定是負環,因爲只有負環纔會讓dist距離變小,否則我們爲什麼要兩次經過同一個點呢。

int spfa()
{
	queue<int> q; 
    for(int i=1; i<=n; i++) //將所有結點入隊
	{
	    st[i] = true;
		q.push(i);	  
    }
	while(q.size()) // 隊列不空
	{
	   int t = q.front(); //取隊頭 
	   q.pop();
	   st[t] = false; // 代表這個點已經不在隊列了
	   
	   for(int i = h[t]; i!=-1; i=ne[i]) // 更新 t 的所有臨邊結點的最短路 
	   {
	   	 int j = e[i];
	   	 if(dist[j] > dist[t]+w[i])
	   	 {
	   	    dist[j] = dist[t] + w[i];
	   	    cnt[j] = cnt[t] + 1; // t到起點的邊數+1 
	   	    
	   	    if(cnt[j] >= n) return true;// 存在負環 
			if(!st[j])  //如果 j 不在隊列,讓 j 入隊 
			{
				q.push(j); 
				st[j] = true;  // 標記 j 在隊中 
			} 	    	
		 }
	   }	
	} 
	 return false;// 不存在負環 
}

剛開始我們需要讓所有點都入隊,因爲 1 這個結點可能跟我們要找的負環是不連通的,這樣的話只通過 1 來是無法判斷的,所以,我們讓所有結點都入隊。

dist數組是否初始化在這裏是不影響的,因爲我們要求的是是否存在負環,不是距離。


Floyd算法

Floyd算法是基於動態規劃的,從結點 i 到結點 j 的最短路徑只有兩種:
1、直接 i 到 j
2、i 經過若干個結點到 k 再到 j
對於每一個k,我們都判斷 d[i][j] 是否大於 d[i][k] + d[k][j],如果大於,就可以更新d[i][j]了。

void floyd()
{
 for(int k=1; k<=n; k++)
   for(int i=1; i<=n; i++)
     for(int j=1; j<=n; j++)
      d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

三次循環完之後,遍歷完所有的 k 後,d[i][j] 存的就是 i——>j的最短路了。


內容若有不對,還望大佬指正😁

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章