最小生成樹與最短路徑算法


簡介

前面寫了一遍關於圖的存儲結構和遍歷算法的文章,這一篇打算回顧一下圖的一些常用算法,包括最小生成樹、最短路徑算法。這些算法很基礎,在生活中經常用到,打算自己動手實現一下,加深理解~~

最小生成樹

生成樹的概念:r若圖是連通的無向圖或強連通的有向圖,則從任何一個頂點出發調用一次BFS或者DFS便可訪問圖中所有的頂點,這種情況下,圖中所有頂點加上遍歷過程中經歷的邊構成的子圖成為原圖的生成樹。對於不連通和不是強連通的有向圖,這樣得到的是生成森林。

最小生成樹:對於加權圖,權值最小的生成樹成為最小生成樹。可以用kruskal(克魯斯卡爾)算法或prim(普里姆)算法求出,這兩種算法依據的思想是貪心思想

prim(普利姆)算法

第一步:選擇一個初始頂點V1,設置一個訪問數組visit[]和最小距離數組distance[]。最小距離數組保存當前已訪問頂點的生成樹中到每個頂點最小的距離。
第二步:根據初始頂點V1,初始化visit和distance數組。
第三步:選擇distance中權重最小的那個頂點,以此頂點為起始點更新distance數組,直至所有頂點都已訪問。

未優化的prim算法時間復雜度為O(N^2),其C語言描述如下,:

/* 輸入分別為:鄰接鏈表、保存生成樹遍歷的數組、最小的權重 */
int prim( GraphAdjList * G, int tree[], int * weight )
{
	if( G == NULL ) return 0;

	int num_vertex = G->numVertex;
	int index = 0;
	int next_visit = 0;    // 下一個要選擇的頂點

	int visit[num_vertex] = {0};
	int distance[num_vertex] = {MAX_INT};

	int first_node  = 0;
	EdgeNode * edge = G->adjList[first_node]->firstEdge;
	/* 初始化距離數組 */
	while( edge )
	{
		/* 當前訪問節點的下標 */
		int vertex_index = edge->adjvex;
		distance[vertex_index] = edge->weight;
		edge = edge->next;
	}
	visit[first_node] = 1;
	tree[index++] = first_node;

	for( int i = 1; i < num_vertex; i++ )
	{
		int min_weight = MAX_INT;
		/* 從距離數組中找出最小距離的頂點 */
		for( int j = 0; j < num_vertex; j++ )
		{
			if( visit[j] == 0 && distance[j] < min_weight )
			{
				min_weight = distance[j];
				next_visit = j;
			}
		}
		visit[next_visit] = 1;
		tree[index++] = next_visit;
		*weight += min_weight;
		/* 新加入節點之后,更新距離數組 */
		EdgeNode * edge = G->adjList[next_visit]->firstEdge;
		while( edge )
		{
			/* 當前訪問節點的下標 */
			int vertex_index = edge->adjvex;
			if( visit[vextex_index] == 0 && edge->weight < distance[vertex_index] )
			{
				distance[vertex_index] = edge->weight;
			}
			edge = edge->next;
		}
	}
	return 1;
}

kruskal(克魯斯卡爾)算法

kruskal算法需要判斷是否存在回路問題,判斷是否存在回路問題方法很多,下面列出兩種比較簡單的方法。

如何判斷存在回路

第一種方法

1、將度數小於2的頂點刪去,並將與其相連的頂點度數減1。若是有向圖則刪除入讀為0的頂點,並將與該點相連的頂點入讀減1。
2、重復上述過程,若只剩下頂點,則存在回路,否則不存在。

第二種方法

1、將未遍歷的節點塗成白色,已遍歷的節點塗成灰色,若該節點的所有相鄰邊都被遍歷了,則塗成黑色。
2、若遍歷時,有一個節點的一條未遍歷的邊指向一個灰色節點,則存在回路。

實現過程

因為這個算法對稀疏圖來說效率比較高,它的時間復雜度為O(ElogE)。所以這里我采用保存邊集的數據結構實現kruskal算法。回路的判斷方法是借鑒數據結構書上的,即對每一條邊,進行編號,如果待加入的邊不屬於同一集合編號內,即可加入,如下圖所示:

A B C D E F 初始化分別在一個邊集,編號分別為 0 1 2 3 4 5

此時** A B C **節點構成一個邊集,其編號都設為0.
** D E **節點構成一個邊集,其編號設為4

** A B C F **節點構成一個邊集,其編號設為0

** A B C D E F **節點構成一個邊集,其編號設為0

其C語言描述如下:

typedef struct Edge
{
	int begin;
	int end;
	int weight;
} Edge[EDGE_NUM];
/* 邊的數目為EDGE_NUM,頂點的數目為VERTEX_NUM */
int kruskal( Edge edge[], Edge tree[], int * weight )
{
	int edge_set[NUM];  // 保存邊的編號
	int index  = 0 ;

	sort( edge );  // 按權重對邊集排序
	/* 初始化邊集數組,把每個頂點單獨放在一個編號里 */
	for( int i = 0; i < VERTEX_NUM; i++ )
	{
		edge_set[i] = i;
	}
	for( int j = 0; j < EDGE_NUM; j++ )
	{
		/* 選出第j小的邊,所在的頂點 */
		int begin = edge[j].begin;
		int end = edge[j].end;
		/* 找到這兩個頂點所在邊集的編號 */
		int edge_num1 = edge_set[begin];
		int edge_num2 = edge_set[end];

		if( edge_num1 != edge_num2 )
		{
			/* 如果這兩個頂點不在同一個邊集上,則把這條邊加入到生成樹tree數組中 */
			tree[index++] = edge[j];
			for( int k = 0; k < NUM; k++ )
			{
				/* 將這兩個頂點邊集合並成一個邊集,遍歷所有這條邊集的頂點,使其都屬於同一個邊集 */
				if( edge_set[k] == edge_num2 )
				{
					edge_set[k] = edge_num1;
				}
			}
		}
	}
}

最短路徑

本科學過的最短路徑問題,但當時僅限於概念上的理解,並沒有實際動手實現過。這里我決定使用Dijkstra算法和Floyd算法,實現最短路徑問題。Dijkstra算法時間復雜度要低於Floyd算法,但Dijkstra不能處理負權的情況??

Dijkstra算法

這個算法跟prim算法很相似,都是依據貪心算法的思想,不同點在於prim算法保存的距離數組,是整個生成樹到每個頂點的距離,而Dijkstra算法保存起點到每個頂點的最短距離。但過程和prim算法類似,步驟如下:
第一步:選擇一個起始點,初始化距離數組distance和訪問數組visit數組。
第二步:從距離數組中選擇距離最小的頂點,並更新distance數組。
第三步:重復第二步,直到所有的頂點都已被訪問。

/* 輸入分別為:鄰接鏈表、保存遍歷經過的頂點、最小的路徑長度 */
int dijkstra( GraphAdjList * G, int tree[], int * length )
{
	if( G == NULL ) return 0;

	int num_vertex = G->numVertex;
	int index = 0;
	int next_visit = 0;    // 下一個要選擇的頂點

	int visit[num_vertex] = {0};
	int distance[num_vertex] = {MAX_INT};

	int first_node  = 0;
	EdgeNode * edge = G->adjList[first_node]->firstEdge;
	/* 初始化距離數組 */
	while( edge )
	{
		/* 當前訪問節點的下標 */
		int vertex_index = edge->adjvex;
		distance[vertex_index] = edge->weight;
		edge = edge->next;
	}
	visit[first_node] = 1;
	tree[index++] = first_node;

	for( int i = 1; i < num_vertex; i++ )
	{
		int min_weight = MAX_INT;
		/* 從距離數組中找出最小距離的頂點 */
		for( int j = 0; j < num_vertex; j++ )
		{
			if( visit[j] == 0 && distance[j] < min_weight )
			{
				min_weight = distance[j];
				next_visit = j;
			}
		}
		visit[next_visit] = 1;
		tree[index++] = next_visit;
		*weight += min_weight;
		/* 新加入節點之后,更新距離數組 */
		EdgeNode * edge = G->adjList[next_visit]->firstEdge;
		while( edge )
		{
			/* 當前訪問節點的下標 */
			int vertex_index = edge->adjvex;
			/* 與prim算法的不同之處 */
			if( visit[vextex_index] == 0 && edge->weight + distance[next_visit] < distance[vertex_index] )
			{
				distance[vertex_index] = edge->weight + distance[next_visit];
			}
			edge = edge->next;
		}
	}
	return 1;
}

Floyd算法

Floyd算法是用動態規划思想求解的,它可以求出每一對頂點之間的距離,時間復雜度為O(N^3)。動態規划很重要的一步就是找到該問題的最優子結構。在最短路徑中,可以看出從** A B 的最短距離,要么是 A B **直接相連的距離,要么經過一個中間節點。其最優子結構可以定義如下:

dist( i, j ) = min( weight( i, j ), weight( i, k ) + weight( k , j ) ) { i < k < j }

其算法過程描述如下:
第一步:初始化每兩頂點之間的距離
第二步:對每一對頂點查看是否有一個頂點** k 使得從 i 到 j**之間的距離變得更短,如果存在就更新這個距離值。

這一題用鄰接表做比較麻煩,因為需要找到一個頂點 k 既與 i 節點相連,又與 j 節點相連,那就得同時遍歷 i 和 j 的鄰接點。這一題采用鄰接矩陣解出的,其C語言描述如下:

/* path[i][j]表示 i 到 j 的最短距離  */
int floyd( MGraph * G, int path[][VERTEX_NUM] )
{
	int num_vertex = G->numVertex;
	/* 初始化path數組 */
	for( int i = 0; i < num_vertex; i++ )
	{
		for( int j = 0; j < num_vertex; j++ )
		{
			path[i][j] = G->edge[i][j];
		}
	}

	for( int k = 0; k < num_vertex; k++ )
	{
		for( int i = 0; i < num_vertex; i++ )
		{
			for( int j = 0; j < num_vertex; j++ )
			{
				/* 遍歷找到是否有一個中間節點,使 i 和 j 之間的距離更小 */
				if( ( path[i][k] + path[k][j] ) < path[i][j] )
				{
					path[i][j] = path[i][k] + path[k][j];
				}
			}
		}
	}
}

總結

理論和實踐總是有一段距離的,自己動手寫了一下,感覺自己多這兩種算法的理解更深了。就像《程序員修煉之道》里說的那樣,要做一個注重實踐的程序員。在實現上述算法的過程中,借鑒了不少博主的文章,忽然間發現,有時候寫文章不僅僅是為了自己,因為你寫的文章有一天或許會幫助到其他人也說不定啊哈哈哈

參考

[1] 博客園 Veegin
[2] 博客園 華山大師兄

EOF


免責聲明!

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



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