用克魯斯卡爾(Kruskal)算法構造最小生成樹(最小支撐樹)

Kruskal算法和Prim算法相比,就是Kruskal算法從邊出發,不斷尋找當前未添加進Et的、且權值最小的邊,若添加後不形成環,則添加成功;否則跳過,繼續嘗試添加下一條邊。最後,判斷邊的數量arcnum是否是點的數量vexnum-1,若是則最小生成樹構造成功,否則失敗。

Prim算法與頂點相關時間複雜度O(|V|²),所以適合頂點少邊多的圖;

Kruskal反之,算法與邊相關,時間複雜度爲O(|E|log|E|),所以適合邊少頂點多的圖;

首先Vt中沒有邊,直接選取權值最小的邊,即V1--V3,之後V3的parent更新,更新爲parent[3] = 1,表明頂點V3的根節點爲V1;

(c)、(d)、(e)圖同理,現在到(f),而目前權值最小的是5,有V1--V4,V3--V4,V2--V3這三條邊,選擇哪條呢?如果添加前面兩條,則會形成環,這不符合最小生成樹,所以只能添加V1--V4這條邊

關鍵是如何判斷是否形成了環呢?這就需要用到並查集的知識了。這裏的條件是,若一條邊的2個頂點,它們通過並查集查詢到的根節點若相同,則判定形成了環。

什麼是並查集呢?並查集是一種樹形結構,又叫“不相交集合”,保持了一組不相交的動態集合,每個集合通過一個代表來識別,代表即集合中的某個成員,通常選擇根做這個代表。

這裏到(d)圖時,V3的根節點是V1,V5的根節點是V2,V6的根節點是V4。從(d) --> (e),連接了V3--V6,查詢V3的根節點是V1,查詢V6的根節點是V4,因此更新parent[4] = 1

後面同理

#include <iostream>
#include <algorithm> 
using namespace std;

//	Kruskal算法只與邊有關,故這裏只存儲邊的關係
//	並規定vstart爲編號小的頂點,vend爲編號大的頂點 
typedef struct{
	int vstart;
	int vend;
	int weight;
}Edge;
//	按照權值,從小到大排序 
bool edgeCmp(const Edge& e1, const Edge& e2)
{
	return e1.weight < e2.weight;
}

//	該圖由arcnum條邊組成 
typedef struct{ 
	int vexnum;
	int arcnum;
	vector<Edge> edge;
}Graph;

bool newGraph(Graph &g)
{
	cout << "-------準備使用創建無向帶權圖-------" << endl;
	cout << "請輸入頂點數和邊數(空格隔開): ";
	cin  >> g.vexnum >> g.arcnum;
	//	圖可以沒有邊,但不能沒有頂點
	//	若無向圖有n個頂點,則最多有n*(n-1)/2條邊(無向完全圖) 
	if( g.vexnum<0 || g.arcnum<=0 || g.arcnum>(g.vexnum*(g.vexnum-1)/2) ){
		cerr << "數據輸入有誤,請檢查數據!" << endl; 
		g.vexnum = g.arcnum = 0;
		return false;
	}
	
	cout << "請輸入" << g.arcnum << "條邊的起始頂點、終止頂點和權值(空格隔開)" << endl;
	int vstart, vend, weight;
	int n = g.arcnum;
	while( n-- ){
		cin >> vstart >> vend >> weight;
		if( vstart<=0 || vend<=vstart || weight<=0 ){
			cerr << "數據輸入有誤,請檢查數據!" << endl;
			g.edge.clear();
			return false;	
		}
		g.edge.push_back( Edge{vstart,vend,weight} );	
	}	 
	
	return true;				
} 

void printRes(vector<Edge>& Et, int cost)
{
	cout << "當前最小生成樹的邊爲:"; 
	for(auto e : Et )
		cout << "V" + to_string(e.vstart) 
			 << "--"
			 << "V" + to_string(e.vend) << ", ";
	cout << endl << "當前最小生成樹的權值爲:" << cost << endl;
	cout << endl; 		
}

//	查找結點Vi的根節點 
int findRoot(int parent[], int i)
{
	int root = i;
	//	若等於,說明根爲本身,已經找到 
	while( root != parent[root] )
		root = parent[root];
	return root;	
} 

bool Kruskal(Graph &g)
{
	vector<Edge> Et;	//存儲目最小生成樹的邊 
	int cost = 0;
	//	並查集的使用,我這裏定義數組下標i爲頂點Vi
	//	數組的值parent[i]爲頂點Vi的父節點的下標
	//	初始時Vt沒有邊,因此每個節點都是根節點 
	int parent[g.arcnum+1];
	for(int i = 1; i <= g.arcnum; ++i)
		parent[i] = i;
	//	將所有邊的權值按照從小到大排序 
	sort(g.edge.begin(), g.edge.end(), edgeCmp);
	for(int i = 0; i < g.arcnum; ++i)
	{
		//	判斷圖中是否有環,使用並查集查詢
		//	若該邊起點,在並查集中的根節點
		//	與該邊終點,在並查集中的根節點相同,則判定有環
		int root1 = findRoot(parent, g.edge[i].vstart);
		int root2 = findRoot(parent, g.edge[i].vend);
		//	不形成環 
		if( root1 != root2 ){
			//	更新權值 
			cost += g.edge[i].weight;
			//	更新並查集 
			parent[root2] = root1;
			//	更新Vt 			
			Et.push_back(g.edge[i]);
			printRes(Et, cost);
		}
		//	形成環,則忽略此邊		
	}
	//	最後判斷邊和頂點數量關係,看是否構成了最小生成樹 
	if( g.vexnum-1 != Et.size() ){
		cerr << "此圖不能構造最小生成樹!" << endl;
		return false; 
	} 
	
	return true;
}

int main()
{
	Graph g;
	if( newGraph(g) ){
		Kruskal(g); 
	}	
	
	return 0;
}

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