一、定義
並查集是一種樹型的數據結構,用於處理一些不相交集合的合併及查詢問題。
基礎的並查集能實現以下三個操作:1.建立集合;2.查找某個元素是否在一給定集合內(或查找一個元素所在的集合); 3.合併兩個集合.“並”“查”“集”三字由此而來。
並查集能解決的問題一般可以轉化爲這樣的形式:初始時n個元素分屬不同的n個集合,通過不斷的給出元素間的聯繫,要求實時的統計元素間的關係(即是否存在直接或間接的聯繫)。
並查集本身不具有結構,可以用數組、鏈表以及樹等實現。最常用的是數組實現。
二、實現
數組實現:
建立標記數組father,用father[i]表示元素i所屬集合的標記。
圖示 |
1.建立集合
2.合併集合&查找元素所屬集合
因此,比較兩個元素x,y是否是同一集合的方法就是比較find(x)是否等於find(y)。
合併集合的方法就是將其中一個點所在集合的根結點的父結點設定爲另一個點所在集合的根結點。
|
1.路徑壓縮
前面的做法就是將元素的父親結點指來指去地指,當這棵樹是鏈的時候,可見判斷兩個元素是否屬於同一集合需要O(n)的時間。
舉個例子:在前面方法的第5步後,如果要查詢第3、5元素所在集合的根結點,那麼每次都需要查詢father[3](father[5])、father[2](father[4])、father[1]的值。
於是,路徑壓縮產生了作用。
路徑壓縮就是在找完根結點之後,在遞歸回來的時候順便把路徑上元素的父親結點都設爲根結點。
int find(int x)//遞歸寫法
{
if(father[x]!=x)
father[x]=find(father[x]);
return father[x];
}
int find(int x)//非遞歸寫法,不太好記但是更快,列幾組數據試一下也不難理解
{
int r=x,q;
while(r!=father[r])
r=father[r];
while(x!=r)
{
q=father[x];
father[x]=r;
x=q;
}
return r;
}
(這個優化平時都可以用,但是對於某些題會造成麻煩,例如加權並查集,這樣的題需要特殊處理)在合併兩個集合(就是兩棵樹)的時候,如果待合併的樹的深度不相同,那麼就有兩種選擇:一種是以深度較小的樹的根結點爲新的根結點,另一種是以深度較大的樹的根結點爲新的根結點。而事實上,選擇以深度較大的樹的根結點爲新的根結點較好,因爲這樣的話新生成的樹深度會更小,可以防止樹的退化(退化指越來越接近鏈表,即深度大而分支少),使資源利用更合理。而合併時這樣選擇,就叫做“按秩合併”。
按秩合併的基本思想是將深度較小的樹指到深度較大的樹的根上。
按秩合併需要新開一個數組depth來記錄深度。depth[x]是(("以x爲根結點的樹"的某個葉結點到x的最長路徑上)邊的數目)的一個最大值。(即以x爲根結點的樹的樹高)
(這個優化比較麻煩,簡單題一般不用)
void make()
{
int i;
for(i=1;i<=n;i++)
{
father[i]=i;
depth[i]=0;//如果初值爲0則可以省略
}
}
void union1(int x,int y)
{
int fx=find(x),fy=find(y);
if(depth[fx]>depth[fy])
father[fy]=fx;
else
{
father[fx]=fy;
if(depth[fx]==depth[fy])
depth[fy]++;
}
}
①求無向圖最小生成樹的Kruskal算法
Kruskal將一個連通塊當做一個集合(連通塊指無向圖中相互連通的一些點)。對於一張有n個點的無向圖,首先將所有的邊按從小到大排序,並認爲每一個點都是孤立的,分屬於n個獨立的集合。然後按順序枚舉每條邊。如果這條邊連接兩個不同的集合,那麼就將這條邊加入最小生成樹,這兩個不同的集合就合併成了一個;如果這條邊連接的兩個點屬於同一集合,就跳過。直到選了n-1條邊爲止。具體參見Kruscal算法。
有的時候,不僅需要像普通並查集一樣記錄一些元素之間有無關係,還需要記錄它們之間有怎樣的關係,這時候就需要引入加權並查集。
通常情況下,用一個數組r來記錄這些關係,r[i]表示元素i與父結點的關係。至於是什麼關係,還要根據具體要求來看。
在find(x)函數中進行路徑壓縮的同時,許多結點的父結點會改變,這時就需要根據實際情況調整權值以保證其正確性。
在union1(x,y)函數中,(不妨設將y集合併入x集合)由於y的父結點的改變,需要調整y對應的權值,但不需要調整y的子結點對應的權值,因爲子結點權值會在find(子結點)時得到調整。
典型例題:
1.銀河英雄傳說
一道裸的加權並查集的題
2.食物鏈
一道變式題