並查集--算法,優化,變種

一、定義
並查集是一種樹型的數據結構,用於處理一些不相交集合的合併及查詢問題。
基礎的並查集能實現以下三個操作:1.建立集合;2.查找某個元素是否在一給定集合內(或查找一個元素所在的集合); 3.合併兩個集合.“並”“查”“集”三字由此而來。
並查集能解決的問題一般可以轉化爲這樣的形式:初始時n個元素分屬不同的n個集合,通過不斷的給出元素間的聯繫,要求實時的統計元素間的關係(即是否存在直接或間接的聯繫)。
並查集本身不具有結構,可以用數組、鏈表以及樹等實現。最常用的是數組實現。

二、實現
數組實現:
建立標記數組father,用father[i]表示元素i所屬集合的標記。


圖示

1.建立集合
開始時每個元素各自獨立,可以把每個元素所屬集合標記爲其自身序號。

void make()
{
	int i;
	for(i=1;i<=n;i++)
    		father[i]=i;
}

2.合併集合&查找元素所屬集合
find函數返回的是元素所屬集合的根結點(別忘了並查集是樹型的數據結構)。方法就是循環或遞歸,尋找當前結點的父結點,父結點的父結點,父結點的父結點的父結點......直到找到一個結點的父結點是它自己,那麼它就是根結點。

int find(int x)//非遞歸寫法
{
	while(father[x]!=x) x=father[x];
	return x;
}
int find(int x)//遞歸寫法
{
	if(father[x])!=x)	return find(father[x]);
	return x;
}

因此,比較兩個元素x,y是否是同一集合的方法就是比較find(x)是否等於find(y)。

bool judge(int x,int y)
{
    x=find(x);
    y=find(y);
    if(x==y)
        return true;
    else
        return false;
}

合併集合的方法就是將其中一個點所在集合的根結點的父結點設定爲另一個點所在集合的根結點。

void union1(int x,int y)//union是關鍵字,不能用,函數名可以隨便換一個方便的
{
    x=find(x);
    y=find(y);
    father[y]=x;
}

優化:
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;
}
(這個優化平時都可以用,但是對於某些題會造成麻煩,例如加權並查集,這樣的題需要特殊處理)
2.按秩合併(啓發式合併)
在合併兩個集合(就是兩棵樹)的時候,如果待合併的樹的深度不相同,那麼就有兩種選擇:一種是以深度較小的樹的根結點爲新的根結點,另一種是以深度較大的樹的根結點爲新的根結點。而事實上,選擇以深度較大的樹的根結點爲新的根結點較好,因爲這樣的話新生成的樹深度會更小,可以防止樹的退化(退化指越來越接近鏈表,即深度大而分支少),使資源利用更合理。而合併時這樣選擇,就叫做“按秩合併”。
按秩合併的基本思想是將深度較小的樹指到深度較大的樹的根上。
按秩合併需要新開一個數組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.食物鏈
一道變式題
②種類並查集--建立補集法
有的時候,元素之間有一些關係,但是關係可以分爲幾類,要求記錄它們之間有怎樣的關係,這時可以用建立補集法。
如果有n個元素從a[1]到a[n],它們之間的關係有關係1和關係2兩種,那麼可以建立數組fa,fa[i]表示與i關係爲關係1的元素的集合的序號,fa[i+n]表示與i關係爲關係2的元素的集合的序號。當讀入i和j爲關係1時,則合併i、j所在集合還有i+n、j+n所在集合;當讀入i和j爲關係2時,合併i、j+n所在集合還有i+n和j所在集合。
典型例題:
1.團隊
2.還是食物鏈(通過上面兩個變種的描述,可以看出它們的適用範圍有很大重疊)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章