通過樹的延伸,我們會發現有許多優秀的算法,今天我們來學習一下什麼是 “並查集” . . . 文章參考啊哈算法,度娘 . . .
相關樹一些文章:
1)《算法筆記》—— 堆排序算法( C++實現)
2) 二叉搜索樹 —— 查找與排序
3) 紅黑樹基本功能 —— C++實現
4) 二叉樹(鏈式存儲)—— C++實現
5) 二叉樹(順序存儲)—— C++實現
6) 數據結構堆(包含堆排序)有這麼簡單嗎? 其實是真的簡單!
並查集詳解
那麼什麼是並查集呢? 度娘解釋如下:
並查集,在一些有N個元素的集合應用問題中,
我們通常是在開始時讓每個元素構成一個單元素的集合,然後按一定順序將屬於同一組的元素所在的集合合併,其間要反覆查找一個元素在哪個集合中
。這一類問題近幾年來反覆出現在信息學的國際國內賽題中,其特點是看似並不複雜,但數據量極大,若用正常的數據結構來描述的話,往往在空間上過大,計算機無法承受;即使在空間上勉強通過,運行的時間複雜度也極高,根本就不可能在比賽規定的運行時間(1~3秒)內計算出試題需要的結果,只能用並查集來描述。
.
並查集是一種樹型的數據結構,用於處理一些不相交集合(Disjoint Sets)的合併及查詢問題。常常在使用中以森林來表示。
我們會通過一個簡單的實例來詳細的講解一下並查集的原理,並將以實現,而這個例子名爲 —— 解密犯罪團伙 . . .
例題描述:
快過年了,犯罪分子們也開始爲年終獎 “奮鬥” 了,小哼的家鄉出現了多次搶劫事件。由於強盜人數過於龐大,所以警方不知道他們有幾個犯罪團伙,但是他們獲得了一些信息。我們利用這些信息來處理分析一下 .
線索如下所示,現在有 11 個強盜:
- —— 1 號 與 2號是同夥
- —— 3 號 與 4號是同夥
- —— 5 號 與 2號是同夥
- —— 4 號 與 6號是同夥
- —— 2 號 與 6號是同夥
- —— 7 號 與 11號是同夥
- —— 8 號 與 7號是同夥
- —— 9 號 與 7號是同夥
- —— 9 號 與 11號是同夥
- —— 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);
}
程序執行結果爲:
.
.
.