一、普通並查集
可以理解為使用數組實現的樹形結構,只保存了每個節點的父節點(前驅)。
功能為:合並兩個節點(及其所在集合) 、 查找節點所屬集合的代表節點(可以理解為根節點)。
原理及用法
以6個元素為例(編號0到5):把0單獨划分為一個集合;把1,2,3,4划分為一個集合;把5單獨划分為一個集合。
1. 初始化 init()
n個元素的並查集,只需要一個容量為n的數組f[n],值全部初始化為自己即可:for(int i=0;i<n;i++) f[i]=i;
2. 查找節點所屬集合 Find(x)
主要代碼:Find(x): if(x == f[x]) return x;
return Find(f[x]);
但若只是簡單的這樣做,會出現上圖第三個圓中的情況,即查找某個節點時遞歸太多次。因此需要“路徑壓縮”,只需增加一步:
Find(x): if(x == f[x]) return x;
return f[x] = Find(f[x]);
3. 合並兩個節點(及其所在集合) Union(x, y)
Union(x,y): int fx=Find(x), fy=Find(y);
if(fx != fy) f [fx] = fy; // 此處換為f [fy] = fx也行,道理相同,意義和效果其實也一樣。
注意:一定是f [fx] = fy,而不是f [x] = y。只有把x和y的最終父節點(前驅)連接起來,所屬的兩個集合才算真正完全連通,整個邏輯也才能正確。
二、擴展域並查集
使用情景:
n個點有m對關系,把n個節點放入兩個集合里,要求每對存在關系的兩個節點不能放在同一個集合。問能否成功完成?
思路:
把每個節點擴展為兩個節點(一正一反),若a與b不能在一起(在同一個集合),則把a的正節點與b的反節點放一起,把b的正節點與a的反節點放一起,這樣就解決了a與b的沖突。若發現a的正節點與b的正節點已經在一起,那么說明之前的某對關系與(a,b)這對關系沖突,不可能同時滿足,即不能成功完成整個操作。
具體實現:
1. 初始化 init()
n個點,每個點擴展為兩個點(一正一反),則需要一個容量為2*n的數組f[n],值全部初始化為自己即可:for(int i=0;i<2*n;i++) f[i]=i;
(注意初始編號,若編號為[1,n],則初始化應該為:for(int i=1;i<=2*n;i++) f[i]=i;)
一個點x的正點編號為x,反點編號為x+n(這樣每個點的反點都是+n,規范、可讀性強、不重復、易於理解)。
2. Find(x)和Union(x, y)不需要修改,含義和實現不變。
3. 解決問題的算法步驟
1)初始化2*n個節點的初始父節點,即它本身。
2)遍歷m對關系,對每對(a,b),先找到a和b的父節點,若相等則說明(a,b)的關系與之前的關系有沖突,不能同時解決,則得到結果:不能完成整個操作。
否則執行:Union(a, b+n), Union(b, a+n). (這時已經Find過了,直接執行f [fx] = fy這一句就等效與Union(x, y) )
3)若m對關系都成功解決,則得到結果:能夠完成整個操作。
拓展:
由於擴展域會直接使數組容量翻倍,所有一般只解決這種“二分”問題,只擴展為2倍即可。
優點在於:結構簡單,並查集的操作也不需要做改變,非常易於理解。 缺點顯然就是:需要額外存儲空間。
三、加權並查集
使用情景:
N個節點有M對關系(M條邊),每對關系(每條邊)都有一個權值w,可以表示距離或划分成多個集合時的集合編號,問題依然是判斷是否有沖突或者有多少條邊是假的(沖突)等。
思路:
給N個節點虛擬一個公共的根節點,增加一個數組s[n]記錄每個節點到虛擬根節點的距離,把x,y直接的權值w看為(x,y)的相對距離。
Union(x,y,w)時額外把x,y到虛擬根節點的距離(s值)的相對差值設置為w;Find(x)時,壓縮路徑的同時把當前s值加上之前父節點的s值,得到真實距離。
具體實現:
1. 初始化 init()
f[n]數組記錄節點的父節點,s[n]數組記錄節點到虛擬根節點的距離: for(int i=0;i<n;i++) { f[i]=i; s[i]=0; }
2. Find(x)
if(x==f[x])return x;
int t = f[x];
f[x] = Find(f[x]);
s[x] += s[t];
// s[[x] %= mod; 若s[x]表示划分成mod個集合時的集合編號等情況時,則需要求余。
return f[x];
3. Union(x, y,w)
int fx = Find(x), fy = Find(y); //此時已經s[x]和s[y]都已經計算為真值。
if(fx != fy) {
f [fx] = fy;
s [fx] = (s[x] - s[y] + w + mod) % mod;
}
4. 解決問題的算法步驟
初始化后,遍歷m對關系:若x,y的父節點不同,則Union(x,y,w);否則,若x與y的差值為w,則說明正確,繼續遍歷,不為w時說明出現沖突。
當s[x]只是代表划分為mod個集合時的集合編號時,應該比較s[x]與s[y]的值是否相同,相同時說明出現沖突;不相同時說明之前已經解決了,正確可繼續遍歷。
拓展:加權並查集主要得賦予並理解s[x]值的意義,較難掌握且應用廣泛
牛客網例題:關押罪犯 https://ac.nowcoder.com/acm/problem/16591 ,里面的題解和討論區有更多講解和入門題目鏈接
直接百度搜素“加權並查集”也可找到更多講解和入門題目鏈接。
牛客網關押罪犯的題解代碼:

#include<cstdio> #include<algorithm> using namespace std; const int maxn = 20002; const int maxm = 100002; struct edge { int a, b, c; }e[maxm]; bool cmp(edge a, edge b) { return a.c > b.c; } int f[2 * maxn]; int Find(int x) { if (x == f[x])return x; return f[x] = Find(f[x]); } int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 0; i < m; i++) scanf("%d%d%d", &e[i].a, &e[i].b, &e[i].c); sort(e, e + m, cmp); //按仇恨值從大到小排序 for (int i = 1; i <= 2 * n; i++)f[i] = i; //初始化並查集 int i; //從大到小依次把每對罪犯安排到不同監獄 for (i = 0; i < m; i++) { int a = Find(e[i].a), b = Find(e[i].b); if (a == b)break; //兩人的正點已在同一個集合,無法解決,最大沖突出現 f[a] = Find(e[i].b + n); //把a和b的反點(敵人)合並 f[b] = Find(e[i].a + n); //把b和a的反點(敵人)合並(每個點都有一個正點和反點) } if (i == m)printf("0"); else printf("%d", e[i].c); return 0; }

#include <iostream> #include <vector> #include <algorithm> using namespace std; typedef long long ll; const int maxn = 200000 + 10; const int gxs = 2; //模2就只有0,1兩個值,分別代表兩個不同的集合 const int mod = 2; int n, m; int f[maxn], s[maxn]; //f記錄父節點(前驅),s記錄到虛擬root點的距離 void init() { for (int i = 0; i < maxn; i++) f[i] = i, s[i] = 0; } //查找 int finds(int x) { if (x == f[x]) return x; int t = f[x]; f[x] = finds(f[x]); s[x] += s[t]; //s[x]原來是與t的相對距離,現在是與root的相對距離 s[x] %= gxs; //s值求余后代表所屬監獄(二選一) return f[x]; } //新建關系 void unions(int x, int y, int w) { int fx = finds(x), fy = finds(y); if (fx != fy) { f[fy] = fx; s[fy] = s[x] - s[y] + w + gxs; //相對距離設置為w,解決這一對沖突 s[fy] %= mod; //求余直接賦予實際意義:所屬的mod個集合的編號 } } struct node { int a, b; ll val; bool operator < (const node &a)const { return val > a.val; } }; vector<node> que; int main() { cin >> n >> m; int a, b; ll v; for (int i = 0; i < m; i++) { cin >> a >> b >> v; que.push_back(node{ a,b,v }); } sort(que.begin(), que.end()); //從大到小排序 init(); for (int i = 0; i < m; i++) { a = que[i].a; b = que[i].b; v = que[i].val; if (finds(a) == finds(b)) { //在同一個集合就不能直接解決沖突 if (s[a] == s[b]) { //若s值相同就說明已經在同一個集合,沖突無法解決 cout << v << endl; //因為從大到小遍歷,第一個解決不了的關系的val就是答案:最小化的最大沖突值 return 0; } //否則說明解決之前的沖突后,當前沖突也被解決。 } else { //不在一個集合就可以通過設置s值解決沖突 unions(a, b, 1); } } cout << 0 << endl; return 0; }