作者:Grey
原文地址:使用並查集處理集合的合併和查詢問題
有若干個樣本a、b、c、d…
,假設型別都是V
,在並查集中一開始認為每個樣本都在單獨的集合裡,使用者可以在任何時候呼叫如下兩個方法 :
方法1:查詢樣本x
和樣本y
是否屬於一個集合,即:
boolean isSameSet(V x,V y);
方法2:把x
和y
各自所在集合的所有樣本合併成一個集合,即:
void union(V x, V y)
但是,isSameSet
和union
方法的代價越低越好,在這兩個方法呼叫非常頻繁的情況下,這兩個方法最好能做到O(1)
的時間複雜度。
節點型別定義如下:
private static class Node<V> {
private final V value;
public Node(V value) {
this.value = value;
}
}
但是每個節點都有一條往上指的指標,節點a
往上找到的頭節點,叫做a
所在集合的代表節點,查詢x
和y
是否屬於同一個集合,就是看看找到的代表節點是不是一個,如果代表節點是同一個,說明這兩個節點就是在同一集合中,把x
和y
各自所在集合的所有點合併成一個集合,只需要某個集合的代表點掛在另外一個集合的代表點的下方即可。
我們可以使用三張雜湊表:
// 快速找到某個節點是否存在
private HashMap<V, Node<V>> nodeMap;
// 找到某個節點的父節點
private HashMap<Node<V>, Node<V>> parentMap;
// 每個代表節點代表的節點個數
private HashMap<Node<V>, Integer> sizeMap;
其中的parentMap
就充當了找代表節點的責任,查詢任何一個節點x
的代表節點,只需要呼叫如下方法即可:
parentMap.get(x)
現在以一個實際例子來說明並查集的基本操作,假設現在有如下的樣本,現在每個樣本都是獨立的集合,每個樣本各自都是自己的代表節點,
假設我做如下操作,
第一個操作,將a
和b
合併成一個集合
union(a,b)
操作完畢後,a
和b
將做如下合併操作,合併後,先隨便以一個點作為合併集合的代表節點,假設代表節點用藍色表示
第二個操作,將b
和d
合併成一個集合
union(b,d)
此時,b
將找到其代表節點a
,d
將找到其代表節點也就是它本身d
,將d
和a
合併在一起,假設我們用a
去連d
,然後將a
,b
,d
這個集合的代表節點更新為d
即:
第三個操作,將b
和h
合併成一個集合
union(b,h)
此時,b
將找到其代表節點d
,h
將找到其代表節點也就是它本身h
,將d
和h
合併在一起,假設我們用d
去連h
,然後將a
,b
,d
,h
這個集合的代表節點更新為h
,如下圖
假設e
,g
,j
都做同樣的合併
union(e,g)
union(g,j)
結果如下
最後,假設我們呼叫
union(j,b)
即:將j
所在集合和b
所在集合合併在一起,那麼就需要j
一直向上找到其代表節點e
,b
向上找到其代表節點h
,然後將兩個代表節點連線起來即可,比如可以是這樣
根據如上流程範例,我們可以瞭解到一個問題,就是如果兩個節點距離各自的代表節點比較長,比如上面的b
點距離其代表節點h
就比較長,隨著資料量增多,這會極大影響效率,所以,並查集的第一個優化就是:
節點往上找代表點的過程,把沿途的鏈變成扁平的
舉例說明,我們上述例子中的第二個操作,將b
和d
合併成一個集合
union(b,d)
此時,b
將找到其代表節點a
,d
將找到其代表節點也就是它本身d
,將d
和a
合併在一起,假設我們用a
去連d
,然後將a
,b
,d
這個集合的代表節點更新為d
即:
如果做了扁平化操作,那麼在合併b
和d
的過程中,不是簡單的用a
去連d
,而是將a
節點的所有下屬節點都直連d
,所以就變成了:
如果我們每一次做union
操作,都順便做一次扁平化,那麼上述例子在最後一步執行之前,應該是這樣的狀態
如果變成了這樣的狀態,那麼每次找自己的代表節點這個操作,只需要往上看一次就可以。這在具體程式碼實現上使用了佇列,即,將某個節點往上直到代表節點的所有元素都記錄在佇列裡面,然後將記錄在佇列中的所有元素的父節點都指向代表節點。
private Node<V> findFather(Node<V> node) {
// 沿途節點都放佇列裡面
// 然後將佇列元素做扁平化操作
Queue<Node<V>> queue = new LinkedList<>();
while (node != parentMap.get(node)) {
queue.offer(node);
node = parentMap.get(node);
}
while (!queue.isEmpty()) {
// 優化2:扁平化操作
parentMap.put(queue.poll(), node);
}
return node;
}
並查集的第二個優化是:
小集合掛在大集合的下面
舉例說明,假設我們經過了若干次的union
之後,形成了如下兩個集合,顯然,h
為代表節點的集合是小集合,只有四個元素,而以e
為代表節點的集合是相對來說就是大集合。
如果此時,要合併b
節點所在集合(代表節點為h
)和j
幾點所在集合(代表節點為e
),那麼就面臨到底應該遷移e
及其下面所有的點到h
上,還是應該遷移h
及下面所有的點到e
上,從兩個集合的數量上來說,遷移h
集合到e
上的代價要比遷移e
到h
上的代價要低的多,所以這就是並查集的第二個優化。
經過了如上兩個優化,並查集中的union
或者isSameSet
方法如果呼叫很頻繁,那麼單次呼叫的代價為O(1),兩個方法都如此。
package snippet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class Code_0049_UnionFind {
public static class UnionFind<V> {
// 快速找到某個節點是否存在
private HashMap<V, Node<V>> nodeMap;
// 找到某個節點的父節點
private HashMap<Node<V>, Node<V>> parentMap;
// 每個代表節點代表的節點個數
private HashMap<Node<V>, Integer> sizeMap;
public UnionFind(List<V> values) {
nodeMap = new HashMap<>();
parentMap = new HashMap<>();
sizeMap = new HashMap<>();
for (V v : values) {
Node<V> n = new Node<>(v);
nodeMap.put(v, n);
parentMap.put(n, n);
sizeMap.put(n, 1);
}
}
// v1,v2必須是已經存在nodeMap中的元素
public void union(V v1, V v2) {
if (!nodeMap.containsKey(v1) || !nodeMap.containsKey(v2)) {
return;
}
if (!isSameSet(v1, v2)) {
Node<V> v1Parent = nodeMap.get(v1);
Node<V> v2Parent = nodeMap.get(v2);
Node<V> small = sizeMap.get(v1Parent) > sizeMap.get(v2Parent) ? v2Parent : v1Parent;
Node<V> large = small == v1Parent ? v2Parent : v1Parent;
sizeMap.put(large, sizeMap.get(large) + sizeMap.get(small));
// 優化1:小集合掛在大集合下面
parentMap.put(small, large);
parentMap.remove(small);
}
}
private Node<V> findFather(Node<V> node) {
Queue<Node<V>> queue = new LinkedList<>();
while (node != parentMap.get(node)) {
queue.offer(node);
node = parentMap.get(node);
}
while (!queue.isEmpty()) {
// 優化2:扁平化操作
parentMap.put(queue.poll(), node);
}
return node;
}
public boolean isSameSet(V v1, V v2) {
if (!nodeMap.containsKey(v1) || !nodeMap.containsKey(v2)) {
return false;
}
return findFather(nodeMap.get(v1)) == findFather(nodeMap.get(v2));
}
private static class Node<V> {
private final V value;
public Node(V value) {
this.value = value;
}
}
}
}