概述
並查集是一種特別的資料結構,在解決連通性問題屢試不爽。以下程式碼均為java語言的實現
並查集的作用先總體說一下
- 1、將兩個元素聯通起來(union)起來,形成一個通路
- 2、檢查任意兩個元素是否是連通的
- 3、連通後,如果把連通的一組數看成一組,那麼還能記錄一共有多少組數
- 4、當然也還能求組員數最大、最小的組的數量【通過計數變形】
對外基礎方法提供2個方法
public void join(int a, int b)
連通a節點和b節點public boolean isJoined(int a, int b)
判斷任意兩個節點是否是連通的
內部需要一個輔助方法
來查詢任意節點的根節點,本身你可以理解並查集是一顆樹,但這顆是反向尋找,並不是像通常的樹,自頂向下dfs的,而是一直向上找尋根節點
private int findP(int x)
查詢x元素的根節點
快速查詢版本
將連通節點的父節點都設定為相同節點(索引),查詢的時候 f(x)=parent[x],但更新比較麻煩,每次更新需要遍歷所有元素O(n)的複雜度,n是總的元素個數
/**
* 並查集 快速查詢
*/
public class UFQuickFind {
int[] parent;
public UFQuickFind(int size) {
parent = new int[size];
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
}
}
private int findP(int x) {//查詢操作,其實是查詢 根節點
if (x < 0 || x >= parent.length)
return -1; //或者直接丟擲異常
return parent[x]; //直接返回 parent就可以了,因為所有 連線 的節點parent值都相同
}
public void join(int a, int b) {
int ap = findP(a);
int bp = findP(b);
if (ap != bp) {
for (int i = 0; i < parent.length; i++) {
if (parent[i] == ap)
parent[i] = bp; //修改成相同的 parent
}
}
}
public boolean isJoined(int a, int b) { //兩個節點是否是 連線的
return findP(a) == findP(b);
}
}
快速合併版
合併的時候,a,b元素,直接讓將a的父親指向b的父親。【通俗一點 a節點的父親 認b節點的根節點做父親了。a節點認祖歸宗了】
/**
* 快速合併
*/
public class UFQuickUnion {
int[] parent;
public UFQuickUnion(int size) {
parent = new int[size];
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
}
}
private int findP(int x) {//查詢操作,其實是查詢 根節點
if (x < 0 || x >= parent.length)
return -1; //或者直接丟擲異常
while (parent[x] != x)//一直搜尋到根節點
x = parent[x];
return x;
}
public void join(int a, int b) {
int ap = findP(a);
int bp = findP(b);
if (ap != bp) {
parent[ap] = bp;//讓其中一個節點的根節點 指向 另外一個節點的根節點
}
}
public boolean isJoined(int a, int b) { //兩個節點是否是 連線的
return findP(a) == findP(b);
}
}
基於每株節點數的合併
由於快速合併的過程,合併的過程是隨機的,如果所有節點都合併到一起,那麼最後這顆樹可能變成一個連結串列【就是接龍,成為一條線】,通過節點數來改進,節點數少的接到節點數多的上面,這樣肯定不會成一個連結串列
public class UFNumsUnion {
int[] parent;
int[] nums;//節點數
public UFNumsUnion(int size) {
parent = new int[size];
nums = new int[size];
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
nums[i] = 1;
}
}
private int findP(int x) {//查詢操作,其實是查詢 根節點
if (x < 0 || x >= parent.length)
return -1; //或者直接丟擲異常
while (parent[x] != x)//一直搜尋到根節點
x = parent[x];
return x;
}
public void join(int a, int b) {
int ap = findP(a);
int bp = findP(b);
if (ap != bp) {
//a節點多,就將 b節點往a節點上合併,這樣的話可以減少樹的高度
if (nums[ap] > nums[bp]) {
parent[bp] = ap;
nums[ap] += nums[bp]; //根節點的節點數 增加了 被合併節點的節點數
} else {
parent[ap] = bp;
nums[bp] += nums[ap];
}
}
}
public boolean isJoined(int a, int b) { //兩個節點是否是 連線的
return findP(a) == findP(b);
}
}
基於層數的實現
【基於每株節點數的合併】雖然已經挺好的了,但偶爾也會不太合理的情況如下所示
A: o
/ | \ \
o o o o 5個節點,兩層
B: o
/ |
o o 4個節點 3層
/
o
上面這種情況按照基於節點數的合併,會得到一個4層的樹(層的深度增加),而更合理的方式是讓A接到B上,這樣最後整體層數保持在3層。層數越小,那麼findP
方法就會變快,而合併也因為呼叫findP
方法也會加快。
基於層數的實現,永遠讓層數矮的接到層數高的樹上
public class UFRankUnion {
int[] parent;
int[] rank;//樹的層數
int plant; //一共有多少株樹
public UFRankUnion(int size) {
plant = size;
parent = new int[size];
rank = new int[size];
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
rank[i] = 1;
}
}
private int findP(int x) {//查詢操作,其實是查詢 根節點
if (x < 0 || x >= parent.length)
return -1; //或者直接丟擲異常
while (parent[x] != x)//一直搜尋到根節點
x = parent[x];
return x;
}
public void join(int a, int b) {
int ap = findP(a);
int bp = findP(b);
if (ap != bp) {
plant--;
//a的層數越高,就將層數少的合併到層數高的上面
if (rank[ap] > rank[bp])
parent[bp] = ap;
else if (rank[ap] < rank[bp]) {
parent[ap] = bp;
} else {
//相同情況的話,隨便就可以,但總體層高會增加1,畫一個相同層的樹合併一下就知道
parent[ap] = bp;
rank[bp]++;
}
}
}
public int getPlant() {
return plant;
}
public boolean isJoined(int a, int b) { //兩個節點是否是 連線的
return findP(a) == findP(b);
}
}
路徑壓縮
我們設想一下並查集的理想狀態,所有樹都是深度為1的樹,這種理想狀況的findP的時間複雜度為O(1),大大提高了查詢效能,如下圖所示
o o
/ | \ \ / | \
o o o o o o o
所有的樹都是以這種方式去組合的
但是我們知道,如果A和B合併,層高是不是又變成2層了,如何才能讓層高再次恢復到1層呢。這裡路徑壓縮的一種方式是在查詢的時候,將查詢的節點不斷往上提高,直接接到根節點上
節點1 o
/ | \
節點2 o o o
/
節點3 o 節點1
查詢節點3的時候,是不是讓節點3接到 節點2的父節點上 parent[節點3]= parent[節點2]
public class UFRankUnionCompressPath {
int[] parent;
int[] rank;//樹的層數
int plant; //一共有多少株樹
public UFRankUnionCompressPath(int size) {
plant = size;
parent = new int[size];
rank = new int[size];
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
rank[i] = 1;
}
}
private int findP(int x) {//查詢操作,其實是查詢 根節點
if (x < 0 || x >= parent.length)
return -1; //或者直接丟擲異常
while (parent[x] != parent[parent[x]])
//如果parent[x]==parent[parent[x]] 說明這顆樹到了第二層,或者第一層
//第一層 因為parent[x]=x所以有 parent[x]==parent[parent[x]]
//第二層 因為第二層的parent是根節點 有: parent[x]= root
//所以有 parent[parent[x]]= parent[root] 而本身 parent[root]=root
{
// x = parent[x]; //
parent[x] = parent[parent[x]]; //將下層節點往頂層提升,最終
}
return parent[x];
}
public void join(int a, int b) {
int ap = findP(a);
int bp = findP(b);
if (ap != bp) {
plant--;
//a的層數越高,就將層數少的合併到層數高的上面
if (rank[ap] > rank[bp])
parent[bp] = ap;
else if (rank[ap] < rank[bp]) {
parent[ap] = bp;
} else {
//相同情況的話,隨便就可以
parent[ap] = bp;
rank[bp]++;
}
}
}
public int getPlant() {
return plant;
}
public boolean isJoined(int a, int b) { //兩個節點是否是 連線的
return findP(a) == findP(b);
}
}
期待下一篇,並查集運用相關得演算法題及題解