又一位資深製片人從索尼日本工作室離職
阿新 • • 發佈:2021-03-16
Union-Find
概述
定義:
並查集是一種樹形的資料結構,顧名思義,並和查,就是用於處理一些不相交的合併(Union)及查詢(Find)問題。常常用森林表示(見百度百科)。
操作:
並查集的操作主要有兩種:
- 合併(Union):把兩個不相交的集合合併為一個集合。
- 查詢(Find):確定元素屬於哪一個子集。還可以查詢兩個元素是否在同一個集合中。
重要思想:
並查集的一個最重要的思想就是:用集合中的一個元素來代表集合,既然是演算法,便是為了讓程式碼變得簡潔又高效,就像是一個團隊必定要有一個leader,不然群龍無首,效率只會更加低下。
關於並查集的思想以及概念,這篇文章將其類比為幫派和幫主,講的很好:傳送門
這裡我們通過Java語言的實現來順帶講解原理。
實現
初始化
public class UnionFindTest {
Node[] node;
//並查集中的結點
private static class Node{
int parent;//指向父節點的指標
boolean root;//是否為根節點,即是否為leader
private Node(){
parent = 1;
root = true;//定義leader
}
}
//將每個元素初始化為一顆單結點樹
public UnionFindTest (int n){
node = new Node[n + 1];
for(int i= 0; i <= n; i++){
node[i] = new Node();
}
}
}
find查詢
壹
public int find(int e){//查詢e的leader
while(!node[e].root){ //如果e的上級不是root
e = node[e].parent;//e繼續找父節點,直到找到leader
}
return e;//找到了leader
}
貳
// 查詢過程, 查詢元素p所對應的集合編號
// O(h)複雜度, h為樹的高度
private int find(int p){
// 不斷去查詢自己的父親節點, 直到到達根節點
// 根節點的特點: parent[p] == p
while(p != parent[p])
p = parent[p];
return p;
}
Union合併
public void union(int a,int b){
node[a].parent += node[b].parent;//讓b的上級為a
node[b].root = false;//令a的leader為總leader
node[b].parent = a;
}
路徑壓縮
優化時機是在執行 find操作 的時候對其進行路徑壓縮。
private int find(int p) {
while (p != parents[p]) {
parents[p] = parents[parents[p]];
p = parents[p];
}
return p;
}
壓縮流程:
對比一下原來的程式碼,我們可以發現只是進行了不大的改變。
在集合的根被遞迴地找到以後,p的父結點就引用它,這對通向根的路徑上的每一個節點遞迴地出現,因此實現了路徑壓縮。
我們讓節點數少的樹往節點數多的樹合併,並使用加權標記法。
路徑壓縮完整程式碼:
建立一個介面:
public interface UF {
int getSize();
boolean isConnected(int p, int q);
void unionElements(int p, int q);
}
實現類:
public class UnionFind implements UF{
private int[] rank; // rank[i]表示以i為根的集合所表示的樹的層數
private int[] parent; // parent[i]表示第i個元素所指向的父節點
// 建構函式
public UnionFind(int size){
rank = new int[size];
parent = new int[size];
// 初始化, 每一個parent[i]指向自己, 表示每一個元素自己自成一個集合
for( int i = 0 ; i < size ; i ++ ){
parent[i] = i;
rank[i] = 1;
}
}
@Override
public int getSize(){
return parent.length;
}
// 查詢過程, 查詢元素p所對應的集合編號
// O(h)複雜度, h為樹的高度
private int find(int p){
if(p < 0 || p >= parent.length)
throw new IllegalArgumentException("p is out of bound.");
// 不斷去查詢自己的父親節點, 直到到達根節點
// 根節點的特點: parent[p] == p
while (p != parents[p]) {
parents[p] = parents[parents[p]];
p = parents[p];
}
return p;
}
// 檢視元素p和元素q是否所屬一個集合
// O(h)複雜度, h為樹的高度
@Override
public boolean isConnected( int p , int q ){
return find(p) == find(q);
}
// 合併元素p和元素q所屬的集合
// O(h)複雜度, h為樹的高度
@Override
public void unionElements(int p, int q){
int pRoot = find(p);
int qRoot = find(q);
if( pRoot == qRoot )
return;
// 根據兩個元素所在樹的rank不同判斷合併方向
// 將rank低的集合合併到rank高的集合上
if(rank[pRoot] < rank[qRoot])
parent[pRoot] = qRoot;
else if(rank[qRoot] < rank[pRoot])
parent[qRoot] = pRoot;
else{ // rank[pRoot] == rank[qRoot]
parent[pRoot] = qRoot;
rank[qRoot] += 1; // 此時, 我維護rank的值
}
}
}
總結
- 用集合中的某個元素來代表這個集合,則該元素稱為此集合的代表元;
- 一個集合內的所有元素組織成以代表元為根的樹形結構;
- 對於每一個元素 p,parents[p] 存放 p 在樹形結構中的父親節點(如果 p 是根節點,則令parents[p] = p);
- 對於查詢操作,假設需要確定 p 所在的的集合,也就是確定集合的代表元。可以沿著pre[p]不斷在樹形結構中向上移動,直到到達根節點。