1. 程式人生 > >求圖的最短路徑---四中演算法優化

求圖的最短路徑---四中演算法優化

最短路徑的四種演算法!

目錄

1、Floyd-Warshall演算法 --- 只有五行的演算法

2、Dijkstra演算法 --- 通過邊實現鬆弛

3、Bellman-Ford --- 解決負權邊

4、Bellman-Ford的佇列優化

 


1、Floyd-Warshall演算法 --- 只有五行的演算法

  描述:

在上面的圖中,有四個頂點八條邊,邊有長有短,我們需要求任意兩個頂點之間的最短路徑。

這個問題也被稱為“多源最短路徑”問題。

  思路:

首先,我們需要一個矩陣來儲存這個圖,沒錯!就是需要鄰接矩陣。這裡為e[][]。例如頂點1到頂點2的距離為2,那麼

e[1][2] == 2 。頂點2沒有辦法直接到頂點4,那麼e[2][4] == inf(正無窮) 。另外,我們規定自己到自己的距離為0 。

                                              

雖然深度優先搜尋或廣度優先搜尋可以解決最短路徑的問題,可是還有沒有別的方法呢??

我們先來看怎麼求路徑,除了一個頂點直接到另一個頂點之外,還有一種方法就是,從一個頂點先到一箇中轉點,然後從這個中轉點再到另一個點,然後對比一下,這兩個方法哪種路徑更短,我們就更新路徑的值,這下就可以做到使多源路徑最短。

舉個例子:假如我們現在只能經過一號頂點來判斷任意兩點之間是否具有最短路徑。我們用下面的程式碼來實現!

for (i = 1; i <= n; i++)
{
	for (j = 1; j <= n; j++)
	{
		if (e[i][j] > e[i][1] + e[1][j])
		{
			e[i][j] > e[i][1] + e[1][j];
		}
	}
}

但是現在還做不到將所有的任意兩點之間的路徑更新為最短,那麼現在只需要加一個迴圈就行了,那就是迴圈所有需要經過的頂點。

for (k = 1; k <= n; k++)
{
	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= n; j++)
		{
			if (e[i][j] > e[i][k] + e[k][j])
			{
				e[i][j] > e[i][k] + e[k][j];
			}
		}
	}
}

這也就是他的名字由來“只有五行程式碼的演算法”。

  原始碼

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <stdlib.h>

/*
* 在一個有向圖中,兩個頂點往往不是最短長度,往往需要另一個頂點去過度
  本程式用一個演算法實現更新鄰接矩陣,實現兩頂點之間的最短路徑
* 郭文峰
* 2018/10/25
*/

int main(void)
{
	int e[51][51] = { 0 };
	int i = 0;
	int j = 0;
	int k = 0;
	int n = 0;
	int m = 0;
	int inf = 99999999999;
	int t1 = 0;
	int t2 = 0;
	int t3 = 0;

	scanf("%d%d", &n, &m);

	//初始化鄰接矩陣
	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= n; j++)
		{
			if (i == j)
				e[i][j] = 0;
			else
				e[i][j] = inf;
		}
	}

	//輸入各個頂點之間的距離
	for (i = 1; i <= m; i++)
	{
		scanf("%d%d%d", &t1, &t2, &t3);
		e[t1][t2] = t3;
	}

	//更新鄰接矩陣最短路徑
	//分別通過K頂點過度,更新路徑
	for (k = 1; k <= n; k++)
	{
		for (i = 1; i <= n; i++)
		{
			for (j = 1; j <= n; j++)
			{
				if (e[i][j] > e[i][k] + e[k][j])
					e[i][j] = e[i][k] + e[k][j];
			}
		}
	}

	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= n; j++)
		{
			printf("%3d", e[i][j]);
		}
		printf("\n");
	}

	system("pause");
	return 0;
}

  執行結果

                                                             

這個方法求最短路徑,他的時間複雜度為O(N^3)。但是他只有五行程式碼,寫起來非常的容易,可是有點慢,那麼下面就要引入Dijkstra演算法。

另外!Floyd-Warshall演算法並不能解決有負權迴路的問題,例如:

                                        

在上面的圖中,1->2->3這樣的路徑每繞一圈最短路徑就會減少。

2、Dijkstra演算法 --- 通過邊實現鬆弛

  描述:

此演算法是用來指定一個點(源點)到其餘各個頂點的最短路徑,也叫做“單源最短路徑”。

例如求下圖中的1號頂點到2、3、4、5、6號頂點的最短路徑。

  思路:

                                                         

與上面的演算法一樣,還是用一個鄰接矩陣來儲存這個圖。

                                               

還需要一個一維陣列dis來儲存一號頂點到其餘各個頂點的初始路程。

                                                    

剛開始,我們將dis數組裡的值叫做估計值。

首先我們找到離一號頂點最近的頂點,然後迴圈判斷一號頂點直接到該頂點的路徑是否大於一號頂點先到其他頂點再到此頂點的路徑。

例如:這裡離一號頂點最近的是二號頂點,但是二號頂點到一號頂點已經是最短路徑了,所以看二號頂點連線著那些頂點。先判斷 

if (dis[3] > dis[2] + e[2][3])
    dis[3] = dis[2] + e[2][3];

如果一號頂點離三號頂點的路徑大於一號頂點先到二號頂點再到三號頂點,那麼就更新路徑資訊。

以此類推。

  原始碼:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <stdlib.h>

/*
* Dijkstra演算法,通過邊鬆弛求最短路徑
* 郭文峰
* 2018/10/25
*/

int main(void)
{
	int e[51][51] = { 0 };
	int book[51] = { 0 };
	int dis[50] = { 0 };
	int inf = 999999999;
	int i = 0;
	int j = 0;
	int n = 0;
	int m = 0;
	int t1 = 0;
	int t2 = 0;
	int t3 = 0;
	int min = 0;
	int u = 0;
	int v = 0;

	//輸入有向圖有n個頂點和m條邊
	scanf("%d%d", &n, &m);

	//初始化鄰接矩陣
	for (i = 1; i <= n; i++)
	{
		for (j = 1; j <= n; j++)
		{
			if (i == j)
				e[i][j] = 0;
			else
				e[i][j] = inf;
		}
	}

	//輸入各個頂點之間邊的長度
	for (i = 1; i <= m; i++)
	{
		scanf("%d%d%d", &t1, &t2, &t3);
		e[t1][t2] = t3;
	}

	//初始化dis陣列
	for (i = 1; i <= n; i++)
		dis[i] = e[1][i];


	book[1] = 1;

	//演算法核心部分
	for (i = 1; i <= n - 1; i++)
	{
		//找到離源點最近的一個點
		min = inf;
		for (j = 1; j <= n; j++)
		{
			if (dis[j] < min && book[j] == 0)
			{
				min = dis[j];
				u = j;
			}
		}
		//標記u點
		book[u] = 1;

		//更新最短路徑
		for (v = 1; v <= n; v++)
		{
			
			if (dis[v] > dis[u] + e[u][v])
			{
				dis[v] = dis[u] + e[u][v];
			}
			
		}
	}

	for (i = 1; i <= n; i++)
	{
		printf("%d ", dis[i]);
	}

	system("pause");
	return 0;
}

  執行結果:

                                                            

上面的程式碼實現複雜度為O(N^2)。

3、Bellman-Ford --- 解決負權邊

  描述:

Dijkstra演算法雖然好,然是並不能解決負權邊的問題。Bellman-Ford演算法無論是思想上還是程式碼實現上都堪稱完美。

  思路:

核心程式碼就四句,我們先來看看。

for (k = 1; k <= n - 1; k++)
{
	for (i = 1; i <= m; i++)
	{
		if (dis[v[i]] > dis[u[i]] + w[i])
			dis[v[i]] = dis[u[i]] + w[i];
	}
}

上面的程式碼外迴圈共迴圈了n-1次,因為1號頂點不需要和自己比,只需要和別的頂點比較,所以是n-1 。內迴圈迴圈了m次,就是列舉每一條邊。dis陣列的作用於Dijkstra演算法一樣,用來記錄源點到各個頂點之間的距離。u、v、w、陣列是用來記錄邊的資訊,就是鄰接表。

if (dis[v[i]] > dis[u[i]] + w[i])
	dis[v[i]] = dis[u[i]] + w[i];

上面兩個程式碼的意思是,看看一號頂點到v[i]頂點是否可以通過u[i]進行縮短,這種操作叫做鬆弛。

如果想把所有邊都鬆弛一遍,只需要加一個迴圈就行。

for (i = 1; i <= m; i++)
	{
		if (dis[v[i]] > dis[u[i]] + w[i])
			dis[v[i]] = dis[u[i]] + w[i];
	}

 

然後跟Dijkstra演算法的思想一樣,還是用dis陣列來儲存所有頂點到一號頂點之間的路徑。然後迴圈n-1次鬆弛每條邊。

  原始碼:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <stdlib.h>

/*
* 本程式是BellmanFord演算法求最短路徑。
  用到了鄰接表三個陣列分別為u[]、v[]、w[]。
  chenk來判斷是否更新結束。
  flag來判斷是否有負權值。
* 郭文峰
* 2018/10/26
*/

int main(void)
{
	int dis[10] = { 0 };
	int bak[10] = { 0 };
	int i = 0;
	int j = 0;
	int k = 0;
	int check = 0;
	int flag = 0;
	int n = 0;
	int m = 0;
	int u[10] = { 0 };
	int v[10] = { 0 };
	int w[10] = { 0 };
	int inf = 999999999;

	//輸入n個頂點和m條邊
	scanf("%d%d", &n, &m);

	//輸入資料建立鄰接表
	for (i = 1; i <= m; i++)
	{
		scanf("%d%d%d", &u[i], &v[i], &w[i]);

	}

	//初始化dis陣列
	for (i = 1; i <= n; i++)
	{
		dis[i] = inf;
	}
	dis[1] = 0;

	for (k = 1; k <= n - 1; k++)
	{
		//先讓bak陣列等於dis陣列
		for (i = 1; i <= n; i++)
		{
			bak[i] = dis[i];
		}

		//更新最短路徑
		for (j = 1; j <= m; j++)
		{
			if (dis[v[j]] > dis[u[j]] + w[j])
				dis[v[j]] = dis[u[j]] + w[j];
		}

		check = 0;
		//判斷是否更新
		for (i = 1; i <= n; i++)
		{
			if (bak[i] != dis[i])
			{
				check = 1;
				break;
			}
		}
		if (check == 0)
			break;

	}

	//判斷是否有負權值
	for (j = 1; j <= m; j++)
	{
		if (dis[v[j]] > dis[u[j]] + w[j])
			flag = 1;
	}

	if (flag == 1)
		printf("有負權值!");
	else
	{
		for (i = 1; i <= n; i++)
		{
			printf("%d ", dis[i]);
		}
	}

	system("pause");
	return 0;
}

  執行結果:

                                                        

此演算法的時間複雜度小於O(NM)。

4、Bellman-Ford的佇列優化

  描述:

Bellman-Ford演算法的時間複雜度並不算小,所以我們需要優化它,我們需要每次僅對最短路估計值發生變化了的頂點的所有出邊執行鬆弛操作。我們可以用一個佇列來維護他。

  思路:

每次選取隊首頂點u,對頂點u的所有出邊進行鬆弛操作。例如有一條u->v的邊,如果一條邊使得源點到頂點v的最短路程變短,且頂點v不在當前的佇列中,就將頂點v放入隊尾。還需要一個數組來判重,不能重複出現,在頂點u的所有出邊鬆弛完畢後,就將頂點u出隊,接下來不斷從佇列中取出新的隊首頂點再進行如上操作,直到佇列為空為止。

 

在這樣一個圖裡,先從一號頂點開始,對一號頂點的出邊進行鬆弛,例如2號頂點鬆弛成功,就將2號頂點入隊……

在一號頂點所有邊都鬆弛完畢後,一號頂點出隊,然後到2號頂點,再對2號頂點的所有出邊進行鬆弛。

以此類推。

  原始碼:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <stdlib.h>

/*
* 本程式在對BellmanFord演算法進行了佇列的優化
* 郭文峰
* 2018/10/26
*/

int main(void)
{
	int dis[10] = { 0 };
	int i = 0;
	int k = 0;
	int n = 0;
	int m = 0;
	int head = 1;
	int tail = 1;
	int que[101] = { 0 };
	int first[10] = { 0 };
	int next[10] = { 0 };
	int u[10] = { 0 };
	int v[10] = { 0 };
	int w[10] = { 0 };
	int book[10] = { 0 };
	int inf = 99999;


	scanf("%d%d", &n, &m);

	for (i = 1; i <= n; i++)
	{
		dis[i] = inf;
	}
	dis[1] = 0;

	//初始化first陣列
	for (i = 1; i <= n; i++)
	{
		first[i] = -1;
	}

	for (i = 1; i <= m; i++)
	{
		scanf("%d%d%d", &u[i], &v[i], &w[i]);
		//建立鄰接表
		next[i] = first[u[i]];
		first[u[i]] = i;
	}

	//將一號頂點入隊
	que[tail] = 1;
	tail++;
	book[1] = 1;

	while (head < tail)
	{
		k = first[que[head]];//當前需要處理的隊首頂點
		//掃描當前頂點的所有邊
		while (k != -1)
		{
			if (dis[v[k]] > dis[u[k]] + w[k])//判斷邊是否符合鬆弛的條件
			{
				dis[v[k]] = dis[u[k]] + w[k];//更新

				if (book[v[k]] == 0)
				{
					que[tail] = v[k];
					tail++;
					book[v[k]] = 1;

				}
			}
			k = next[k];
		}
		book[que[head]] = 0;
		head++;
	}

	//輸出一號頂點到其餘各頂點的最短路徑
	for (i = 1; i <= n; i++)
	{
		printf("%d ", dis[i]);
	}

	system("pause");
	return 0;
}





  執行結果: