前言:java
很多搞IT的朋友聽到「算法」時老是以爲它太難,過高大上了。今天,跟大夥兒分享一個比較俗氣,可是卻很是高效實用的算法,如標題所示Union-Find,是研究關於動態連通性的問題。不保證我能清晰的表述並解釋這個算法,也不保證你能夠領會這個算法的絕妙之處。可是,只要跟着思路一步一步來,相信你必定能夠理解它,並像我同樣享受它。算法
-----------------------------------------數組
爲了便於引入算法,下面咱們假設一個場景:網絡
假設如今有A,B兩人素不相識,但A經過熟人甲,甲經過熟人乙,乙經過熟人丙,丙經過熟人丁,而丁又恰好與B是熟人。就這樣,A經過一層一層的人際關係最後認識了B。函數
基於以上介紹的「關係網」,如今給出一道思考題:13億中國人當中一共有幾個「關係網」呢?性能
------------------------------------------測試
1.Union-Find初探優化
是的,想到1,300,000,000這個數字,或許此刻你大腦已經懵了。那好,咱們就先從小數據分析:ui
圖1spa
從上圖中,其實很好理解。初始每一個人都是單獨的一個「點」,用科學語言,咱們把它描述爲「連通份量」。隨着一個一個關係的確立,即點與點之間的鏈接,每鏈接一次,總連通份量數即減1(理解算法的關鍵點之一)。最後的「關係網」幾乎能夠很輕易地數出來。因此,只要你把全部國人兩兩之間的聯繫給出,而後不斷連線,連線,...,最後再統計一下不就完事兒了麼~
問題是:怎麼存儲點的信息?點與點怎麼連,怎麼判斷該不應連?
所以,咱們須要維護2個變量,其中一個變量count表示實時的連通份量數,另外一個變量能夠用來存儲具體每個點所屬的連通份量。由於不須要存儲複雜的信息。這裏咱們選經常使用的數組 id[N] 存儲便可。而後,咱們須要2個函數find(int x)和union(int p,int q)。前者返回點「x」所屬於的連通份量,後者將p,q兩點進行鏈接。注意,所謂的鏈接,其實能夠簡單的將p的連通份量值賦予q或者將q的連通份量值賦予p,即:
id[p]=q 或者id[q]=p。
有了上面的分析,咱們就能夠牛刀小試了。且看Java代碼實現初版。
Code:
1 package com.gdufe.unionfind; 2 3 import java.io.File; 4 import java.util.Scanner; 5 6 public class UF { 7 8 int count; //連通份量數 9 int[] id; //每一個數所屬的連通份量 10 11 public UF(int N) { //初始化時,N個點有N個份量 12 count = N; 13 id = new int[N]; 14 for (int i = 0; i < N; i++) 15 id[i] = i; 16 } 17 //返回連通份量數 18 public int getCount(){ 19 return count; 20 } 21 //查找x所屬的連通份量 22 public int find(int x){ 23 return id[x]; 24 } 25 //鏈接p,q(將q的份量改成p所在的份量) 26 public void union(int p,int q){ 27 int pID=find(p); 28 int qID=find(q); 29 for(int i=0;i<id.length;i++){ 30 if(find(i)==pID){ 31 id[i]=qID; 32 } 33 } 34 count--; //記得每進行一次鏈接,份量數減「1」 35 } 36 //判斷p,q是否鏈接,便是否屬於同一個份量 37 public boolean connected(int p,int q){ 38 return find(p)==find(q); 39 } 40 41 public static void main(String[] args) throws Exception { 42 43 //數據從外部文件讀入,「data.txt」放在項目的根目錄下 44 Scanner input = new Scanner(new File("data.txt")); 45 int N=input.nextInt(); 46 UF uf = new UF(N); 47 while(input.hasNext()){ 48 int p=input.nextInt(); 49 int q=input.nextInt(); 50 if(uf.connected(p, q)) continue; //若p,q已屬於同一連通份量再也不鏈接,則故直接跳過 51 uf.union(p, q); 52 System.out.println(p+"-"+q); 53 54 } 55 System.out.println("總連通份量數:"+uf.getCount()); 56 } 57 58 }
測試結果:
2-3
1-0
0-4
5-7
總連通份量數:4
分析:
find()操做的時間複雜度爲:O(l),Union的時間複雜度爲:O(N)。由於算法能夠很是高效地實現find(),因此咱們也把它稱爲「quick-find」算法。
--------------------
2.Union-find進階:
仔細一想,咱們上面再進行union()鏈接操做時,實際上就是一個進行暴力「標記」的過程,即把全部連通份量id跟點q相同的點找出來,而後所有換成p的id。算法自己沒有錯,可是這樣的代價過高了,得想辦法優化~
所以,這裏引入了一個抽象的「樹」結構,即初始時每一個點都是一棵獨立的樹,全部的點構成了一個大森林。每一次鏈接,實際上就是兩棵樹的合併。經過,不斷的合併,合併,再合併最後長成了一棵棵的大樹。
圖2
Code:
1 package com.gdufe.unionfind; 2 3 import java.io.File; 4 import java.util.Scanner; 5 6 public class UF { 7 8 int count; //連通份量數 9 int[] id; //每一個數所屬的連通份量 10 11 public UF(int N) { //初始化時,N個點有N個份量 12 count = N; 13 id = new int[N]; 14 for (int i = 0; i < N; i++) 15 id[i] = i; 16 } 17 //返回連通份量數 18 public int getCount(){ 19 return count; 20 } 21 22 //查找x所屬的連通份量 23 public int find(int x){ 24 while(x!=id[x]) x = id[x]; //若找不到,則一直往根root回溯 25 return x; 26 } 27 //鏈接p,q(將q的份量改成p所在的份量) 28 public void union(int p,int q){ 29 int pID=find(p); 30 int qID=find(q); 31 if(pID==qID) return ; 32 id[q]=pID; 33 count--; 34 } 35 /* 36 //查找x所屬的連通份量 37 public int find(int x){ 38 return id[x]; 39 } 40 41 //鏈接p,q(將q的份量改成p所在的份量) 42 public void union(int p,int q){ 43 int pID=find(p); 44 int qID=find(q); 45 if(pID==qID) return ; 46 for(int i=0;i<id.length;i++){ 47 if(find(i)==pID){ 48 id[i]=qID; 49 } 50 } 51 count--; //記得每進行一次鏈接,份量數減「1」 52 } 53 */ 54 //判斷p,q是否鏈接,便是否屬於同一個份量 55 public boolean connected(int p,int q){ 56 return find(p)==find(q); 57 } 58 59 public static void main(String[] args) throws Exception { 60 61 //數據從外部文件讀入,「data.txt」放在項目的根目錄下 62 Scanner input = new Scanner(new File("data.txt")); 63 int N=input.nextInt(); 64 UF uf = new UF(N); 65 while(input.hasNext()){ 66 int p=input.nextInt(); 67 int q=input.nextInt(); 68 if(uf.connected(p, q)) continue; //若p,q已屬於同一連通份量再也不鏈接,則故直接跳過 69 uf.union(p, q); 70 System.out.println(p+"-"+q); 71 72 } 73 System.out.println("總連通份量數:"+uf.getCount()); 74 } 75 76 }
測試結果:
2-3
1-0
0-4
5-7
總連通份量數:4
分析:
利用樹自己良好的連通性,咱們算法僅須要O(l)時間代價進行union()操做,但此時find()操做的時間代價有所增長。結合本算法對quick-find()的優化,咱們把它稱爲「quick-union」算法。
--------
3.Union-Find再進階
等等,還沒完!
表面上,上述引入「樹」結構的算法時間複雜度由原來的O(N)改進爲O(lgN)。可是,不要忽略了這樣一種極端狀況,即每鏈接一個點以後,樹在不斷往下生長,最後長成一棵「禿樹」(沒有任何樹枝)。
圖3
爲了避免讓咱們前面作的工做白費,必須得采起某些措施避免這種惡劣的狀況給咱們算法帶來的巨大代價。因此...
是的,或許你已經想到了,就是在兩棵樹進行鏈接以前作一個判斷。每一次都優先選擇將小樹合併到大樹下面,這樣子樹的高度不變,能避免樹一直往下增加了!下圖中,數據增長了「6-2」的一條鏈接,得知以「2」爲根節點的樹比「6」的樹大,對比(f)和(g)兩種鏈接方式,咱們最優選擇應該是(g),即把小樹併到大樹下。
圖4
基於此,咱們還得引入一個變量對以每一個結點爲根節點的樹的大小進行維護,具體咱們以sz[i]表示i結點表明的樹(或子樹)的結點數做爲它的大小,初始sz[i]=1。由於如今的每個結點都有了權重,因此咱們也把這種樹結構稱爲「加權樹」,本算法稱爲「weightedUnionFind」。
Code:
1 package com.gdufe.unionfind; 2 3 import java.io.File; 4 import java.util.Scanner; 5 6 public class UF { 7 8 int count; // 連通份量數 9 int[] id; // 每一個數所屬的連通份量 10 int[] sz; 11 12 public UF(int N) { // 初始化時,N個點有N個份量 13 count = N; 14 sz = new int[N]; 15 id = new int[N]; 16 for (int i = 0; i < N; i++) 17 id[i] = i; 18 19 for (int i = 0; i < N; i++) 20 sz[i] = 1; 21 22 } 23 24 // 返回連通份量數 25 public int getCount() { 26 return count; 27 } 28 29 // 查找x所屬的連通份量 30 public int find(int x) { 31 while (x != id[x]) 32 x = id[x]; // 若找不到,則一直往根root回溯 33 return x; 34 } 35 36 // 鏈接p,q(將q的份量改成p所在的份量) 37 public void union(int p, int q) { 38 int pID = find(p); 39 int qID = find(q); 40 if (pID == qID) 41 return; 42 43 if (sz[p] < sz[q]) { //經過結點數量,判斷樹的大小並將小樹併到大樹下 44 id[p] = qID; 45 sz[q] += sz[p]; 46 } else { 47 id[q] = pID; 48 sz[p] += sz[q]; 49 } 50 count--; 51 } 52 53 /* 54 * //查找x所屬的連通份量 public int find(int x){ return id[x]; } 55 * 56 * //鏈接p,q(將q的份量改成p所在的份量) public void union(int p,int q){ int pID=find(p); 57 * int qID=find(q); if(pID==qID) return ; for(int i=0;i<id.length;i++){ 58 * if(find(i)==pID){ id[i]=qID; } } count--; //記得每進行一次鏈接,份量數減「1」 } 59 */ 60 // 判斷p,q是否鏈接,便是否屬於同一個份量 61 public boolean connected(int p, int q) { 62 return find(p) == find(q); 63 } 64 65 public static void main(String[] args) throws Exception { 66 67 // 數據從外部文件讀入,「data.txt」放在項目的根目錄下 68 Scanner input = new Scanner(new File("data.txt")); 69 int N = input.nextInt(); 70 UF uf = new UF(N); 71 while (input.hasNext()) { 72 int p = input.nextInt(); 73 int q = input.nextInt(); 74 if (uf.connected(p, q)) 75 continue; // 若p,q已屬於同一連通份量再也不鏈接,則故直接跳過 76 uf.union(p, q); 77 System.out.println(p + "-" + q); 78 79 } 80 System.out.println("總連通份量數:" + uf.getCount()); 81 } 82 83 }
測試結果:
2-3
1-0
0-4
5-7
6-2
總連通份量數:3
4.算法性能比較:
|
讀入數據 |
find() |
union() |
總時間複雜度 |
quick-find |
O(M) |
O(l) |
O(N) |
O(M*N) |
quick-union |
O(M) |
O(lgN~N) |
O(l) |
O(M*N)極端 |
WeightedUF |
O(M) |
O(lgN) |
O(N) |
O(M*lgN) |
----------------------
結語:
讀到了最後,有朋友可能以爲「不就是一個O(N)到O(lgN)的轉變嗎,有必要這麼長篇大論麼」?對此,本人就只有無語了。有過算法複雜度分析的朋友應該知道算法由O(N)到O(lgN)所帶來的增加效益是多麼巨大。雖然,前文中13億的數據,就算咱們用最後的加權樹算法一時半會兒也沒法算出。但假如如今一樣是100w的數據,那麼咱們最後的「加權樹」由於總體的時間複雜度:O(M*lgN)能夠在1秒左右跑完,而O(M*N)的算法可能得花費1千倍以上的時間,至少1小時內還沒算出來(固然啦,也可能你機器的是高性能的~)。
最後的最後,羅列本人目前所知曉的本算法適用的幾個領域:
l 網絡通訊(好比:是否須要在通訊點p,q創建通訊鏈接)
l 媒體社交(好比:向通一個社交圈的朋友推薦商品)
l 數學集合(好比:判斷元素p,q以後選擇是否進行集合合併)