並查集
基本概念
並查集,在一些有N個元素的集合應用問題中,我們通常是在開始時讓每個元素構成一個單元素的集合,然后按一定順序將屬於同一組的元素所在的集合合並,其間要反復查找一個元素在哪個集合中。
並查集是一種樹型的數據結構,用於處理一些不相交集合(Disjoint Sets)的合並及查詢問題。常常在使用中以森林來表示。
實現原理
通過更新維護父親節點使得,合並后的集合最終擁有同一個點根節點,擁有相同根節點即為同類。
- Search 查找自己的根節點;(紅圈標記為根節點)
- Merge 合並兩個節點在一個集合;(假設尋找合並節點5和2)
- 壓縮路徑;壓縮路徑可以使得在多次查詢時,查詢時間得到優化,具體過程是優化其結構,使得查詢點的父親節點為根節點。(上圖壓縮路徑后得到)
代碼實現

1 void init(){ // 初始化自己祖先就是自己 2 for(int i = 1 ; i<= n; i++){ 3 pre[i] = i; 4 } 5 } 6 7 int Search(int x){ // 遞歸尋找自己的祖先 8 return x == pre[x] ? x : pre[x] = Search(pre[x]); 9 } 10 11 void Merge(int x, int y){ // 合並兩個節點 12 int fx = Search(x); 13 int fy = Search(y); 14 if(fx != fy) pre[fx] = fy; // 把x合並到y即把x祖先設置為y的祖先 15 }
帶權並查集
基本概念
帶權並查集即是結點存有權值信息的並查集;當兩個元素之間的關系可以量化,並且關系可以合並時,可以使用帶權並查集來維護元素之間的關系;帶權並查集每個元素的權通常描述其與並查集中祖先的關系,這種關系如何合並,路徑壓縮時就如何壓縮;帶權並查集可以推算集合內點的關系,而一般並查集只能判斷屬於某個集合。
經典例題
食物鏈(FJUTOJ2022 & POJ1182)
傳送門:FJUTOJ2022 && POJ1182
題意:
動物王國中有三類動物A,B,C,A吃B, B吃C,C吃A。
現有N個動物,以1-N編號。每個動物都是A,B,C中的一種,但是我們並不知道它到底是哪一種。
用兩種說法對這N個動物所構成的食物鏈關系進行描述:
- "1 X Y",表示X和Y是同類。
- "2 X Y",表示X吃Y。
給出K句話,有些是真的,有些是假的,滿足下列任一條件即為假話,否則是真話:
1) 當前的話與前面的某些真的話沖突,就是假話;
2) 當前的話中X或Y比N大,就是假話;
3) 當前的話表示X吃X,就是假話。
輸出假話的數量;
解題思路:
這個題目需要維護推算集合內部的關系,所以可以利用帶權並查集解決。
創建利用pre數組和rela數組判斷集合關系,pre判斷集合之間的關系,rela判斷集合內部元素的關系,這題我們可以建立三種關系同類,捕食,和被捕食三種關系,我們在rela數組中分別用0,1,2表示:
- 0表示和根節點是同類關系
- 1表示和跟節點是捕食關系(吃根節點)
- 2表示和根節點是被捕食關系(被根節點吃)
確定表示了三種關系表示,剩下是需要維護的關系,我們需要維護些什么關系呢?
首先是合並考慮壓縮路徑時的關系維護,我們壓縮路徑時已知B和A的關系,以及A和A根節點的關系,需要推導出B和A根節點的關系,如圖是我們橙色線是我們要推導出的關系,黑色線是以知關系。
我們列舉所有情況在表格中來看,是否存在某種關系。
結點B與A關系 | B與根關系 | |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
0 | 2 | 2 |
1 | 0 | 1 |
1 | 1 | 2 |
1 | 2 | 0 |
2 | 0 | 2 |
2 | 1 | 0 |
2 | 2 | 1 |
從表格中我們顯然可以得到關系` rela[b] = (rela[a] + rela[b]) % 3`壓縮路徑關系的代碼如下。

1 int Find(int x){ // 查找當前結點的根節點 2 if(x == pre[x]) return x; 3 else{ // 壓縮路徑 4 int temp = pre[x]; 5 pre[x] = Find(pre[x]); // 遞歸尋找頭根點,壓縮路徑節點 6 rela[x] = (rela[x] + rela[temp]) % 3; // 壓縮路徑關系 7 } 8 return pre[x]; 9 }
然后我們考慮關系的查找,我們以及知道A和B在同一集合,即代表他們根節點相同,我們要確定兩者之間的關系,我們還是線畫出關系圖,橙色線是我們要推導出的關系,黑色線是以知關系。
我們同樣在表格中寫出對應關系
從表格中可以得到關系`relation[a->b] = (rela[a] - rela[b]) % 3`,減法可能會產生負數,所以要先+3再進行取模,查找關系的代碼如下

1 if(Find(x) == Find(y)){ // 如果兩個根節點相同 2 relation = (rela[x] - rela[y] + 3) % 3; // 推出兩個根節點之間的關系 3 return relation == r; // 判斷給出關系是否與已經存在的關系矛盾 4 }
結點B與根關系 | A與B關系 | |
---|---|---|
0 | 0 | 0 |
0 | 1 | 2 |
0 | 2 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
1 | 2 | 2 |
2 | 0 | 2 |
2 | 1 | 1 |
2 | 2 | 0 |
最后我們考慮合並兩個節點時關系的維護,我們已經知a和其根節點的關系,以及b和其根節點的關系,當我們把b集合合並到a集合時,我們需要考慮b根節點和a根節點存在的關系,關系圖如下,橙色線是我們要推導出的關系,黑色線是以知關系。
關系表如下
結點B與根關系 | 結點B與A的關系 | B根節點和A根節點的關系 | |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 0 | 1 | 1 |
0 | 0 | 2 | 2 |
0 | 1 | 0 | 2 |
0 | 1 | 1 | 0 |
0 | 1 | 2 | 1 |
0 | 2 | 0 | 1 |
0 | 2 | 1 | 2 |
0 | 2 | 2 | 0 |
上面這個表並沒有列出所有情況,但是我們已經可以從表格中可以得到關系`relation[pre[b]->prea[a]] = (rela[a] - rela[b] + relation[b -> a]) % 3`合並關系的代碼如下

1 void Merge(int x, int y, int r){ // 合並兩個節點關系 2 int fx = Find(x); // 查找 x,y的根節點 3 int fy = Find(y); 4 5 if(fx != fy){ //如根節點不同進行合並 6 pre[fx] = fy; //把x節點集合合並到y 7 rela[fx] = (rela[y] - rela[x] + r + 3) % 3; //計算x頭節點與y頭節點的關系 8 } 9 }
AC代碼

1 #include <cstdio> 2 #include <cstring> 3 #include <cmath> 4 #include <cstdlib> 5 #include <ctime> 6 #include <cctype> 7 #include <cstring> 8 #include <cmath> 9 #include <iostream> 10 #include <sstream> 11 #include <string> 12 #include <list> 13 #include <vector> 14 #include <set> 15 #include <map> 16 #include <queue> 17 #include <stack> 18 #include <algorithm> 19 #include <functional> 20 #define pr pair<int,LL> 21 #define lowbit(x) (x&(-x)) 22 #define rep(i,a,n) for (int i=a;i<=n;i++) 23 #define per(i,a,n) for (int i=a;i>=n;i--) 24 #define mem(ar,num) memset(ar,num,sizeof(ar)) 25 #define debug(x) cout << #x << ": " << x << endl 26 using namespace std; 27 typedef long long LL; 28 typedef unsigned long long ULL; 29 const int prime = 999983; 30 const int INF = 0x7FFFFFFF; 31 const LL INFF =0x7FFFFFFFFFFFFFFF; 32 const double pi = acos(-1.0); 33 const double inf = 1e18; 34 const double eps = 1e-6; 35 const LL mod = 1e9 + 7; 36 const int maxn = 5e5 + 7; 37 const int maxm = 4e6 + 7; 38 39 40 inline int read () { //讀入優化 41 int X = 0, w = 1; char ch = 0; 42 while(ch < '-') { if(ch == '-') w = -1; ch = getchar(); } 43 while(ch >= '0' && ch <= '9') X = (X << 3) + (X << 1) + ch - '0', ch = getchar(); 44 return X * w; 45 } 46 47 int pre[maxn],rela[maxn]; 48 int n, k, ans; 49 50 void init() // 初始化 51 { 52 for(int i = 1; i <= n; i++){ 53 pre[i] = i; // 頭節點等於自己本身 54 rela[i] = 0; // 自己和自己肯定是同類 55 } 56 ans = 0; //記錄假話數量 57 } 58 59 int Find(int x){ // 查找當前結點的根節點 60 if(x == pre[x]) return x; 61 else{ // 壓縮路徑 62 int temp = pre[x]; 63 pre[x] = Find(pre[x]); // 遞歸尋找根節點,壓縮路徑節點 64 rela[x] = (rela[x] + rela[temp]) % 3; // 壓縮路徑關系 65 } 66 return pre[x]; 67 } 68 69 void Merge(int x, int y, int r){ // 合並兩個節點關系 70 int fx = Find(x); // 查找 x,y的根節點 71 int fy = Find(y); 72 73 if(fx != fy){ //如根節點不同進行合並 74 pre[fx] = fy; //把x節點集合合並到y 75 rela[fx] = (rela[y] - rela[x] + r + 3) % 3; //計算x頭節點與y頭節點的關系 76 } 77 78 } 79 80 bool solve(int x,int y,int r){ // 判斷真話假話 81 int relation; 82 if(x > n||y > n||(r == 1&&x == y)){ // 根據題意直接判斷的假話 83 return false; 84 } 85 if(Find(x) == Find(y)){ // 如果兩個根節點相同 86 relation = (rela[x] - rela[y] + 3) % 3; // 推出兩個根節點之間的關系 87 return relation == r; // 判斷給出關系是否與已經存在的關系矛盾 88 } 89 else 90 return true; //否則為真 91 } 92 /// 0 表示與根節點是同類 93 /// 1 表示與根節點是捕食關系 94 /// 2 表示與根節點是被捕食關系 95 int main() 96 { 97 n = read(); 98 k = read(); 99 init(); 100 int c, x, y; 101 while(k--){ 102 c = read(); 103 x = read(); 104 y = read(); 105 c --; 106 if(solve(x,y,c)){ 107 Merge(x,y,c); //真話合並兩個節點關系 108 }else{ 109 ans++; //假話答案自增 110 } 111 } 112 printf("%d\n",ans); 113 return 0; 114 }
我在剛學習帶權並查集時看的是這位大佬的博客,大家也可以進行參考:帶權並查集
第一次寫博客,以上是我的一些個人理解,如有錯誤麻煩各位大佬指正。