天天看點

克魯斯卡爾算法、并查集

1. 并查集

看《大話資料結構》中的克魯斯卡爾算法時一直沒能真正了解代碼含義,關鍵是因為沒搞懂并查集,直到看了這篇文章。

并查集是一種樹型的資料結構,用于處理一些不相交集合的合并及查詢問題。對于圖中的兩個頂點A和B,通過并查集可以判斷是否AB之間是否有路徑。同時,也可以判斷将A和B相連之後是否可以形成環,因為如果AB之間有路徑的話,那麼再将A和B相連就會出現環。

并查集的基本操作包括:初始化,查找,合并。為了提高查找的效率通常會用到路徑壓縮。

我們使用數組

int parent[maxSize]

來存儲每個頂點的爸爸(不太嚴謹,暫時這樣叫吧),

parent[i]

存儲的就是頂點

i

的爸爸,如果頂點

i

與頂點

j

的爸爸相同,說明他們在一個集合中,那麼他們可以連通。

1.1初始化

初始化時可以将每個頂點的爸爸初始化為他自己,自己是自己的爸爸(。。。)

for(int i = 0; i < N; i++)
	parent[i] = i;
           

1.2查找

找爸爸,這裡找的是爸爸的爸爸的爸爸的。。。直到找到自己是自己爸爸的那個頂點。

int Find(int v) {
	while(parent[v] != v) {
		v = parent[v];
	}
	return v;
}
           

1.3合并

如果兩個頂點沒有共同的爸爸,即他們不在同一個樹中,則将兩者合并到一個樹中。

void Union(int s, int e) {
	int sRoot = Find(s);
	int eRoot = Find(e);
	if(sRoot != eRoot) {
		parent[sRoot] = eRoot;//s和e的祖宗不同,将兩個樹合成一個樹,這裡假設s的祖宗是e的祖宗的兒砸
	}
}
           

s和e的祖宗不同,将兩個樹合成一個樹,這裡假設s的祖宗是e的祖宗的兒砸,這個假設不一定合适,憑什麼s的祖宗輩分低,畢竟現在誰都想當爸爸(手動狗頭)。

合并的時候誰當爸爸也是有講究的,具體1.4路徑壓縮。

1.4路徑壓縮

引用這篇部落格的例子:

克魯斯卡爾算法、并查集

路徑壓縮是為了友善找到爸爸(老大)。

int Find(int v) {
	int root = v;
	while(parent[root] != root) {
		root = parent[root];//尋找祖先結點
	}
	//将沿途的非祖先結點的爸爸全部變為祖先結點(現在root為祖先結點)
	int tmp;
	while(v != root) {
		tmp = parent[v];//儲存v的爸爸
		parent[v] = root;//将v的爸爸改為最祖先的結點
		v = tmp;//對v的爸爸進行同樣的操作
	}
	return v;
}
           

我們也可以用遞歸的思路重寫上面的代碼:

int Find(int v) {
	if(v != parent[v]) {
		parent[v] = Find(parent[v]);
	}
	return parent[v];
}
           

路徑壓縮後樹的深度明顯減少了,利于查找,當我們合并兩個樹時,選擇層數多的樹當爸爸會讓整個樹的深度更小。那麼我們在合并操作時,最好讓深度比較深的當爸爸。

克魯斯卡爾算法、并查集
克魯斯卡爾算法、并查集

我們定義一個數組存放每個結點的深度(或者稱為rank),初始化為1,即每個結點隻包含自己。

int rank[maxSize]

.

for(int i = 0; i < N; i++)
	rank[i] = 1;
           

重新定義合并函數:

bool Union(int s, int e){
	int sRoot = Find(s);
	int eRoot = Find(e);
	if(sRoot != eRoot){
		if(rank[sRoot] < rank[eRoot]){//sRoot是小樹,将其加入大樹中
			parent[sRoot] = eRoot;
			rank[eRoot] += rank[sRoot];
		}else{//eRoot為小樹,将其加入sRoot中
			parent[eRoot] = sRoot;
			rank[sRoot] += rank[eRoot];
		}
		return true;//s和e爸爸不同,需要合并傳回true
	}
	return false;//s和e爸爸相同,不需要合并,傳回false
}
           

2. 克魯斯卡爾算法

克魯斯卡爾算法的原理在此不再解釋,貪心算法的典型應用。

以下面這個圖為例:

克魯斯卡爾算法、并查集

上代碼:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

/******圖的鄰接矩陣存儲結構*******/
typedef int VertexType;	//頂點類型, 使用者自定義
typedef int EdgeType;	//權值類型, 使用者自定義
#define MAXVEX 9		//最大頂點個數
#define INFINITY INT_MAX

vector<int> parent(MAXVEX);
vector<int> Rank(MAXVEX);

typedef struct
{
	vector<VertexType> vexs;	//頂點矩陣
	vector<vector<EdgeType>> arc;//鄰接矩陣, 二維矩陣
	int numVertexs, numEdges;	//圖中的頂點數和邊數
}MGraph;

struct Edge {
	int begin;//這裡是無向圖,起始節點預設為下标較小的點
	int end;
	int weight;
	bool operator < (const Edge& a) const {
		return weight < a.weight;
	}
};//邊集數組

int Find(int v) {
	if (v != parent[v])
		parent[v] = Find(parent[v]);
	return parent[v];
}

bool Union(int s, int e) {
	int sRoot = Find(s);
	int eRoot = Find(e);
	if (sRoot != eRoot) {
		if (Rank[sRoot] < Rank[eRoot]) {//sRoot是小樹,将其加入大樹中
			parent[sRoot] = eRoot;
			Rank[eRoot] += Rank[sRoot];
		}
		else {//eRoot為小樹,将其加入sRoot中
			parent[eRoot] = sRoot;
			Rank[sRoot] += Rank[eRoot];
		}
		return true;//s和e爸爸不同,需要合并傳回true
	}
	return false;//s和e爸爸相同,不需要合并,傳回false
}
//最小生成樹克魯斯卡爾算法
void MinSpanTree_Kruskal(MGraph G) {
	vector<Edge> edges(G.numEdges);//邊集數組
	//根據G的鄰接矩陣表示得到邊集數組
	int k = 0;
	for (int i = 0; i < G.numVertexs; i++) {
		for (int j = i + 1; j < G.numVertexs; j++) {//無向圖的鄰接矩陣是對稱的,不需要将重複的邊添加到邊集數組中
			if (G.arc[i][j] != INT_MAX) {
				edges[k].begin = i;
				edges[k].end = j;
				edges[k].weight = G.arc[i][j];
				k++;
			}
		}
	}

	//對邊集數組進行排序
	sort(edges.begin(), edges.end());
	//初始化并查集與Rank
	for (int i = 0; i < G.numVertexs; i++) {
		parent[i] = i;//初始化數組值
		Rank[i] = 1;
	}

	for (int i = 0; i < G.numEdges; i++) {
		if (Union(edges[i].begin, edges[i].end)) {
			cout << "(" << edges[i].begin << ", " << edges[i].end << ")\t" << edges[i].weight << endl;
		}
	}
}

int main()
{
	MGraph G;
	G.numVertexs = 9;//9個頂點
	G.numEdges = 15;//15條邊
	for (int i = 0; i < G.numVertexs; i++)
		G.vexs.push_back(i);
	//鄰接矩陣
	G.arc = {
		{ 0, 10, INFINITY, INFINITY, INFINITY, 11, INFINITY, INFINITY, INFINITY },
		{ 10, 0, 18, INFINITY, INFINITY, INFINITY, 16, INFINITY, 12 },
		{ INFINITY, 18, 0, 22, INFINITY, INFINITY, INFINITY, INFINITY, 8 },
		{ INFINITY, INFINITY, 22, 0, 20, INFINITY, 24, 16, 21 },
		{ INFINITY, INFINITY, INFINITY, 20, 0, 26, INFINITY, 7, INFINITY },
		{ 11, INFINITY, INFINITY, INFINITY, 20, 0, 17, INFINITY, INFINITY },
		{ INFINITY, 16, INFINITY, INFINITY, INFINITY, 17, 0, 19, INFINITY },
		{ INFINITY, INFINITY, INFINITY, 16, 7, INFINITY, 19, 0, INFINITY },
		{ INFINITY, 12, 8, 21, INFINITY, INFINITY, INFINITY, INFINITY, 0 }
	};
	MinSpanTree_Kruskal(G);
	system("pause");
	return 0;
}
           

在搞清楚并查集之後,再看克魯斯卡爾算法的實作就比較簡單了。

最核心的部分就是這段:

for (int i = 0; i < G.numEdges; i++) {
	if (Union(edges[i].begin, edges[i].end)) {
		cout << "(" << edges[i].begin << ", " << edges[i].end << ")\t" << edges[i].weight << endl;
	}
}
           

邊排序結果:

克魯斯卡爾算法、并查集

運作結果:

克魯斯卡爾算法、并查集