並查集入門及例題分析
一、並查集的原理
並查集(Union-Find)是一種樹型的資料結構,用於處理一些不相交集合的合併及查詢問題。
主要涉及兩種操作:合併和查詢。
具體地說,初始狀態下,並查集中的元素是互不相交的,經過一系列操作(Union)後,合併成一個集合。
而在進行了某次合併之後,如果想知道:某兩個元素是否已經處在同一個集合中了?這時候就需要查詢(Find)操作。
一張江湖解說圖如下:
簡單來講,這個過程可以理解為找祖宗和認祖宗的故事。如果此時你想找到楊左使的管理者,就可以用並查集結構,如果你想讓宋遠橋歸屬到張無忌門派,就可以使用到並查集結構。
那並查集的初始與結構是怎麼樣的呢?
二、並查集的資料結構及實現方式
1、構建並初始化並查集
初始化並查集結構,用上面圖來說,就是先初始有多少個江湖人士,然後預設他們都是各自一派的管理員。程式碼如下:
class findUnionCollection{ private int[] s; // 江湖人數的陣列集 int count; // 門派個數 // 構建江湖人士的陣列集和門派個數 public findUnionCollection(int length){ s = new int[length]; s = initCollection(s); count = length; } // 將每個江湖人士設定為各自一派的管理者(根節點預設為-1) private int[] initCollection(int[] target){ for (int i =0;i<target.length;i++){ target[i]=-1; } return target; } }
有了各個門派的一個集合,此時我們應該也要有個可以找到每個門派的管理者一個查詢方法。
程式碼講解:
public int find(int x){
// 當這個成員的所對應的值小於0,即為-1,則為門派的管理員,直接返回
if(s[x]<0){
return x;
}
// 如果不為0,則這個成員所對應的值是他的直接上層大佬
// 此時可以去遞迴這個大佬的大佬,直到找到最終的管理員
return find(s[x]);
}
當然,這裡有個優化點,就是我們這邊只是關心這個門派的管理員,而不關心這個門派的各個層次的關係。所以我們在查詢的時候,可以將這層直接指引到管理員,方便下次直接查詢,術語簡稱路徑壓縮。
public int find(int x){
// 當這個成員的所對應的值小於0,即為-1,則為門派的管理員,直接返回
if(s[x]<0){
return x;
}
// 如果不為0,則這個成員所對應的值是他的直接上層大佬
// 此時可以去遞迴這個大佬的大佬,直到找到最終的管理員
// 將各個門派的成員都指向最終管理員(實際就是路徑壓縮)
return s[x] = find(s[x]);
}
有了找祖宗的方法,當然也少不了認祖宗的方法,換江湖的說話,就是投靠另一門派
程式碼解析:
// 合併兩個江湖人士所屬門派
public void unionCollection(int child1,int child2){
// 先判斷兩個江湖人士是否所屬一派,如果是,直接返回,無須合併
if(find(child1)==find(child2)){
return;
}
// 之後我們這個成員所屬門派的管理員的深度進行對比
// 如果兩個管理者的深度一樣,就隨機選一個管理員作為最終管理員,將加深其深度(減1)
// 如果child1的深度比child2的深度深,則將child2的管理員指向child1管理員
if(s[find(child1)]<s[find(child2)]){
s[find(child2)]=find(child1);
}else {
if(s[find(child1)]==s[find(child2)]){
s[find(child2)]--;
}
s[find(child1)]=find(child2);
}
由於每一次合併,都會減少一個門派,所以門派的數量也就遞減
count--;
}
三、例題實戰
1、題目描述
班上有 N 名學生。其中有些人是朋友,有些則不是。他們的友誼具有是傳遞性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那麼我們可以認為 A 也是 C 的朋友。所謂的朋友圈,是指所有朋友的集合。
給定一個 N * N 的矩陣 M,表示班級中學生之間的朋友關係。如果Mi = 1,表示已知第 i 個和 j 個學生互為朋友關係,否則為不知道。你必須輸出所有學生中的已知的朋友圈總數。
示例 1:
輸入:
[[1,1,0],
[1,1,0],
[0,0,1]]
輸出: 2
說明:已知學生0和學生1互為朋友,他們在一個朋友圈。
第2個學生自己在一個朋友圈。所以返回2。
示例 2:
輸入:
[[1,1,0],
[1,1,1],
[0,1,1]]
輸出: 1
說明:已知學生0和學生1互為朋友,學生1和學生2互為朋友,所以學生0和學生2也是朋友,所以他們三個在一個朋友圈,返回1。
注意:
N 在[1,200]的範圍內。
對於所有學生,有Mi = 1。
如果有Mi = 1,則有Mj = 1。
2、解題思路
1)先構建並初始一個長度為N的並查集陣列,此時朋友圈的數量初始的長度;
2)遍歷M二維陣列,判斷每個學生之間的關係是否為1,為1則調合並操作將兩者關聯起來,同時對朋友圈的數量遞減;
3)遍歷完則直接返回朋友圈的數量。
3、程式碼例項:
class findUnionCollection{
private int[] s; // 江湖人數的陣列集
int count; // 門派個數
// 構建江湖人士的陣列集和門派個數
public findUnionCollection(int length){
s = new int[length];
s = initCollection(s);
count = length;
}
// 將每個江湖人士設定為各自一派的管理者(根節點預設為-1)
private int[] initCollection(int[] target){
for (int i =0;i<target.length;i++){
target[i]=-1;
}
return target;
}
public int find(int x){
// 當這個成員的所對應的值小於0,即為-1,則為門派的管理員,直接返回
if(s[x]<0){
return x;
}
// 如果不為0,則這個成員所對應的值是他的直接上層大佬
// 此時可以去遞迴這個大佬的大佬,直到找到最終的管理員
// 將各個門派的成員都指向最終管理員(實際就是路徑壓縮)
return s[x] = find(s[x]);
}
}
public void unionCollection(int child1,int child2){
// 先判斷兩個江湖人士是否所屬一派,如果是,直接返回,無須合併
if(find(child1)==find(child2)){
return;
}
// 之後我們這個成員所屬門派的管理員的深度進行對比
// 如果兩個管理者的深度一樣,就隨機選一個管理員作為最終管理員,將加深其深度(減1)
// 如果child1的深度比child2的深度深,則將child2的管理員指向child1管理員
if(s[find(child1)]<s[find(child2)]){
s[find(child2)]=find(child1);
}else {
if(s[find(child1)]==s[find(child2)]){
s[find(child2)]--;
}
s[find(child1)]=find(child2);
}
由於每一次合併,都會減少一個門派,所以門派的數量也就遞減
count--;
}
class Solution {
public Solution(){
}
public static int findCircleNum(int[][] M) {
int studentNums = M[0].length;
findUnionCollection FUC = new findUnionCollection(studentNums);
for(int i=0;i<studentNums-1;i++){
for(int j = i+1; j<M[i].length;j++){
if(M[i][j]==1){
FUC.unionCollection(i,j);
}
}
}
return FUC.getCount();
}
public static void main(String[] var0) {
int[][] M = new int[][]{{1,1,0},{1,1,0},{0,0,1}};
System.out.println("findUnionCollection:"+findCircleNum(M