算法:並查集
快速掌握
理解算法
在計算機科學中,並查集是一種樹型的數據結構,用於處理一些不交集(Disjoint Sets)的合並及查詢問題。有一個聯合-查找算法(union-find algorithm)定義了兩個用於此數據結構的操作:
- Find:確定元素屬於哪一個子集。這個確定方法就是不斷向上查找找到它的根節點,它可以被用來確定兩個元素是否屬於同一子集。
- Union:將兩個子集合並成同一個集合。
由於支持這兩種操作,一個不相交集也常被稱為聯合-查找數據結構(union-find data structure)或合並-查找集合(merge-find set)。其他的重要方法,MakeSet,用於建立單元素集合。有了這些方法,許多經典的划分問題可以被解決。
為了更加精確的定義這些方法,需要定義如何表示集合。一種常用的策略是為每個集合選定一個固定的元素,稱為代表,以表示整個集合。接着,Find(x) 返回 x 所屬集合的代表,而 Union 使用兩個集合的代表作為參數。
說明:左邊是A,筆誤!
上圖中簡單演示了並查集的兩個操作,一個是FIND,一個UNION。
並查集(樹)
並查集(樹)是一種將一個集合以樹形結構進行組合的數據結構,如上圖所示。其中每一個節點保存着到它的父節點的引用(
在並查集樹中,每個集合的代表即是集合的根節點。
- “查找”根據其父節點的引用向根行進直到到底樹根。
- “聯合”將兩棵樹合並到一起,這通過將一棵樹的根連接到另一棵樹的根。
實現這樣操作的偽代碼如下:
function MakeSet(x)
x.parent := x
function Find(x)
if x.parent == x
return x
else
return Find(x.parent)
function Union(x, y)
xRoot := Find(x)
yRoot := Find(y)
xRoot.parent := yRoot
這是並查集樹林的最基礎的表示方法,這個方法不會比鏈表法好,這是因為創建的樹可能會嚴重不平衡;然而,可以用兩種辦法優化。
優化方法一:按秩合並
第一種方法,稱為“按秩合並”,即總是將更小的樹連接至更大的樹上。因為影響運行時間的是樹的深度,更小的樹添加到更深的樹的根上將不會增加秩除非它們的秩相同。在這個算法中,術語“秩”替代了“深度”,因為同時應用了路徑壓縮時(見下文)秩將不會與高度相同。單元素的樹的秩定義為0,當兩棵秩同為r的樹聯合時,它們的秩r+1。只使用這個方法將使最壞的運行時間提高至每個MakeSet、Union或Find操作、。
優化后的MakeSet
和Union
偽代碼:
function MakeSet(x)
x.parent := x
x.rank := 0
function Union(x, y)
xRoot := Find(x)
yRoot := Find(y)
if xRoot == yRoot
return
// x和y不在同一個集合,合並它們。
if xRoot.rank < yRoot.rank
xRoot.parent := yRoot
else if xRoot.rank > yRoot.rank
yRoot.parent := xRoot
else
yRoot.parent := xRoot
xRoot.rank := xRoot.rank + 1
優化方法二:路徑壓縮
第二個優化,稱為“路徑壓縮”,是一種在執行“查找”時扁平化樹結構的方法。關鍵在於在路徑上的每個節點都可以直接連接到根上;他們都有同樣的表示方法。為了達到這樣的效果,Find
遞歸地經過樹,改變每一個節點的引用到根節點。得到的樹將更加扁平,為以后直接或者間接引用節點的操作加速。
這兒是Find
:
function Find(x)
if x.parent != x
x.parent := Find(x.parent)
return x.parent
這兩種方法的優勢互補,同時使用二者的程序每個操作的平均時間僅為,
是
的反函數,其中
是急速增加的阿克曼函數。因為
是其的反函數,故
在
十分巨大時還是小於5。因此,平均運行時間是一個極小的常數。
實際上,這是漸近最優算法:Fredman和Saks在1989年解釋了的平均時間內可以獲得任何並查集。
並查集算法-Java實現
package search;
public class UnionFindSet {
private int[] parents_;
private int[] ranks_;
public UnionFindSet(int n)
{
parents_ = new int[n+1];
ranks_ = new int[n+1];
for(int i=0;i<=n;i++)
{
parents_[i]=i;
ranks_[i]=i;
}
}
public boolean Union(int u,int v)
{
int pu = Find(u);
int pv = Find(v);
if(pu==pv)
return false;
if (ranks_[pv] > ranks_[pu])
parents_[pu] = pv;
else if (ranks_[pu] > ranks_[pv])
parents_[pv] = pu;
else {
parents_[pv] = pu;
ranks_[pu] += 1;
}
return true;
}
public int Find(int u)
{
while (parents_[u]!=u)
{
parents_[u]=parents_[parents_[u]];
u=parents_[u];
}
return u;
}
}
主要操作
合並兩個不相交集合
操作很簡單:先設置一個數組(陣列)Father[x],表示x的“父親”的編號。 那么,合並兩個不相交集合的方法就是,找到其中一個集合最父親的父親(也就是最久遠的祖先),將另外一個集合的最久遠的祖先的父親指向它。
void Union(int x,int y)
{
fx = getfather(x);
fy = getfather(y);
if(fy!=fx)
father[fx]=fy;
}
判斷兩個元素是否屬於同一集合
仍然使用上面的數組。則本操作即可轉換為尋找兩個元素的最久遠祖先是否相同。尋找祖先可以采用遞歸實現,見后面的路徑壓縮算法。
bool same(int x,int y)
{
return getfather(x)==getfather(y);
}
/*返回true 表示相同根結點,返回false不相同*/