並查集
借鑑百度百科的解釋,並查集就是在一些有N個元素的集合問題中,開始的時候讓每一個元素成爲本身的集合,而後按照必定的順序將屬於同一組的元素所在的集合進行合併(合併的是集合),在合併的期間須要方法查找元素所在的集合。並查集的原理比較簡單,解決的問題的特色是看似並不複雜,但數據量極大。例如:圖的連通子圖問題,一個圖裏面有幾個連通子圖,判斷這幅圖是否連通等。若用正常的數據結構來描述,每每時空複雜度會太高。並查集是一種樹形數據結構,用於處理一些不相交集合的合併和查詢問題。並查集的原理就是朋友的朋友就是個人朋友。
基本
正如名字表達的那樣,並查集須要合併和查詢集合,因此並查集通常須要一個數組和兩個函數:int[] parent, int find(int element)和void unionElements(int firstOne, int secondOne)。
parent數組用來記錄每一個元素的前導點,也就是肯定該元素所在的集合id;find函數用來查找某元素所在的集合id;unionElements用來合併不一樣的集合。其基本過程以下圖所示。
實現代碼:html
public class UnionFind1 { private int[] id; //存儲元素的root private int size; //並查集大小 //構建新的並查集 public UnionFind1(int size) { this.size = size; id = new int[size]; for (int i=0; i<size; i++) id[i] = i; //每一個元素都指向本身 } //查看元素所屬的集合 public int find(int element) { return id[element]; } //判斷是否屬於同一集合 public boolean isConnected(int firElement, int secElement) { return find(firElement) == find(secElement); } //合併兩個集合 public void unionElements(int firElement, int secElement) { int firUnion = find(firElement); int secUnion = find(secElement); if(firUnion != secUnion) { //合併效率低下,合併一次是O(n)的複雜度 for(int i=0; i<this.size; i++) { if(id[i] == firUnion) { id[i] = secUnion; } } } } //打印元素 private void printSet() { for (int id : this.id) System.out.print(id + " "); System.out.println(); } //入口 public static void main(String[] args) { int n = 10; UnionFind1 union = new UnionFind1(n); System.out.println("初始化: "); union.printSet(); System.out.println("鏈接5 6"); union.unionElements(5, 6); System.out.println("鏈接1 2 "); union.unionElements(1, 2); System.out.println("鏈接2 3"); union.unionElements(2, 3); System.out.println("鏈接1 4"); union.unionElements(1, 4); System.out.println("鏈接1 5"); union.unionElements(1, 5); System.out.println("最終結果: "); union.printSet(); System.out.println("1 6 是否鏈接: " + union.isConnected(1, 6)); System.out.println("1 8 是否鏈接: " + union.isConnected(1, 8)); System.out.println("1 4 是否鏈接: " + union.isConnected(1, 4)); } }
Output: 初始化: 0 1 2 3 4 5 6 7 8 9 鏈接5 6 鏈接1 2 鏈接2 3 鏈接1 4 鏈接1 5 最終結果: 0 6 6 6 6 6 6 7 8 9 1 6 是否鏈接: true 1 8 是否鏈接: false 1 4 是否鏈接: true
優化1
上述的並查集合並時候的複雜度是O(n)的,如今對其改進,在合併的時候,當前集合的爸爸去當別人的兒子,當前集合其餘元素的爸爸(前導點)不變,這樣的話,須要改變查找的方式,由於對某個集合來講,最終的爸爸只有一個。例如對於下面這個數組集合:
元素:0 1 2 3
爸爸:1 2 3 3
查找元素0和1的"爸爸",應該是3而不是1和2,因此find的尋找方式應該改變,應該找到最終的爸爸(元素和爸爸是同一個的點)。這種方式合併的複雜度降低了,可是find的複雜度增長了。
實現代碼:數組
public class UnionFind2 { private int[] id; private int size; public UnionFind2(int size) { this.size = size; id = new int[size]; for (int i=0; i<size; i++) id[i] = i; } //找到最終的root public int find(int element) { while(element != id[element]) element = id[element]; return element; } public boolean isConnected(int firElement, int secElement) { return find(firElement) == find(secElement); } public void unionElements(int firElement, int secElement) { int firUion = find(firElement); int secUion = find(secElement); if (firUion == secUion) return; id[firUion] = secUion; } private void printSet() { for (int id : this.id) System.out.print(id + " "); System.out.println(); } public static void main(String[] args) { int n = 10; UnionFind2 union = new UnionFind2(n); System.out.println("初始化: "); union.printSet(); System.out.println("鏈接5 6"); union.unionElements(5, 6); System.out.println("鏈接1 2 "); union.unionElements(1, 2); System.out.println("鏈接2 3"); union.unionElements(2, 3); System.out.println("鏈接1 4"); union.unionElements(1, 4); System.out.println("鏈接1 5"); union.unionElements(1, 5); System.out.println("最終結果: "); union.printSet(); System.out.println("1 6 是否鏈接: " + union.isConnected(1, 6)); System.out.println("1 8 是否鏈接: " + union.isConnected(1, 8)); System.out.println("1 4 是否鏈接: " + union.isConnected(1, 4)); } }
Output: 初始化: 0 1 2 3 4 5 6 7 8 9 鏈接5 6 鏈接1 2 鏈接2 3 鏈接1 4 鏈接1 5 最終結果: 0 2 3 4 6 6 6 7 8 9 1 6 是否鏈接: true 1 8 是否鏈接: false 1 4 是否鏈接: true
優化2
優化1的修改方案,是犧牲查詢函數的複雜度來換取合併函數複雜度的降低,而且還有引入一個新的問題,就是合併後的數組極可能會達到線性鏈表的狀態,例如:
元素:0 1 2 3 4 5 6 7 8 9
爸爸:1 2 3 4 5 6 7 8 9 9
這樣畫出來的連通子圖是一條鏈表來的,這種緣由是合併方式不合理形成的。因此第二種優化的方式可從合併方式下手,引入一個權重來衡量到底誰應該當爸爸。有兩種權重可供選擇,一種是重量,一種是高度。
重量(數目):就是集合爸爸底下有多少數目的子孫;高度(代,箭頭數):就是集合爸爸底下有多少代子孫。
基於重量,誰重誰當爸爸。
實現代碼:數據結構
public class UnionFind3 { private int[] id; private int[] weight; private int size; public UnionFind3(int size) { this.size = size; this.id = new int[size]; this.weight = new int[size]; for (int i=0; i<size; i++) { this.id[i] = i; this.weight[i] = 1; //初始化爲1個元素 } } public int find(int element) { while(element != id[element]){ element = id[element]; } return element; } public boolean isConnected(int firElement, int secElement) { return find(firElement) == find(secElement); } public void unionElements(int firElement, int secElement) { int firUion = find(firElement); int secUion = find(secElement); if(firUion == secUion) return; //減小find的查找時間,誰重誰是爸爸 if(weight[firUion] > weight[secUion]) { id[secUion] = firUion; weight[firUion] += weight[secUion]; } else { id[firUion] = secUion; weight[secUion] += weight[firUion]; } } private void printSet() { for (int id : this.id) System.out.print(id + " "); System.out.println(); } public static void main(String[] args) { int n = 10; UnionFind3 union = new UnionFind3(n); System.out.println("初始化: "); union.printSet(); System.out.println("鏈接5 6"); union.unionElements(5, 6); System.out.println("鏈接1 2 "); union.unionElements(1, 2); System.out.println("鏈接2 3"); union.unionElements(2, 3); System.out.println("鏈接1 4"); union.unionElements(1, 4); System.out.println("鏈接1 5"); union.unionElements(1, 5); System.out.println("最終結果: "); union.printSet(); System.out.println("1 6 是否鏈接: " + union.isConnected(1, 6)); System.out.println("1 8 是否鏈接: " + union.isConnected(1, 8)); System.out.println("1 4 是否鏈接: " + union.isConnected(1, 4)); } }
Output: 初始化: 0 1 2 3 4 5 6 7 8 9 鏈接5 6 鏈接1 2 鏈接2 3 鏈接1 4 鏈接1 5 最終結果: 0 2 2 2 2 6 2 7 8 9 1 6 是否鏈接: true 1 8 是否鏈接: false 1 4 是否鏈接: true
基於高度,誰子孫代數多誰當爸爸。
實現代碼:函數
public class UnionFind4 { private int[] id; private int[] height; private int size; public UnionFind4(int size) { this.size = size; this.id = new int[size]; this.height = new int[size]; for (int i=0; i<size; i++) { id[i] = i; height[i] = 1; } } public int find(int element) { while(element != id[element]) element = id[element]; return element; } public boolean isConnected(int fir, int sec) { return find(fir) == find(sec); } public void unionElements(int fir, int sec) { int firUion = find(fir); int secUion = find(sec); if(firUion == secUion) return; //使用高度決定誰被誰插入,減小find的時間 if(height[firUion] > height[secUion]) id[secUion] = firUion; else if(height[firUion] < height[secUion]) id[firUion] = secUion; else { id[firUion] = secUion; height[secUion]++; } } private void printSet() { for (int id : this.id) System.out.print(id + " "); System.out.println(); } public static void main(String[] args) { int n = 10; UnionFind4 union = new UnionFind4(n); System.out.println("初始化: "); union.printSet(); System.out.println("鏈接5 6"); union.unionElements(5, 6); System.out.println("鏈接1 2 "); union.unionElements(1, 2); System.out.println("鏈接2 3"); union.unionElements(2, 3); System.out.println("鏈接1 4"); union.unionElements(1, 4); System.out.println("鏈接1 5"); union.unionElements(1, 5); System.out.println("最終結果: "); union.printSet(); System.out.println("1 6 是否鏈接: " + union.isConnected(1, 6)); System.out.println("1 8 是否鏈接: " + union.isConnected(1, 8)); System.out.println("1 4 是否鏈接: " + union.isConnected(1, 4)); } }
Output: 初始化: 0 1 2 3 4 5 6 7 8 9 鏈接5 6 鏈接1 2 鏈接2 3 鏈接1 4 鏈接1 5 最終結果: 0 2 6 2 2 6 6 7 8 9 1 6 是否鏈接: true 1 8 是否鏈接: false 1 4 是否鏈接: true
優化3
無論是基於重量仍是高度,並查集仍是有可能會出現深的節點,這時候能夠進行干預,進行路徑壓縮,具體的方法就是在每一步的查詢操做進行壓縮,讓當前元素的"爸爸"升級成"爺爺",這種路徑壓縮只能在基於重量的並查集實現,由於在壓縮的時候,當前集合的重量是不會變的,可是高度頗有可能改變。
實現代碼:優化
public class UnionFind5 { private int[] id; private int[] weight; private int size; public UnionFind5(int size) { this.size = size; this.id = new int[size]; this.weight = new int[size]; for (int i=0; i<size; i++) { id[i] = i; weight[i] = 1; } } //路徑壓縮,指向本身爸爸的爸爸,進一步壓縮 public int find(int element) { while(element != id[element]) { id[element] = id[id[element]]; element = id[element]; } return element; } public boolean isConnected(int fir, int sec) { return find(fir) == find(sec); } public void unionElements(int fir, int sec) { int firUion = find(fir); int secUion = find(sec); if (firUion == secUion) return; if (weight[firUion] > weight[secUion]) { id[secUion] = firUion; weight[firUion] += weight[secUion]; } else { id[firUion] = secUion; weight[secUion] += weight[firUion]; } } private void printSet() { for (int id : this.id) System.out.print(id + " "); System.out.println(); } public static void main(String[] args) { int n = 10; UnionFind5 union = new UnionFind5(n); System.out.println("初始化: "); union.printSet(); System.out.println("鏈接5 6"); union.unionElements(5, 6); System.out.println("鏈接1 2 "); union.unionElements(1, 2); System.out.println("鏈接2 3"); union.unionElements(2, 3); System.out.println("鏈接1 4"); union.unionElements(1, 4); System.out.println("鏈接1 5"); union.unionElements(1, 5); System.out.println("最終結果: "); union.printSet(); System.out.println("1 6 是否鏈接: " + union.isConnected(1, 6)); System.out.println("1 8 是否鏈接: " + union.isConnected(1, 8)); System.out.println("1 4 是否鏈接: " + union.isConnected(1, 4)); } }
Output: 初始化: 0 1 2 3 4 5 6 7 8 9 鏈接5 6 鏈接1 2 鏈接2 3 鏈接1 4 鏈接1 5 最終結果: 0 2 2 2 2 6 2 7 8 9 1 6 是否鏈接: true 1 8 是否鏈接: false 1 4 是否鏈接: true
這篇博客主要是方便本身往後複習和查看使用,主要參考了下面這篇博客:
https://www.cnblogs.com/noKing/p/8018609.html
下面還有一篇關於並查集的概念解釋得不錯的概念分享給你們:
https://www.cnblogs.com/-new/p/6662301.html
謝謝你們,這是本人的第一篇技術博客,歡迎你們批評指正,轉載註明出處就行。this