樹的神奇用法 之 “並查集“,一個簡單例子快速掌握!

通過樹的延伸,我們會發現有許多優秀的算法,今天我們來學習一下什麼是 “並查集” . . . 文章參考啊哈算法,度娘 . . .

相關樹一些文章:

1)《算法筆記》—— 堆排序算法( C++實現)
2) 二叉搜索樹 —— 查找與排序
3) 紅黑樹基本功能 —— C++實現
4) 二叉樹(鏈式存儲)—— C++實現
5) 二叉樹(順序存儲)—— C++實現
6) 數據結構堆(包含堆排序)有這麼簡單嗎? 其實是真的簡單!

並查集詳解在這裏插入圖片描述

那麼什麼是並查集呢? 度娘解釋如下:

並查集,在一些有N個元素的集合應用問題中,我們通常是在開始時讓每個元素構成一個單元素的集合,然後按一定順序將屬於同一組的元素所在的集合合併,其間要反覆查找一個元素在哪個集合中。這一類問題近幾年來反覆出現在信息學的國際國內賽題中,其特點是看似並不複雜,但數據量極大,若用正常的數據結構來描述的話,往往在空間上過大,計算機無法承受;即使在空間上勉強通過,運行的時間複雜度也極高,根本就不可能在比賽規定的運行時間(1~3秒)內計算出試題需要的結果,只能用並查集來描述。
.
並查集是一種樹型的數據結構,用於處理一些不相交集合(Disjoint Sets)的合併及查詢問題。常常在使用中以森林來表示。

我們會通過一個簡單的實例來詳細的講解一下並查集的原理,並將以實現,而這個例子名爲 —— 解密犯罪團伙 . . .

例題描述:

在這裏插入圖片描述
快過年了,犯罪分子們也開始爲年終獎 “奮鬥” 了,小哼的家鄉出現了多次搶劫事件。由於強盜人數過於龐大,所以警方不知道他們有幾個犯罪團伙,但是他們獲得了一些信息。我們利用這些信息來處理分析一下 .
線索如下所示,現在有 11 個強盜:

  1. —— 1 號 與 2號是同夥
  2. —— 3 號 與 4號是同夥
  3. —— 5 號 與 2號是同夥
  4. —— 4 號 與 6號是同夥
  5. —— 2 號 與 6號是同夥
  6. —— 7 號 與 11號是同夥
  7. —— 8 號 與 7號是同夥
  8. —— 9 號 與 7號是同夥
  9. —— 9 號 與 11號是同夥
  10. —— 1 號 與 6號是同夥

我們規定:強盜同夥的同夥也是同夥. . .

要想高效的解決這個問題,我們可以使用並查集,下面我們就來研究一下並查集在這個例子中如何使用 . . .
.

解決方法詳解過程

—— 首先,我們假設這 11 個強盜是相互不認識的,他們每個人都是一個獨立的犯罪集團,我們將警方提供的線索,一步步地來將他們來 “合併”。

1)我們需要一個一維數組,其中的每個元素都對應着下標的 BOSS(當前已知的最大BOOS 是誰,例如剛開始的數組狀態爲:
在這裏插入圖片描述

2)現在我們將根據那些線索將這個數組中的數據進行合併,假如我們將兩個犯罪團伙合併之後,那麼他們誰是 BOSS 呢?當然,我們這裏假定左邊的強盜更厲害一點,並且將這個法則稱之爲 "靠左" 法則。比如現在的第一條線索 ——> 1 號 與 2號是同夥,根據 “靠左” 法則,那麼 2號的BOSS 將是 1號,如下圖所示:
在這裏插入圖片描述

3)處理第二條線索 ——> 3 號 與 4號是同夥,處理過程與 2)相同,根據 “靠左” 法則,那麼 4號的BOSS 將是 3號,如下圖所示:
在這裏插入圖片描述

4)處理第三條線索 ——> 5 號 與 2號是同夥,我們看見這一條線索時會發現 2號已經有了 BOSS了,那麼我們還能將 2號跟隨 5號嗎?古人云:擒賊先擒王,我們只需要找 2號的 BOSS 1號和 5號談就行了 . . . 前面所說 強盜同夥的同夥也是同夥,所以 5號是與 1號是有關係的,他們屬於一個犯罪團伙,我們只需要找到 2號的BOSS,並且將這個 BOSS 成爲 5號的 小弟就行了,即 1號 帶領手下 2號 歸順了 5號, 如下圖所示:
在這裏插入圖片描述

這裏的 f[1] 是 5,寫的不夠仔細,大家涼解 ^ _ ^

5)處理第四條線索 ——> 4 號 與 6號是同夥,因爲 f[4] = 3,所以我們是將 6號 加入 3號的犯罪團伙,如下圖所示:
在這裏插入圖片描述

這裏的 f[1] 是 5,寫的不夠仔細,大家涼解 ^ _ ^

6)處理第五條線索 ——> 2 號 與 6號是同夥,我們根據 “靠左” 原則擒賊先擒王 原則,直接讓 6號的 BOSS 3號 與 2 號的BOSS 5號談就行了,所以我們的結果是 3號成爲了 5號的小弟,這裏特別注意的是,當 2號查找自己的最大BOSS時,在此過程中會進行 “路徑壓縮”,那麼什麼是 “路徑壓縮” 呢?當誰能找到自己的大BOSS 誰就能成爲 大BOSS的真系屬下 . . .
如下圖所示(代碼將會在最後進行展示):
在這裏插入圖片描述

7)處理第六條線索 ——> 7 號 與 11號是同夥,直接根據 “靠左” 原則,如下圖所示:
在這裏插入圖片描述

8)處理第七條線索 ——> 8 號 與 7號是同夥,直接根據 “靠左” 原則,如下圖所示:
在這裏插入圖片描述

9)處理第八條線索 ——> 9 號 與 7號是同夥,f[9] = 9,f[7] = 8,我們根據 “靠左” 原則擒賊先擒王 原則,將 f[8] 的值改爲 9即可,如下圖所示:
在這裏插入圖片描述

10)處理第九條線索 ——> 9 號 與 11號是同夥,我們發現一個有趣的現象,如下所示:
在這裏插入圖片描述
9號 與 11號已經在一個團伙中,所以他們本質上並沒有什麼區別,但是當 11號在查找他的 大BOSS時,會對這個路徑進行 “路徑壓縮”,如下圖所示:
在這裏插入圖片描述

11)處理最後一條線索 ——> 1 號 與 6號是同夥與 上一條 9)類似,又是一次 “路徑壓縮” 的過程。所以最終圖解爲:
在這裏插入圖片描述

.
既然我們已經分析了所有的線索,那麼現在一共有多少個犯罪團伙呢?其實我們只需要數一數有幾個 最大的BOSS就行了,如下所示:
在這裏插入圖片描述

並查集通過一個一維數組來實現,其本質是維護一個森林。剛開始的時候,森林的每個點都是孤立的(也可以理解爲每個點就是一棵只有一個結點的樹),之後通過一些條件,逐漸將這些樹合併成一棵大樹。合併的過程就是 "認爹" 的過程。在此文章中,我們遵守的是 "靠左" 原則"擒賊先擒王" 原則

合併的過程中,找到其根源然後進行一些算法操作,在此過程中,我們會進行路徑壓縮 . . .


模擬解題源碼如下:

#include <stdio.h>
// n —— 多少個元素		m —— 多少個線索
int f[1001] = { 0 }, n, m, sum = 0;

// 初始化數組中的元素,開始時每個罪犯都是自己獨立的
void init() {
    int i;
	for (i = 1; i <= n; ++i) {
		f[i] = i;
	}
}

// 找到自己的最大BOSS,並且在過程中進行 ——> 路徑壓縮
// "擒賊先擒王" 原則
int getf(int v) {
	if (f[v] == v) return v;		// 找到了
	else {
		f[v] = getf(f[v]);			// 將最大的BOSS直接領導他的小兵,建議自己畫個圖測試一下
		return f[v];
	}
}

// 合併兩個子集合
void merge(int v, int u) {
	int t1, t2;		// 爲 v,u 的最大BOSS
	
	t1 = getf(v);
	t2 = getf(u);
	
	// 如果不在同一個集合之中,則合併
	if (t1 != t2) {
		f[t2] = t1;		// "靠左" 原則
	}
}

int main() {
	int i, x, y;

	scanf("%d%d", &n, &m);			// 輸入元素個數、線索個數

	init();

	for (i = 1; i <= m; ++i) {
		scanf("%d%d", &x, &y);		// 輸入每一條線索 
		merge(x, y);				// 開始合併犯罪
	}
	
	// 掃描出有多少個獨立的犯罪團伙
	for (i = 1; i <= n; ++i) {
		if (f[i] == i) ++sum;
	}

	printf("%d\n", sum);
}

程序執行結果爲:
在這裏插入圖片描述

.
.
.


浪子花夢

一個有趣的程序員 ~

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