視頻地址 :https://www.bilibili.com/video/av38498175?p=1
參考鏈接:借這個問題科普一下並查集各種情況下的時間復雜度 - 省份數量 - 力扣(LeetCode) (leetcode-cn.com)
一,並查集(Disjoint Set)概述
1,並查集的作用
① 檢查圖中是否存在環
2 ,並查集的流程
① 設定一個集合,叫並查集
② 往集合里面添加邊,怎么添加呢?取邊的起點和終點,判斷兩點是否都在集合里面。如果都在,則出現了環,如果不在,則將兩個點放入集合中。
③ 繼續添加下一條邊,直到沒有邊。如果最后都沒有找到環,就是圖中不存在環。
二,並查集的構造
1, 在上述並查集的流程中,如果我們用集合表示並查集,自然也可以實現。但使用集合的話,在進行“集合合並”或者是“點是否屬於該集合的判斷”的話,時間復雜度應該是高於使用根數組(憑感覺,關於時間復雜度真的搞不懂)。
2, 並查集構造的三個動機:
能夠表示點加入集合的不同狀態;方便查找點是否存在於集合中;方便兩個不同的集合進行合並。
3, 根數組(我不知道專業名稱,這里暫時這樣稱呼)
為了滿足上述三點,於是便有人想出了並查集算法,想出了用根數組:p 實現並查集。
p[i]:表示第i個點的父節點。
p 的初始化:p[i] = i; 或 p[i] = -1;
4, 表示點加入集合的不同狀態
根數組用樹的結構去表示點的狀態。為什么用樹呢?因為並查集算法就是為了檢查環的存在,所以一旦有環的存在就會被判定為異常,即並查集無需表示環,而無環的連通圖就是樹。
有了數組p[i],就能根據父節點構造出森林出來,位於同一棵樹的點自然屬於同一集合。
5, 查找點是否存在於集合
並查集算法用根代表某一個集合。如果兩個點的根一樣,則表明兩個點處於同一棵樹上,即兩個點同處於它們的根所代表的集合中。
而查找根的方法我們可以輕易根據數組p實現,只需要一層一層的用父節點往上循環,直到根節點。
那么,如何判斷是否為根節點呢?因為根節點從未加入其他節點,所以根據初始化條件的不同,根節點的 p[i] = i; 或 p[i] = -1; 這就是初始化的目的。
6, 集合的合並
既然,我們根據樹和根節點來作為集合的判斷依據,那么,如果我們要合並集合a和集合b,其實就是合並樹a和樹b。所以我們只需要將樹a的根指向樹b的根,或者將樹b的根指向樹a就可以了。
7, 由於在合並集合的時候,我們對邊的順序是沒有要求的。這種連接方式,並不能正確表示原來圖的結構,只能表示點的連通關系。
8, 代碼

#define _CR_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 10 int p[N]; int find(int x) // 找到根節點 { int t = x; while (p[t] != -1) t = p[t]; return t; } int join(int x, int y) // 合並兩個集合 { x = find(x), y = find(y); if (x == y) return 0; p[x] = y; return 1; } int main(void) { memset(p, -1, sizeof(p)); // 初始化 int edges[7][2] = { // 邊集 {0,1},{1,2},{2,3},{4,5},{5,6},{1,4},{1,6} }; int f = 0; for (int i = 0; i < 7; i++) { int x = edges[i][0]; int y = edges[i][1]; if (join(x, y) == 0) { printf("存在環!\n"); f = 1; break; } } if (f == 0) printf("不存在環!\n"); system("pause"); return 0; }
三,按秩合並 與 路徑壓縮
1,目的:對上述算法中:“查找點的根”,這一步驟的時間復雜度的優化。
2,舉例說明:在集合合並的時候,在極端情況下會出現 0-1 1-2 2-3 3-4…… 這樣一直讓樹的深度增加的情況。
這種情況就會導致點在查找根的時候,時間復雜度的增加。
3,所以,為了降低算法的時間復雜度,有人提出了壓縮路徑和按秩合並的思想。
4,按秩合並
① 秩:這里指樹的深度。算法使用 rank 數組來記錄樹的深度,如 rank[x] = y 表示 以 x 點為根結點的樹的深度為 y。
② 算法未開始時,此時所有的樹只有一個點,沒有邊,所以每個點的深度為 0,所以rank數組初始化為全0
③ 算法開始合並時,比較要合並的兩棵樹的深度。
當兩棵樹的深度不一致時,讓低的樹的根指向高的樹的根,這樣新合並的樹的高度就等於之前高的樹的深度,而不會再度增加。
當兩棵樹的深度一致時,隨便讓一棵樹的根指向另一棵樹的根,這樣新合並的樹的高度就等於之前樹的深度加上1,而不會增加很多。
④ 代碼

#define _CR_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 10 int p[N], rank[N]; int find(int x) // 找到根節點 { int r = x; while (p[r] != -1) r = p[r]; return r; } int join(int x, int y) // 合並兩個集合 { x = find(x), y = find(y); if (x == y) return 0; if (rank[x] > rank[y]) // 讓低的指向高的 p[y] = x; else if (rank[x] < rank[y]) p[x] = y; else { p[x] = y; rank[y]++; } return 1; } int main(void) { memset(p, -1, sizeof(p)); // 初始化 memset(rank, 0, sizeof(rank)); int edges[7][2] = { // 邊集 { 0,1 },{ 1,2 },{ 2,3 },{ 4,5 },{ 5,6 },{ 1,4 },{ 1,6 } }; int f = 0; for (int i = 0; i < 7; i++) { int x = edges[i][0]; int y = edges[i][1]; if (join(x, y) == 0) { printf("存在環!\n"); f = 1; break; } } if (f == 0) printf("不存在環!\n"); system("pause"); return 0; }
5,壓縮路徑
① 直接在每一次查找某一點的根節點后,將該點到根節點的路徑上的所有點指向根節點。
② 不過這種壓縮路徑存在一定的延遲,即兩個集合剛合並后,你並沒有完成壓縮路徑,而是在查找時,才會去壓縮路徑。
正如圖中,剛開始並沒有用到除了根節點的點,所以一直沒有壓縮路徑。
一直到“取1-4”,此時點1和點4都不是根節點,所以在合並之前的查找,它會將點1到根節點路徑上的點指向它的根節點3,將點4到根節點路徑上的點指向它的根節點6。、
最后,壓縮完路徑的兩個樹在進行合並。
③ 代碼

#define _CR_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 10 int p[N]; int find(int x) // 找到根節點 { int r = x; while (p[r] != -1) r = p[r]; while (x != r) { int t = p[x]; p[x] = r; x = t; } return x; } int join(int x, int y) // 合並兩個集合 { x = find(x), y = find(y); if (x == y) return 0; p[x] = y; return 1; } int main(void) { memset(p, -1, sizeof(p)); // 初始化 int edges[7][2] = { // 邊集 {0,1},{1,2},{2,3},{4,5},{5,6},{1,4},{1,6} }; int f = 0; for (int i = 0; i < 7; i++) { int x = edges[i][0]; int y = edges[i][1]; if (join(x, y) == 0) { printf("存在環!\n"); f = 1; break; } } if (f == 0) printf("不存在環!\n"); system("pause"); return 0; }

#define _CR_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 10 int p[N]; int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } int join(int x, int y) // 合並兩個集合 { x = find(x), y = find(y); if (x == y) return 0; p[x] = y; return 1; } int main(void) { for (int i = 0; i < N; i++) // 初始化 p[i] = i; int edges[7][2] = { // 邊集 { 0,1 },{ 1,2 },{ 2,3 },{ 4,5 },{ 5,6 },{ 1,4 },{ 1,6 } }; int f = 0; for (int i = 0; i < 7; i++) { int x = edges[i][0]; int y = edges[i][1]; if (join(x, y) == 0) { printf("存在環!\n"); f = 1; break; } } if (f == 0) printf("不存在環!\n"); system("pause"); return 0; }
其中,第二個代碼是用遞歸實現 find函數,搜索時找根節點,回溯時壓縮路徑。
而且初始化為-1時,不能用遞歸實現,因為要指向根節點,如果初始化為自身則可以將返回值作為根節點,-1則不行。
四,按秩合並 + 路徑壓縮
① 只是簡單的將兩個函數放在一起,不同做什么特殊處理
② 明明壓縮路徑的時候,改變了樹高,那么,為什么rank數組不需要維護?
答:不太清楚。應該是相對高度不變吧。
③ 代碼

#define _CR_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<string.h> #define N 10 int p[N], rank[N]; int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } int join(int x, int y) // 合並兩個集合 { x = find(x), y = find(y); if (x == y) return 0; if (rank[x] > rank[y]) // 讓低的指向高的 p[y] = x; else if (rank[x] < rank[y]) p[x] = y; else { p[x] = y; rank[y]++; } return 1; } int main(void) { for (int i = 0; i < N; i++) // 初始化 p[i] = i; memset(rank, 0, sizeof(rank)); int edges[7][2] = { // 邊集 { 0,1 },{ 1,2 },{ 2,3 },{ 4,5 },{ 5,6 },{ 1,4 },{ 1,6 } }; int f = 0; for (int i = 0; i < 7; i++) { int x = edges[i][0]; int y = edges[i][1]; if (join(x, y) == 0) { printf("存在環!\n"); f = 1; break; } } if (f == 0) printf("不存在環!\n"); system("pause"); return 0; }
=========== ========= ========= ======= ====== ====== ===== === == =
菩薩蠻 其三 唐 韋庄
如今卻憶江南樂,當時年少春衫薄。騎馬倚斜樓,滿樓紅袖招。
翠屏金屈曲,醉入花叢宿。此度見花枝,白頭誓不歸。