並查集總結(路徑壓縮+啟發式合併)
阿新 • • 發佈:2019-01-26
並查集
一、並查集是處理什麼問題的:
並查集,是一種用來管理元素分組情況的資料結構,可以處理一些不相交集合的合併與查詢問題;它可以進行合併操作,但不能進行分割操作。
二、兩大操作:
(1)查詢元素a和元素b是否屬於同一集合;
(2)合併元素a和元素b所在的集合;
三、主要的步驟:
初始化:把每個點所在集合初始化為其自身。(通常來說,這個步驟在每次使用該資料結構時只需要執行一次,無論何種實現方式,時間複雜度均為O(N)。)查詢:查詢元素所在的集合,即根節點。
合併:將兩個元素所在的集合合併為一個集合。
基本思想:
在一些有N個元素的集合應用問題中,我們通常是在開始時讓每個元素構成一個單元素的集合,然後按一定順序將屬於同一組的元素所在的集合合併,其間要反覆查詢一個元素在哪個集合中,最終就形成了多個(可能一個)不同的集合,其中每一個集合都具有不同於其它集合的屬性,例如:每一個集合可以表示一個連通分支,則屬於同一個集合的元素就表示這個連通分支的頂點,這個集合中的每一個頂點都是相互可達的,這樣就可以就可以解決任意輸入的兩個元素是否可達的問題了。
四、處理並查集問題需要解決了的幾個難點:
(1)如何合併兩個不相交集合;
(2)如何判斷兩個元素是否屬於同一個集合;
(3)路徑壓縮,優化時間。
優化方法:1、路徑壓縮:對每一個節點,一旦向上走到了一次根節點,就把這個節點到父親的邊改為直接連向根的邊;
2、啟發式合併:讓讓深度較小的樹成為深度較大的樹的子樹。
五、並查集的結構:
並查集是使用樹形結構實現的,不過,不是二叉樹。每個元素對應一個節點,每個集合對應一棵樹。
六、並查集實現中的注意點:(在使用路徑壓縮時,為了方便起見,即使樹的高度發生了變化,我們也不修改rank的值)
在樹形資料結構裡,如果發生了退化的情況,那麼複雜度就會變得很高。因此,有必要想辦法避免退化的發生。在並查集中,只要按照如下方法就可以避免退化。
(1)對每棵樹,記錄這棵樹的高度(rank).
(2)合併時如果兩棵樹的rank不同,那麼從rank小的向rank大的連邊。
我做的第一題並查集 HDU 1213;
#include <iostream> #include <stdio.h> #include <string.h> #define N 1000 using namespace std; int father[N]; int num; void set_mark(int n) { int i; for(i = 1; i <= n; i++) { father[i] = i; } return; } int getfather(int v) { int geng = v; int temp; while(geng != father[geng]) { geng = father[geng]; } while(geng != v) { temp = father[v]; father[v] = geng; v = temp; } return geng; } void Merge(const int &i, const int &j) { int x = getfather(i); int y = getfather(j); if(x != y) { father[x] = y; num -= 1; } return; } int main() { int T; int n, m; cin >> T; while(T--) { int a, b; cin >> n; num = n; set_mark(n); cin >> m; for(int i = 0; i < m; i++) { cin >> a; cin >> b; Merge(a, b); } cout << num << endl; } return 0; }
以下每一塊是一個模板(分四個函式標記、找根、合併、查詢)
int father[maxn];
int n;
void Init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
// 遞迴法
int getFather(const int &v) {
if (father[v] == v)
return v;
else
return getFather(father[v]);
}
// 遞迴另一種寫法。
int getFather(const int &v) {
if (father[v] != v) {
int root = getFather(father[v]);
return father[v] = root;
}
else
return v;
}
// 非遞迴,且不帶路徑壓縮。
int getFather(const int &v) {
int r = v;
while(r != father[r])
r = father[r];
return r;
}
*/
// 非遞迴,路徑壓縮
int getFather(const int &v) {
int t1 = v, t2;
while (v != father[v])
v = father[v];
while (t1 != father[t1]) { // 沿途上所有結點的父親改成根。這一步是順便的,不增加時間複雜度,卻使得今後的操作比較快。這個優化稱為路徑壓縮。
t2 = father[t1];
father[t1] = v;
t1 = t2;
}
return v;
}
// 歸併:把節點i、節點j放到同一個根底下。
void merge(const int &i, const int &j) {
int x = getFather(i);
int y = getFather(j);
if (x != y) // 可選,主要是為了防止getFather()路徑壓縮的時候出現死迴圈。
father[x] = y; // 有向圖注意順序,該行程式碼含義:a->b。
}
// 查詢:查詢節點i跟節點j是否在同一根下。
bool judge(const int &i, const int &j) {
if (getFather(i) == getFather(j))
return true;
else
return false;
}
優化思路:
merge函式可以採用啟發式合併,思路就是把深度較小的那顆子樹併到深度較大的那顆子樹上。
int father[maxn];
int n;
void Init() {
for (int i = 0; i <= n; ++i) {
rank[i] = 0;
father[i] = i;
}
}
int getFather(const int &x) {
int px = x , i ;
while ( px != father[px]) // find root
px = father[px];
while ( x != px ) { // path compression
i = father [ x ];
father [ x ] = px ;
x = i;
}
return px ;
}
void merge(const int &x, const int &y) { // 下面還有一種寫法
x = getFather(x);
y = getFather(y);
if (rank[x] > rank[y])
father[y] = x;
else {
father[x] = y;
if (rank[x] == rank[y])
rank[y]++;
}
}
bool judge(const int &i, const int &j) {
if (getFather(i) == getFather(j))
return true;
else
return false;
}
int father[maxn], rank[maxn];
void Init(const int &v) {
father[v] = -1;
rank[v] = 0;
}
int getFather(const int &v) {
int t1 = v;
while (father[t1] != -1)
t1 = father[t1];
while (v!=t1) {
int t2 = father[v];
father[v] = t1;
v = t2;
}
return t1;
}
void merge(const int &a, const int &b) {
int t1 = getFathet1(a);
int t2 = getFathet1(b);
if(rank[t1] > rank[t2])
father[t2] = t1;
else
father[t1] = t2;
if(rank[t1] == rank[t2])
++rank[t2];
}
bool judge(const int &i, const int &j) {
if (getFather(i) == getFather(j))
return true;
else
return false;
}
/*
另一種寫法:
*/
int f[maxn], rank[maxn], num[maxn];
void Init() {
for (int i = 0; i <= n; ++i) {
rank[i] = 1;
num[i] = 1;
father[i] = i;
}
}
// f[]陣列存放根節點,rank[]陣列來存放根節點的深度,num[]陣列來存放節點個數,rank[]陣列和num[]陣列的初始化都應為1
// 啟發式合併:
void merge(int x, int y)
{
fx = getFather(x);
fy = getFather(y);
if (fx == fy) return;
if (rank[fx] > rank[fy]) {
father[fy] = fx;
num[fx] += num[fy];
}
else {
father[fx] = fy;
num[fy] += num[fx];
if (rank[fx] == rank[fy]) {
++rank[fy];
}
}
}
// 路徑壓縮:
int getFather(int x) {
if(father[x] == x)
return x;
else
return father[x] = getFather(father[x]);
}
// 仍有一種寫法:
int father[maxn];
void Init() {
for(int i = 0; i < n; ++i)
father[i] = -1;
}
int getFather(int x) {
if (father[x] < 0)
return x;
father[x] = getFather(father[x]);
return father[x];
}
int getFather(int x) {
int p = x, t;
while (father[p] >= 0)
p = father[p];
while (x != p) {
t = father[x];
father[x] = p;
x = t;
}
return x;
}
void merge(int x, int y) {
x = getFather(x);
y = getFather(y)
if (x == y) return;
if (father[x] < father[y]) {
father[x] += father[y];
father[y] = x;
} else {
father[y] += father[x];
father[x] = y;
}
}
bool judge(const int &i, const int &j) {
if (getFather(i) == getFather(j))
return true;
else
return false;
}