1. 程式人生 > >並查集-用並查集判斷圖中是否有環(能夠應用到kruskal的最小生成樹)

並查集-用並查集判斷圖中是否有環(能夠應用到kruskal的最小生成樹)

     先不介紹並查集的概念,先從它的應用說起吧,它有兩個功能,第一就是判斷給定的一個節點是否屬於某一個集合,更確切的說是屬於哪個集合。第二個功能就是合併兩個集合。

      給定一組資料,如:1, 2, 3, 4, 5, 6, 7, 8,  9,他們是一個大的集合,但是也可以將他們每一個數字看成一個獨立的集合,然後我們通過合併來形成一個由他們所有組成的大集合。有的人很奇怪,他們已經是一個集合了,為什麼還要重新把他們組織起來呢,而且如果要查詢某一個元素我們直接遍歷這個集合不就可以了嗎,時間複雜度是線性的。熟悉並查集的人可能會馬上說出:利用並查集的兩種操作:並和查,我們就能夠重新組織這些數字,當他們有一定的邏輯,

在查詢和合並的時候都能夠在小於線性的時間內完成。用什麼表示並查集呢,我們應用樹的表示形式,但是我們不建立樹的節點,而是應用一個parent數字來表示當前索引為index的元素的祖先是誰。

    上述的例子: i = 1, 2, ..., 9, parent[i] = i, 說明對於擁有單個元素的集合來說,它的根就是它本身,接下來遍歷一遍陣列,建立並查集:

    對於1, 2, 他們的parent[1] = 1, parent[2] = 2, 只要祖先不一樣,說明他們屬於不同的集合,那麼就要合併他們,那讓1,還是2來當做祖先呢,可以自由的選擇,稍後我會講述,這樣的隨便可能會造成最後並查集同於普通的集合,失去了快速超找的優勢。合併之後,就會出現有兩個元素的集合,他們以樹的形式表示的時候祖先設定為1,然後合併另外兩個集合{1, 2}和{3}, 首先判斷他們是否在同一個集合內,很簡單,就是判斷這兩個集合用樹的形式表示的時候是不是有著共同的祖先,第一個集合的祖先是1, 第二個集合的祖先是3,很顯然,不一樣,所以就要進行合併。以此類推,直到所有單個集合被合併為止。上面有可能造成並查集同於普通集合的關鍵就是在於在合併兩個集合的時候,根節點的選取,如果A, 始終是大的集合,B 始終是小的集合,例如:{1, 2}和{3}, {1, 2, 3}和{4}, {1, 2, 3, 4}和{5}, 那麼如果始終把小的集合的根當做合併後集合的根的話,那麼並查集最後用樹的形式表示出來的時候就是像連結串列的形式。所以需要一種平衡策略,就是用上述相反的方法來做:把集合元素大的集合的根作為兩個集合合併後的根。這樣樹的高度就會降低。同時還要應用路徑壓縮的方法,來降低樹的高度。


上圖是並查集合並的例子。

給出查詢和合並的基本程式碼:

int find(int x) {
	int r = x;
	while (parents[r] != r)
		r = parents[r];
	int i = x, j;
	while (i != r) {
		j = parents[i];
		parents[i] = r;
		i = j;
	}
	return i;
}

並查集合並的程式碼:

oid _union(int x, int y) {
	int fx = find(x);
	int fy = find(y);
	if (fx == fy)
		return;
	if (rank[x] > rank[y]) {
		parents[y] = x;
	} else {
		if (rank[x] == rank[y])
			rank[y]++;
		parents[x] = y;
	}
}

設定rank陣列是為了降低樹的高度。

上面的程式碼如果看不太懂,理解不太透徹,那就用一個例子來幫助大家理解吧,應用並查集來判斷一個圖中是否有環。


這個圖有環是已知的吧,但是要用程式碼來判斷,就很複雜,首先我們把每個點看成獨立的集合{0} ,{1}, {2}, 然後規定如果兩個點之間有邊相連,如果這兩個點不屬於同一個集合,那就將他們所屬的結合合併,看邊0-1,直接將這兩個點代表的集合合併{0, 1}, 其中讓1來當父節點, 看邊1-2, 它們分別屬於不同的集合,合併集合之後是{1, 2},讓2來當父節點,依照這種邏輯關係,0的祖先節點就是2, 然後在看邊0-2,他們屬於一個集合,因為他們有著共同的祖先2, 這就說明0-2之間在沒有0-2這條邊之前已經連通了,如果在加上這條邊的話那從0到2就有兩條路徑可達,就說明存在一個環了,下面給出程式碼:

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

typedef struct edge_s {
	int src;
	int dst;
}edge_t;

class Graph {
private:
	int arc_num;
	int vex_num;
	int *parents;
	int *rank;
	edge_t *arcs;
public:
	Graph(int _vex_num, int _arc_num) {
		arc_num = _arc_num;
		vex_num = _vex_num;
		arcs = new edge_t[arc_num];
		parents = new int[vex_num];
		for (int i = 0; i < vex_num; i++)
			parents[i] = i;
		rank = new int[vex_num];
		memset(rank, 0, vex_num * sizeof(int));
	}
	~Graph() {
		delete []arcs;
		delete []parents;
		delete []rank;
	}
	void setEdge(int index, int src, int dst);
	int getArcNum();
	int getVexNum();
	int getEdgeSrc(int index);
	int getEdgeDst(int index);
	int find(int x);
	void _union(int x, int y);
};

void Graph::setEdge(int index, int src, int dst) {
	if (index < arc_num) {
		arcs[index].src = src;
		arcs[index].dst = dst;
	}
}

int Graph::getArcNum() {
	return arc_num;
}

int Graph::getVexNum() {
	return vex_num;
}

int Graph::getEdgeSrc(int index) {
	return arcs[index].src;
}

int Graph::getEdgeDst(int index) {
	return arcs[index].dst;
}

int Graph::find(int x) {
	int r = x;
	while (parents[r] != r)
		r = parents[r];
	int i = x, j;
	while (i != r) {
		j = parents[i];
		parents[i] = r;
		i = j;
	}
	return i;
}

void Graph::_union(int x, int y) {
	int fx = find(x);
	int fy = find(y);
	if (fx == fy)
		return;
	if (rank[x] > rank[y]) {
		parents[y] = x;
	} else {
		if (rank[x] == rank[y])
			rank[y]++;
		parents[x] = y;
	}
}

bool isContainCycle(Graph &g) {
	int i;
	for (i = 0; i < g.getArcNum(); i++) {
		int fx = g.find(g.getEdgeSrc(i));
		int fy = g.find(g.getEdgeDst(i));
		if (fx == fy)
			return true;
		g._union(fx, fy);
	}
	return false;
}

int main(int argc, char *argv[]) {
	Graph g = Graph(3, 3);
	g.setEdge(0, 0, 1);
	g.setEdge(1, 1, 2);
	g.setEdge(2, 0, 2);
	if (isContainCycle(g))
		cout << "yes" << endl;
	else
		cout << "non" << endl;
	cin.get();
	return 0;
}