並查集,在一些有N個元素的集合應用問題中,我們通常是在開始時讓每個元素構成一個單元素的集合,然后按一定順序將屬於同一組的元素所在的集合合並,其間要反復查找一個元素在哪個集合中。這一類問題近幾年來反復出現在信息學的國際國內賽題中,其特點是看似並不復雜,但數據量極大,若用正常的數據結構來描述的話,往往在空間上過大,計算機無法承受;即使在空間上勉強通過,運行的時間復雜度也極高,根本就不可能在比賽規定的運行時間(1~3秒)內計算出試題需要的結果,只能用並查集來描述。
首先我們給出並查集(union-find)的api:
在《algorithm》書中,對與uf的應用直接給出了代碼,但是其中find和union方法並沒有直接實現,而是空着的。書中代碼如下:
注意:這里看見有一個StdIn的類作為類似於輸入流的作用,這個在java中是不存在的,這是這本書自己構造的一個類。相信以后在這本書中也會有類似的部分,可以在網上找到其中的具體實施代碼,我會發送出來的。
為什么不直接實現find和union方法呢,這里會考慮到數據結構和算法復雜度的問題。
我們將其分為:quickunion,quickfind,weightedquickunion,weightedquickunion with path compression;
1)Quick-Find
其中判斷p和q兩個元素是否連接便是讓兩個元素的id[]相同。
此時,find()便是直接返回id[p]的值
而union()便是將連接的兩個集合的id[]變為一致:當然,我們首先要判斷她們是不是已經連接;
由上圖便可看出,我們union(3,4),得到兩者的id[]相同,即id[3]=id[4]=3(id值為連接的集合包含的任意一個數);
得到的代碼如下:
package unionFind; public class QuickFindUF { private int id[]; private int count; public QuickFindUF(int N){ count=N; id=new int[N]; for(int i=0;i<N;i++){ id[i]=i; } } public int count(){ return count; } public boolean connected(int p,int q){ return find(p)==find(q); } public int find(int p){ return id[p]; } public void union(int p,int q){ int pid=find(p); int qid=find(q); if(pid==qid)return; for(int i=0;i<id.length;i++){ if(id[i]==pid)id[i]=qid; } count--; } }
2)Quick-Union
Quick-Union的結構如下圖所示:
即每一個結點的id為上一個結點。而根節點便是root=id[root];
判斷兩個元素是否連接則是判斷兩個元素的根(root)是否相同。
find()則為找到元素的根節點;
union(p,q)即將p的根節點與q的根節點連接。
代碼實現如下:
package unionFind; public class QuickUnionUF { private int id[]; private int count; public QuickUnionUF(int N){ id=new int[N]; count=N; for(int i=0;i<N;i++){ id[i]=i; } } public int count(){ return count; } public int find(int i){ while(i!=id[i]){i=id[i];} return i; } public void union(int p,int q){ if(find(p)==find(q))return; id[find(p)]=find(q); count--; } public boolean connected(int p,int q){ return find(p)==find(q); } }
3)Weighted Quick-Union
當然,我們如果胡亂的將根節點相互連接,會導致這個樹的結構非常糟糕,比如:
我們可以看到這個樹的結構非常非常糟糕。
為了避免這個情況,我們記錄樹的大小,並且總是將小的樹連接到大的樹:
使用這種方法可以很大程度的優化樹的結構,例如上圖的樹我們可以變為:
具體實現代碼如下:
package unionFind; public class WeightedQuickUnionUF { private int id[]; private int count; private int sz[]; public WeightedQuickUnionUF(int N){ count=N; id=new int[N]; sz=new int[N]; for(int i=0;i<N;i++){ id[i]=i; sz[i]=1; } } public int find(int p){ while(p!=id[p])p=id[p]; return p; } public void union(int p,int q){ int pid=find(p); int qid=find(q); if(qid==pid)return ; if(sz[pid]<sz[qid]){ id[pid]=qid; sz[qid]+=sz[pid]; } else{ id[qid]=pid; sz[pid]+=sz[qid]; } count--; } public int count(){ return count; } public boolean connected(int p,int q){ return find(p)==find(q); } }
4)Weighted Quick-Union with Path Compression
最優情況下,我們希望所有的節點都直接連接到根節點上,但是又不希望像QuickUnion那樣大量修改連接,這時,我們可以在檢查節點的同時將它與根節點直接連接。
例如,我們對下列並查集進行union(7,3);
在采取最優算法下,結果如下:
可以看出,我們將遍歷到的節點都直接與根節點直接連接,這一切只需要在find內的循環進行修改就可以實現。
具體的代碼如下:
package unionFind; public class WeightedQuickUnionUFWPC { private int id[]; private int count; private int sz[]; public WeightedQuickUnionUFWPC(int N){ count=N; id=new int[N]; sz=new int[N]; for(int i=0;i<N;i++){ id[i]=i; sz[i]=1; } } public int find(int p){ int root=p; while(root!=id[root])root=id[root]; while(p!=root){ int x=p; id[x]=root; p=id[p]; } return root; } public void union(int p,int q){ int pid=find(p); int qid=find(q); if(qid==pid)return ; if(sz[pid]<sz[qid]){ id[pid]=qid; sz[qid]+=sz[pid]; } else{ id[qid]=pid; sz[pid]+=sz[qid]; } count--; } public int count(){ return count; } public boolean connected(int p,int q){ return find(p)==find(q); } }
這四種方法能夠適應不同的情況,但是對於算法復雜度來說,這四種方法就會有很大的差別:
對於每一項的得出,《algorithm》給出了很詳細的解釋,我希望自己能夠有時間寫一篇文章來細講一下。(別說了。感覺還有好多坑沒填)