【Algorithms公開課學習筆記1】 Union-Find 合併查詢
Union-Find 合併查詢
動態連線性
判斷連線性的關鍵
等價關係模型:如果有(a,b),(b,c),那麼也會有(a,c)。其中()表示有連線。
連通分量:最大的可連通物件集合,有兩個特點:1)連通分量內部任意兩個物件都是相連通的;2)連通分量內部的物件不與外部物件相連通。
利用連通分量,可以方便地實現並查集的兩個操作:查詢請求
和合併命令
查詢:檢查兩個物件是否在相同的連通分量中
合併:將兩個物件的分享替換成其並集
快速查詢
快速查詢是基於貪心策略的一種演算法,貪心策略是指在問題求解的時候,只找出當前最優解。
基於此,設計一種資料結構來儲存實驗物件。
長度為N的整型陣列
如果p和q有相同的id,則表示他們有連線
因此,查詢和合並操作就變為:
查詢:檢查p和q是否具有相同的id,若相同則代表連通。如圖id[1]=id[2],表示1和2相連通
合併:在合併p和q物件時,將值為所有等於id[p]的值重新賦值為id[q]。如圖合併0和1,需要將id[0]=id[5]=id[6]=0的值全部賦值為id[1]=1的值
具體演算法
public class QuickFind {
private int[] id;
/**
* 建構函式,初始化資料結構的屬性
*
* @param N
* 陣列額長度
*/
public QuickFind(int N) {
// 初始化資料結構的值
id = new int[N];
for (int i = 0; i < N; i++) {
id[i] = i;
}
}
/**
* 查詢操作,判斷連通性
*
* @param p
* @param q
* @return
*/
public boolean connected(int p, int q) {
return id[p] == id[q];
}
/**
* 合併操作
*
* @param p
* @param q
*/
public void union(int p, int q) {
int pid = id[p];
int qid = id[q];
for (int i = 0; i < id.length; i++) {
if (pid == id[i]) {
id[i] = qid;
}
}
}
/* setter和getter */
public int[] getId() {
return id;
}
public void setId(int[] id) {
this.id = id;
}
}
時間複雜度分析
演算法 | 初始化 | 合併(包含查詢) | 查詢 |
---|---|---|---|
快速查詢 | N | N | 1 |
快速合併
快速合併是基於懶策略的一種演算法,懶策略是指在問題求解時儘量避免計算,直到不得不進行計算。
基於此,設計另外一種該資料結構來儲存實驗物件。
長度為N的整型陣列
id[i]是i的父親,從而構造成樹的結構
如果i=id[i],則表示id[i]是樹根
因此,合併和查詢操作就變為:
查詢:檢查p和q是否具有相同的根,如有則代表連通。
合併:在合併p和q物件時,將p的根的id(父親)設成q的根。
具體演算法
public class QuickUnion {
public class QU {
private int[] id;
public QU(int N) {
// 初始化屬性值
id = new int[N];
for (int i = 0; i < N; i++) {
id[i] = i;
}
}
/**
* 查詢指定值的根
*
* @param i
* 待查詢根的值
* @return
*/
public int root(int i) {
// 當id[i]=i時就是根
while (id[i] != i)
i = id[i];
return i;
}
/**
* 查詢連通性
*
* @param p
* @param q
* @return
*/
public boolean connected(int p, int q) {
return root(p) == root(q);
}
/**
* 合併操作,將p的根的id(父親)設成q的根
*
* @param p
* @param q
*/
public void union(int p, int q) {
int proot = root(p);
int qroot = root(q);
id[proot] = qroot;
}
/** getter和setter */
public int[] getId() {
return id;
}
public void setId(int[] id) {
this.id = id;
}
}
時間複雜度分析
查詢:取決於物件p和q的深度(而樹的深度有可能為N,線性級別)
合併:常數級別,包含查詢時就是查詢的時間複雜度
演算法 | 初始化 | 合併(包含查詢) | 查詢 |
---|---|---|---|
快速查詢 | N | N | 1 |
快速合併 | N | N | N |
帶權的快速合併
上面所描述的快速合併中,存在兩個缺點是:一是查詢時間消耗過大,二是樹的深度容易過大
針對上述缺點,引入帶權的快速合併
演算法。該演算法的特點:
改進快速合併,避免生成過高的樹
追蹤每個樹的大小(物件的個數)
在合併的時候,通過“將小樹連線到大樹的根”來達到平衡樹高的效果
資料結構與“快速合併”相同,合併和查詢操作為:
查詢:檢查p和q是否具有相同的根,如有則代表連通(與快速合併相同)。
合併:在合併p和q物件時,將小樹的根的id設為大樹的根(小樹接入大樹)。故需要額外維護資料size array來儲存樹的大小。
程式碼:修改union部分
/**
* 合併操作,將小樹的根的id(父親)設成大樹的根
*
* @param p
* @param q
*/
public void union(int p, int q) {
int proot = root(p);
int qroot = root(q);
if(size[p]>size[q]){
id[qroot] = proot;
size[proot]+=size[qroot];
}else{
id[proot] = qroot;
size[qroot]+=size[proot];
}
}
時間複雜度分析
查詢:取決於物件p和q的深度(而樹的深度最多為lgN)
合併:常數級別,包含查詢時就是查詢的時間複雜度
演算法 | 初始化 | 合併(包含查詢) | 查詢 |
---|---|---|---|
快速查詢 | N | N | 1 |
快速合併 | N | N | N |
帶權快速合併 | N | lgN | lgN |
帶壓縮路徑的快速合併
在做合併和查詢找之前,將樹的路徑進行壓縮,從而保持樹的扁平化。
程式碼實現
public int root(int i) {
// 當id[i]=i時就是根
while (id[i] != i){
//將i的根指向其爺爺輩
id[i]=id[id[i]];
i = id[i];
}
return i;
}
時間對複雜度比
對於在N個物件中有M個並查集的情況
演算法 | 時間 |
---|---|
快速查詢 | MN |
快速合併 | MN |
帶權快速合併 | N+MlogN |
帶壓縮路徑快速合併 | N+MlogN |