並查集
並查集是一種樹狀結構,他支援兩件事
我們把集合轉化成樹,一顆樹代表一個集合,樹根代表集合的老大,查詢隸屬集合就回傳樹根是誰(一個樹餔可能有兩顆樹根吧),合併的時侯,就把一顆樹的樹根只到另一顆,以下為詳細的描述。
初始
一開始的時候,每個點自成一個集合,所以把樹根都設為自己。
查詢
查詢的時候,要查到樹根為自己的點,為止否則的話就要繼續查。
| int Find(int x)
{
if (x == p[x])
return x;
return find(p[x]);
}
|
狀態壓縮:在合併之後原本被指向的樹根就沒用了,我們可以一邊做查詢時,一邊做更新。
| int Find(int x)
{
if (x == p[x])
return x;
return p[x] = find(p[x]);
}
|
合併
找出兩個點的樹根,將一個樹根合併指到另一個樹根。
| void Union(int a, int b)
{
a = Find(a);
b = Find(b);
if (a == b)
return;
p[a] = b;
}
|
啟發式合併:建立一個 代表樹的高度,亦是元素最大遞迴次數, 一開始為 。再來,我們每次都讓高度小的高度大的合併,如果遇到高度一樣的,就讓合併別人的樹高度加 。如果要把高度變為 ,則至少需要 個點,由此推出 N 個點所形成最高之高度為 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | void Union(int a, int b)
{
a = Find(a);
b = Find(b);
if (a == b)
return;
if (rank[a] < rank[b])
p[a] = b;
else if (rank[a] > rank[b])
p[b] = a;
else
{
p[a] = b;
rank[a]++;
}
}
|
也可以維護並查集的個數,個數大的合併個數小的並查集。
| void Union(int a, int b)
{
a = Find(a);
b = Find(b);
if (a == b)
return;
if (sz[a] < sz[b])
swap(a, b);
sz[a] += sz[b];
p[b] = a;
}
|
完整程式碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 | int p[N], rank[N];
void init()
{
for (int i = 0; i < N; i++)
{
p[i] = i;
rnak[i] = 1;
}
}
int Find(int x)
{
if (x == p[x])
return x;
return p[x] = find(p[x]);
}
void Union(int a, int b)
{
a = Find(a);
b = Find(b);
if (a == b)
return;
if (rank[a] < rank[b])
p[a] = b;
else if (rank[a] > rank[b])
p[b] = a;
else
{
p[a] = b;
rank[a]++;
}
}
|
相關題目
UVa 10608 - Friends
給定 個人的朋友關係,如果兩個人是朋友,他們在同一個朋友圈,問有幾個人在最大的朋友圈。
UVa 11503 - Virtual Friends
給定 個人的朋友關係,如果兩個人是朋友,問兩個人成為新朋友,他們的朋友圈總共有幾個人。
這兩題題目相似,都要維護每個集合的個數,最後答案會根據每個集合的數量計算出來。
UVa 10608 這題需要注意最大的朋友圈可能不只一個,如果有多個集合大小一樣,需要累加答案。
UVa 11503 給定朋友的名字,在實作上,會利用 map
將名字對應數字再做計算。
UVa 00615 - Is It A Tree?
給你數條有向邊,判斷是否為一顆樹?
有向樹有幾個條件:
- 只有一個入度 的點,其他都是入度 的點。
- 是連通圖且無環。
第二項條件可以用並查集或 DFS 判斷。
帶權並查集
在並查集上增加數值,數值會在查詢的時候更新,讓並查集能解決更多問題。
更新原則:我到根節點的距離=我到父節點的距離+父節點到根節點的距離。
| int find(int u) // p[u]: 父節點, root: 根節點
{
if (u != p[u])
return p[u];
int f = p[u];
int root = find(p[u]);
v[u] += v[p[u]];
return p[u] = root;
}
|
UVa 01329 - Corporative Network
給定 個節點和兩種操作,一開始每個點都沒有父節點。
- E x
: 輸出 x
到根節點的距離。
- I x y
: 將 x
的父節點設為 y
,距離為 。
帶刪除並查集
刪除操作是指將特定點 從當前集合移除,自己獨立一個集合或是加入新的集合,實作步驟如下:
- 開一個陣列 維護每個點的編號。
- 查詢 :
Find(id[x])
。
- 合併 :
Union(id[x],id[y])
。
- 將 從當前集合移除
- 扣除 在原本集合的貢獻。
- 為 產生新的編號。
- 保留舊編號,否則會影響 原本的子節點。
UVa 11987 - Almost Union-Find
給定 個點和 筆操作,一開始每個點都是自己一個集合,操作有三種:
- 1 p q
: 將 所在的集合合併。
- 2 p q
: 將 和 所在的集合合併。
- 3 p
: 詢問 所在的集合元素個數和總和。
參考程式碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 | #include <bits/stdc++.h>
using namespace std;
const int MXV = 200000 + 5;
int p[MXV], sum[MXV], sz[MXV];
int tot = -1, id[MXV];
struct DisjointSet
{
void init(int n = MXV - 1)
{
for (int i = 0; i <= n; i++)
{
sz[i] = 1;
p[i] = i;
sum[i] = i;
id[i] = i;
}
tot = n;
}
int find(int u) { return u == p[u] ? u : find(p[u]); }
void Union(int x, int y)
{
int fx = find(id[x]), fy = find(id[y]);
if (fx == fy)
return;
sz[fy] += sz[fx];
sum[fy] += sum[fx];
p[fx] = fy;
}
};
int main()
{
int n, m;
DisjointSet djs;
while (cin >> n >> m)
{
int x, y, z;
djs.init(n);
for (int i = 0; i < m; ++i)
{
cin >> x;
if (x == 3)
{
cin >> y;
int fy = djs.find(id[y]);
cout << sz[fy] << ' ' << sum[fy] << '\n';
continue;
}
cin >> y >> z;
int fy = djs.find(id[y]), fz = djs.find(id[z]);
if (fy == fz)
continue;
if (x == 2)
{
// remove from old set
sz[fy] -= 1;
sum[fy] -= y;
// create new set
id[y] = ++tot;
p[id[y]] = id[y];
sz[id[y]] = 1;
sum[id[y]] = y;
}
djs.Union(y, z);
}
}
}
|