1.並查集概述:
a.什麼叫並查集?
從字面意思理解,並就是合併,查就是查詢,集就是集合的意思。
並查集是一種用於分離集合操作的抽象數據結構類型。它所處理的是集合之間的關係,即動態維護和處理集合元素之間複雜的關係,當給出兩個元素的一個無序對(a,b)時,需要快速合併a和b所在的集合,這期間需要反覆查找某個元素所在的集合。
b.簡介:
在這種數據類型中,n個不同的元素被分爲若干組,每組是一個集合,這種集合叫做分離集合。並查集支持查找一個元素所屬的集合以及兩個元素各自所屬的集合的合併。
2.並查集中的基本操作:
a.初始化操作
首先我們要初始化一個並查集,一開始每一個元素相當於都是一個獨立的集合。
father數組代表集合
//初始化並查集
for (int i = 1; i <= n; i++) {
father[i] = i;
}
b.查找操作:
由於規定同一個集合中只存在一個根節點,因此查找操作就是對給定的結點尋找其根節點的過程。實現的方式可以是遞推或者遞歸,但是其思路都是一樣的,即反覆尋找父親結點,直到找到根節點(即father[i] = i)
I.遞推實現:
int findFather(int x) { //函數返回元素x所在集合的根節點
while (x != father[x]) { //如果不是根節點,繼續循環
x = father[x]; //獲得自己的父親節點
}
return x;
}
II.遞歸實現:
int findFather1(int x) {
if (x == father[x]) return x; //如果找到了根節點 就返回節點編號x
else return findFather1(father[x]); //否則,遞歸判斷x的父親結點是否是根節點
}
路徑壓縮查找方法:
上述所說並查集的查找方法是沒有優化的,在極端情況下效率較低。比如給出的元素是1e5,查詢次數在1e5那麼每次都要花費1e5的時間查找,那麼你就喫T了,怎麼優化?
father[1] = 1;
father[2] = 1;
father[3] = 2;
father[4] = 3;
路徑壓縮後:
father[1] = 1;
father[2] = 1;
father[3] = 1;
father[4] = 1;
如圖:
這樣就相當於把當前查詢結點的路徑上所有結點的父親結點指向根節點,查找的時候就不需要一直回溯去找父親了,查詢的複雜度爲O(1)。
我們怎麼實現?
在原有的find函數中,重新從x開始走一遍尋找根節點的過程,把路徑上的所有結點的父親改爲根節點r
//路徑壓縮查找函數(遞推)
int find(int x) {
int a = x; //由於下面的x會改變爲根節點,因此把原先的x保存一下
while (x != father[x]) { //尋找根節點
x = father[x];
}
//x存放的是根節點。把下面路徑上的所有節點的父親結點改爲根節點
while (a != father[a]) {
int z = a; //因爲a要被father[a]覆蓋,所以保存一下a的值
a = father[a]; //回溯父親結點
father[z] = x; //將原來的父親結點 改爲根節點x
}
return x;
}
遞歸寫法:
//路徑壓縮查找函數(遞歸)
int find(int v) {
if (v == father[v]) return v; //找到根節點
else {
int F = find(father[v]); //遞歸尋找father[v]的根節點v
father[v] = F; //將根節點F賦給father[v]
return F; //返回根節點
}
}
整個並查集的查找操作大致就是這樣了。
c.合併操作Union
合併是把兩個集合合併成一個集合,而合併的過程一般是把其中一個集合的根節點的父親結點指向另一個集合的根節點。
主要分爲兩步走:
1.對於給定的兩個元素a,b,判斷他們是否在同一個集合,調用上面的查找函數,查找根節點是否一樣。
2.合併兩個集合,我們在1中通過find函數找到了兩個根節點fa,fb那麼只要把其中的一個節點的父親結點指向另一個根節點即可。
father[fa] = fb即可。
//合併操作
void Union(int a, int b) {
int faA = findFather(a);
int faB = findFather(b);
if (faA != faB) {
father[faA] = faB;
}
}
3.入門並查集題目——親戚:
題目鏈接
a.問題描述:
b.分析:
這是一道很直觀可以發現使用並查集的一道題目。
有n個人,m個親戚關係,我們可以看做有n個集合,判斷是否有親戚關係只要判斷是否在同一集合,判斷是否在同一集合可以判斷這個點的根節點是否一樣,主要用到並查集的查詢,合併等操作。
c.AC_Code:
#include <cstdio>
using namespace std;
const int maxn = 2e4 + 5;
int father[maxn];
void init(int n) {
for (int i = 1; i <= n; i++) father[i] = i;
}
int find(int x) {
if (x == father[x]) return x;
else {
int f = find(father[x]);
father[x] = f;
return f;
}
}
void merge(int x, int y) {
int fa = find(x);
int fb = find(y);
if (fa != fb) father[fa] = fb;
}
int main() {
int n, m, q;
scanf("%d%d%d", &n, &m, &q);
init(n);
while (m --) {
int x, y;
scanf("%d%d", &x, &y);
if (find(x) != find(y)) {
merge(x, y);
}
}
while (q --) {
int x, y;
scanf("%d%d", &x, &y);
if (father[find(x)] == father[find(y)]) printf("Yes\n");
else printf("No\n");
}
return 0;
}