在計算機科學中,並查集是一種樹型的數據結構,用於處理一些不交集(Disjoint Sets)的合併及查詢問題。有一個**聯合-查找算法(union-find algorithm)**定義了兩個用於此數據結構的操作:
- Find:確定元素屬於哪一個子集。它可以被用來確定兩個元素是否屬於同一子集。
- Union:將兩個子集合併成同一個集合。
由於支持這兩種操作,一個不相交集也常被稱爲聯合-查找數據結構(union-find data structure)或合併-查找集合(merge-find set)。其他的重要方法,MakeSet,用於創建單元素集合。有了這些方法,許多經典的劃分問題可以被解決。[1]
並查集
並查集是一種樹形結構,又叫“不相交集合”,保持了一組不相交的動態集合,每個集合通過一個代表來實現,代表即是集合中的某個成員,通常選擇根節點作爲此代表。
主要操作
Make_Set(x )
建立一個新的集合,其唯一成員就是x,因此這個集合的代表也是x,並查集要求各集合是不相交的,因此要求x沒有在其他集合中出現過。Find_Set(x)
返回能代表x所在集合的節點,通常返回x所在集合的根節點。有遞歸和非遞歸兩種方法.Union_Set(x,y)
將包含x,y的動態集合合併爲一個新的集合。合併兩個集合的關鍵是找到兩個集合的根節點,如果兩個根節點相同則不用合併;如果不同,則需要合併。
通過步驟分析,易知:- 當只有一個元素的時候,元素即是集合。
- 每次在連接之後,都會產生一個代表。
- 兩個集合連接過程中,小的集合中將代表節點指向大的集合代表節點上。
易知確定兩個元素是否屬於同一子集,是根據他們是否有同一個代表來判斷的.
並查集的優化
[路徑壓縮]
通過上面分析,容易知道在判斷兩個節點是否是屬於同一子集的時候,如下圖b,判斷b
,g
是否屬於同一子集,b
每次都需要經過b -> h ->c ->f
,g
每次都要經過g - >d -> f
,在判斷出兩個節點的代表節點爲同一個,判斷出屬於同一個節點。
優化過程是:每次經過了上述過程之後,都將沿途中的幾點直接指向代表節點。
如上操作之後,集合中元素變成如下:
查找路徑上的每個節點都直接指向根節點,這樣下次再找根節點的時間複雜度會變成o(1)
.
【按秩合併】
合併時,如果兩個集合的秩相同,任選一個根做爲父節點,並增加其秩。
秩不同時,讓較小秩的集合指向較大秩的集合,這時秩的大小不變。
秩和集合的數目是不一樣的,秩表示節點高度的一個商界;集合的數目表示集合中節點的總數。
代碼實現
public class UnionFindSet {
public static class Node{
// whatever
}
public HashMap<Node,Node > fatherMap ; // key: child , value:father
public HashMap<Node,Integer> sizeMap; // 秩的大小, node 代表節點
public UnionFindSet() {
fatherMap = new HashMap<Node, Node>();
sizeMap = new HashMap<Node, Integer>();
}
public void makeSets(List<Node> nodes) {
fatherMap.clear();
sizeMap.clear();
for (Node node : nodes) {
fatherMap.put(node, node);
sizeMap.put(node, 1);
}
}
private Node findHead(Node node) {
Node father = fatherMap.get(node);
if (father != node) {
father = findHead(father);
}
fatherMap.put(node, father);
return father;
}
public boolean isSameSet(Node a, Node b) {
return findHead(a) == findHead(b);
}
public void union(Node a, Node b) {
if (a == null || b == null) {
return;
}
Node aHead = findHead(a);
Node bHead = findHead(b);
if (aHead != bHead) {
int aSetSize= sizeMap.get(aHead);
int bSetSize = sizeMap.get(bHead);
// 按照大小合併
if (aSetSize <= bSetSize) {
fatherMap.put(aHead, bHead);
sizeMap.put(bHead, aSetSize + bSetSize);
} else {
fatherMap.put(bHead, aHead);
sizeMap.put(aHead, aSetSize + bSetSize);
}
}
}
}
應用場景
假設有一比較常見的問題:[3]
在一個地圖中,找出一共有多少個島嶼。
我們用一個二維數組表示這個地圖,地圖中的 1 表示陸地,0 表示水域。一個島嶼是指由上下左右相連的陸地,並且被水域包圍的區域。
你可以假設地圖的四周都是水域。
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 0, 0, 0, 0, 0 },
{ 0, 1, 1, 1, 0, 0, 0, 0 },
{ 0, 0, 1, 0, 0, 1, 1, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0 },
{ 0, 0, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 1 },
比如這種情況,期望輸出結果爲4。
【常規思路】
public class Test {
static int daoyu[][] =
{
{ 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 0, 0, 0, 0, 0 },
{ 0, 1, 1, 1, 0, 0, 0, 0 },
{ 0, 0, 1, 0, 0, 1, 1, 0 },
{ 0, 0, 0, 0, 0, 1, 1, 0 },
{ 0, 0, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 1 },
};
public static void main(String[] args) {
int count = 0;
for (int i = 0; i < daoyu.length; i++)
for (int j = 0; j < daoyu[i].length; j++)
if (daoyu[i][j] == 1) {
count++;
lj(i, j);
}
System.out.println("島嶼數量爲:" + count);
}
//遞歸修改附近數值
static void lj(int i, int j) {
daoyu[i][j] = 2;
// 上邊
if (i - 1 >= 0 && daoyu[i - 1][j] == 1)
lj(i - 1, j);
// 下邊
if (i + 1 < daoyu.length && daoyu[i + 1][j] == 1)
lj(i + 1, j);
// 左邊
if (j - 1 >= 0 && daoyu[i][j - 1] == 1)
lj(i, j - 1);
// 右邊
if (j + 1 < daoyu[i].length && daoyu[i][j + 1] == 1)
lj(i - 1, j + 1);
}
}
【並行系統解決方案】
這是比較常見的解決方案,但是,假設我地圖非常大,大到單核cpu無法承載運算,此時就需要搭建並行系統。
可以把整個地圖劃分成比較小的部分,如圖3所示,有八個島嶼。
使用並行系統的計算方式,可以將島嶼分成四個部分(當然,還可以更小),如圖3綠線分割。每個部分分別計算出各個島嶼的數量,左上爲3個,右上爲2個,左下爲3個,右下爲4個。此時需要解決的問題就是各個島嶼的合併問題。易知合併島嶼只需要處理邊界問題即可。先考慮左上和右上合併的情況。(其他合併類似,是一個遞歸過程)。易知c和左上沒有邊界之間的交叉,判斷下一個a,b。
通過並查集判斷是否屬於同一個子集來判斷是否已經在上次判斷爲屬於同一個子集。如果沒有,合併。對合並的操作,對兩個島嶼的數量相加,在減去合併島嶼的數量。如上圖3,左上和右上合併之後,上面的島嶼變爲4個。按照此類過程,一直到合併結束。
如果,不用並查集,方法將會變得比較困難。
代碼不進行書寫 。自行code.
參考:
https://zh.wikipedia.org/wiki/%E5%B9%B6%E6%9F%A5%E9%9B%86 [1]
https://blog.csdn.net/Yu1441/article/details/78895168 [3]