從并查集Union Find算法談算法解決實際問題的過程
算法
并查集
算法
尋找路徑
從并查集Union Find算法談算法解決實際問題的過程算法解決實際問題的一般步驟
并查集算法概述
實際問題引出
問題建模
算法版本一 QuickFind一、使用數組的值表示連接關系
二、算法的具體實現
三、算法分析
算法版本二 QuickUnion一、在樹組內創建樹表示連接關系
二、算法的具體實現
算法分析
改進版本二 垂直化到扁平化第一步:帶權合并
實現
再進一步:壓縮路徑
四種算法時間復雜度對比
解決實際問題: 鄉村公路暢通工程問題描述
問題解決
代碼說明
算法解決實際問題的一般步驟
Steps to developing a usable algorithm.
問題建模(Model the problem.)
尋找算法解決問題(Find an algorithm to solve it.)
算法的復雜度評判(Fast enough? Fits in memory?)
尋找影響時間或控件復雜的的原因(If not, figure out why.)
尋找問題源頭(Find a way to address the problem.)
迭代上述過程,直到算法達標(Iterate until satisfied.)
并查集算法概述
給定一個包含N個對象的集合,集合中對象可以相互連接,實現如下命令: Union
Union command: 連接兩個對象。 Find | Connect
Query: 檢查兩個對象是否被連接。
[圖片上傳中。。。(1)] 上圖經歷過的操作如下: union(0,1)
union(1,2)
union(3,4)
union(3,8)
union(4,9)
union(0,5)
union(5,6)
union(1,6)
union(2,7)
此時根據此圖:檢查結果如下: True: connected(0,6)
True: connected(0,5)
False: connected(2,3)
False: connected(6,9)
實際問題引出
如下圖,P和Q兩個點之間是否存在一條路徑(P,Q是否連通)?
[圖片上傳中。。。(2)]在具體一點,現在要解決的實際問題有如下幾個:
一張圖片中像素點的連接檢查。
大型網絡中設備之間的連接檢查。查找
社交網絡中的好友關系。
游戲場景中的路徑檢查。
問題建模
復雜問題簡單化 使用整型數組來抽象實際問題。舍棄一些細節,僅僅描述其連接關系。 具體的實現 使用數組的下標了表示問題中的某一個對象,使用對應的值存儲連接參數。 進一步抽象 A: 連接關系 如果對象A和對象B連接,那么Connected(A,B)
返回True
如果對象A和對象B連接,B又和C連接,那么Connected(A,C)
, 返回True
B:連接組件 如果集合{p, q, r, t}
中, p,q,r
連接,t
和其他三個不連接,則有兩個連接組件: {p, q, r}
和 {t}
[圖片上傳中。。。(3)]
問題解決: 創建數組,使用union
方法創建連接組件,使用find/connected
方法檢查對象是否在同一個連接組件中。
算法版本一 QuickFind
通過問題建模,確定了數據結構和基本方法。解決步驟如下:
一、使用數組的值表示連接關系
數據結構 創建Int數組 id[N], 在這個包含N個元素的數組id中,如果id[p] 和 id[q]的值相同,那么p,q是連通的。 查找方法 檢查兩個對象對應的編號下的值是否一樣。 連接方法 合并p到q,修改所有值為id[p]的元素的值為id[q]
[圖片上傳中。。。(4)]
union(6,1)
[圖片上傳中。。。(5)]
二、算法的具體實現
/* Quick Find 數據結構:* 優點:查找操作比較快,只需要執行一次* 缺點:合并操作效率慢,可能需要nn次數組讀取 */public class QuickFindUF{private int[] id;public QuickFindUF(int N){id = new int[N];for (int i = 0; i < N; i++ )id[i] = i;}//查找:檢查是否連通就是比較兩個點對應的值public boolean connected(int p, int q){ return id[p] == id[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 (id[i] == pid) id[i] = qid;}}
三、算法分析
初始化 初始化的時候,算法需要遍歷整個數組,時間負載度為
查找 在查找操作中,僅僅需要訪問數組的兩個元素即可,程序執行效率的時間復雜度為常數
。 合并 合并操作在這一版本的算法中,需要遍歷整個數組,時間復雜度為
最壞的情況 當需要連接
個點的時候,算法需要遍歷數組
次。當前CPU每秒可以執行
條指令;內存內可以存儲
個字節的內容;讀取內存內所有內容則需要1s。此時,擁有
個節點,要執行
次連接,則需要
次操作,需要31年不眠不休的運算。
算法版本二 QuickUnion
一、在樹組內創建樹表示連接關系
數據結構 在數組內創建樹,連接后在同一連接組件的元素, 組成一棵樹。 查找方法 檢查兩個對象的根是否相同。 連接方法 合并兩個節點P , Q,即將Q的Root(根節點)設置為P的根節點。
[圖片上傳中。。。(6)]上圖中,3的根節點可以追溯的9: 3->4->9->9
5的根節點可以追溯到6: 5->6->6
這個時候,合并 3 和 5: union(3,5)
把五的根節點設置給3的根幾點,也就是修改三的根節點9為6.
[圖片上傳中。。。(7)]
二、算法的具體實現
/快速合并優點:連接的時候時間復雜度為N缺點:查找的時候時間復雜度為N(t) */public class QuickUnionUF{private int[] id;public QuickUnionUF(int N){id = new int[N];for (int i = 0; i < N; i++) id[i] = i;}private int root(int i){ //查找樹的根while (i != id[i]) i = id[i];return i;}public boolean connected(int p, int q){ //檢查是否在同一棵樹內return root(p) == root(q);}public void union(int p, int q){ //合并兩棵樹int i = root(p);int j = root(q);id[i] = j;}}
算法分析
初始化 初始化的時候,算法需要遍歷整個數組,時間負載度為
查找 在查找操作中,需要回溯到一棵樹的根節點,程序執行效率的時間復雜度為常數
。 合并 合并操作在這一版本的算法中,需要多次回溯一棵樹的根節點,時間復雜度為
, t為樹的高度。 最壞的情況 按照目前的算法,如果一顆樹過于高,那么尋找root的操作就會浪費大量的時間。這種算法復雜度高于版本1。
改進版本二 垂直化到扁平化
版本二QuickUnion 的缺陷就在于,合并操作很容易一不小心就產生一顆非常高的樹,然后在回溯這棵樹的時候,需要經過大量的節點才能找到root,故此效率低下。
第一步:帶權合并
解決的辦法是降低樹的高度,在合并操作中,如果碰到兩棵樹的合并,將小一點的樹放在大樹的下面,將小樹的根節點作為大樹的根節點的子節點,則可以降低樹的高度。因此我們需要一個額外的數組來記錄每一個樹的尺寸。這些值也就被乘坐權重,改進后的算法被稱作帶權合并。
[圖片上傳中。。。(8)] 上圖中,如果按照Quick-union的方法,很有可能得到一顆很高的樹,原因就在于一顆大樹的根節點被當作了小樹的子節點。 加權之后,進行判斷,確保小樹永遠被放置在大樹內,大大降低了樹的高度。
實現
public class WeightedQuickUnionUF {private int[] id;private int[] sz;//初始化數組 public WeightedQuickUnionUF(int N) {id = new int[N];sz = new int[N];for (int i = 0; i < N; i++){id[i] = i;sz[i] = 1;//初始化每顆樹高度都為1 }}private int root(int i) {while (i != id[i]) i = id[i];return i;}public boolean connected(int p, int q) {return root(p) == root(q);}public void union(int p, int q) {int i = root(p);int j = root(q);if (i == j) return;//比較樹的大小,確定讓小樹追加在大樹內if(sz[i] < sz[j]) { id[i] = j; sz[j] += sz[i]; }else { id[j] = i; sz[i] += sz[j]; }}}
帶權前后樹的高度對比:
[圖片上傳中。。。(9)]
再進一步:壓縮路徑
帶權合并之后樹的高度明顯降低,回溯的代價也就更小了。但是,在回溯的時候我們一就是一個節點一個節點的往上追溯。
[圖片上傳中。。。(10)]
在上圖中要找到3的root: 3->4->9 return 9
因為id[4] = 9 如果利用這個特性,在這個時候壓縮一下追查的路徑:
3->id[4] return 9
這樣一來回溯的成本將大大降低,因此可以對追查root的代碼進一步優化:
private int root(int i){while (i == id[i]){i = id[id[i]];id[i] = i;}}
效果如下: 壓縮素經之前:
[圖片上傳中。。。(11)]壓縮路徑之后:
[圖片上傳中。。。(12)]
四種算法時間復雜度對比
[圖片上傳中。。。(13)]
解決實際問題: 鄉村公路暢通工程
問題描述
某省調查城鎮交通狀況,得到現有城鎮道路統計表,表中列出了每條道路直接連通的城鎮。省政府“暢通工程”的目標是使全省任何兩個城鎮間都可以實現交通(但不一定有直接的道路相連,只要互相間接通過道路可達即可)。現在要求任意給出兩個城鎮,查看其是否連通?
問題解決
通過對問題的建模,發現可以直接套用Union-Find算法即可:
編碼實現: 首先創建一個數組,包含所有鄉鎮并使其對應一個編號,其次使用鄉鎮對應的數組下標創建一個數組 然后使用連接已經連通的鄉鎮,最后進行程序測試。
代碼說明
代碼實現,為了方便輸入,這里使用Python來實現算法:
-- coding:utf-8 --#這里的鄉鎮僅作演示,實際問題中可以考慮從文件中讀取PLACES = ['大王村', '河陽', '蠻荒殿', '流波鎮', '青云山', '萬蝠洞', '鳳凰鎮', '南疆古鎮', '七里侗']#通過地名獲取編號def idof(place):return PLACES.index(place)#檢查輸入的地名def cheked(place):return place in PLACES#算法實現class UnionFind:'''算法的Python實現,實際上邏輯是一樣的'''def init(self, N):self.sd = []self.sz = []for i in range(N):self.sd.append(i)self.sz.append(1)def root(self, i):while self.sd[i] != i:i = self.sd[self.sd[i]]self.sd[i] = ireturn idef connected(self, p, q):return self.root(p) == self.root(q)def union(self, p, q):i = self.root(p)j = self.root(q)if i == j:returnif self.sz[i] < self.sz[j]:self.sd[i] = jself.sz[j] += self.sz[i]else:self.sd[j] = iself.sz[i] += self.sz[j]if name == "main":uf = UnionFind(len(PLACES))#隨便連接幾個點,以便于測試uf.union(idof('大王村'), idof('流波鎮'))uf.union(idof('大王村'), idof('蠻荒殿'))uf.union(idof('流波鎮'), idof('七里侗'))#錄入要查尋的地名place_a = raw_input("請輸入要查詢的第一個地點:")place_b = raw_input("請輸入要查尋的第二個地點:")#輸出結果if cheked(place_a) and cheked(place_b):result = uf.connected(idof(place_a), idof(place_b))print "%s 和 %s 之間是否連通?\t %s" % (place_a, place_b, result)else:print "輸入地名有誤"
程序運行結果:
[圖片上傳中。。。(14)] 連通情況:
[圖片上傳中。。。(15)]
**
greenpointan**
** 退出賬號
當前文檔
** 恢復至上次同步狀態** 刪除文檔** 導出...** 預覽文檔** 分享鏈接
系統
** 設置
** 使用說明
** 快捷幫助
** 常見問題
** 關于
**
算法從并查集Union Find算法談算法解決實際問題的過程
網絡和大數據Tell me about your faimly
GUI程序開發基于面部識別的日志系統的設計與實現
離散數學數學史上曾經出現過的三大危機
歸檔
** 網絡和大數據
** 口語學習##算法解決實際問題的一般步驟
Steps to developing a usable algorithm.
- 問題建模(Model the problem.)
- 尋找算法解決問題(Find an algorithm to solve it.)
- 算法的復雜度評判(Fast enough? Fits in memory?)
- 尋找影響時間或控件復雜的的原因(If not, figure out why.)
- 尋找問題源頭(Find a way to address the problem.)
- 迭代上述過程,直到算法達標(Iterate until satisfied.)
并查集算法概述
給定一個包含N個對象的集合,集合中對象可以相互連接,實現如下命令:
Union
Union command: 連接兩個對象。
Find | Connect
Query: 檢查兩個對象是否被連接。
上圖經歷過的操作如下:
union(0,1)
union(1,2)
union(3,4)
union(3,8)
union(4,9)
union(0,5)
union(5,6)
union(1,6)
union(2,7)
此時根據此圖:檢查結果如下:
True:
connected(0,6)
True: connected(0,5)
False:
connected(2,3)
False: connected(6,9)
實際問題引出
如下圖,P和Q兩個點之間是否存在一條路徑(P,Q是否連通)?
在具體一點,現在要解決的實際問題有如下幾個:
- 一張圖片中像素點的連接檢查。
- 大型網絡中設備之間的連接檢查。查找
- 社交網絡中的好友關系。
- 游戲場景中的路徑檢查。
問題建模
復雜問題簡單化
使用整型數組來抽象實際問題。舍棄一些細節,僅僅描述其連接關系。
具體的實現
使用數組的下標了表示問題中的某一個對象,使用對應的值存儲連接參數。
進一步抽象
A: 連接關系
如果對象A和對象B連接,那么Connected(A,B)
返回True
如果對象A和對象B連接,B又和C連接,那么Connected(A,C)
, 返回True
B:連接組件
如果集合{p, q, r, t}
中, p,q,r
連接,t
和其他三個不連接,則有兩個連接組件:
{p, q, r}
和 {t}
問題解決:
創建數組,使用union
方法創建連接組件,使用find/connected
方法檢查對象是否在同一個連接組件中。
算法版本一 QuickFind
通過問題建模,確定了數據結構和基本方法。解決步驟如下:
一、使用數組的值表示連接關系
數據結構
創建Int數組 id[N], 在這個包含N個元素的數組id中,如果id[p] 和 id[q]的值相同,那么p,q是連通的。
查找方法
檢查兩個對象對應的編號下的值是否一樣。
連接方法
合并p到q,修改所有值為id[p]的元素的值為id[q]
union(6,1)
二、算法的具體實現
/* Quick Find 數據結構:
* 優點:查找操作比較快,只需要執行一次
* 缺點:合并操作效率慢,可能需要n*n次數組讀取
* */
public class QuickFindUF
{
private int[] id;
public QuickFindUF(int N)
{
id = new int[N];
for (int i = 0; i < N; i++ )
id[i] = i;
}
//查找:檢查是否連通就是比較兩個點對應的值
public boolean connected(int p, int q)
{ return id[p] == id[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 (id[i] == pid) id[i] = qid;
}
}
三、算法分析
初始化
初始化的時候,算法需要遍歷整個數組,時間負載度為$N$
查找
在查找操作中,僅僅需要訪問數組的兩個元素即可,程序執行效率的時間復雜度為常數$1$。
合并
合并操作在這一版本的算法中,需要遍歷整個數組,時間復雜度為$N$
最壞的情況
當需要連接$N$個點的時候,算法需要遍歷數組$N2$次。當前CPU每秒可以執行$109$條指令;內存內可以存儲$109$個字節的內容;讀取內存內所有內容則需要1s。此時,擁有$109$個節點,要執行$109$次連接,則需要$109 * 10^9$次操作,需要31年不眠不休的運算。
算法版本二 QuickUnion
一、在樹組內創建樹表示連接關系
數據結構
在數組內創建樹,連接后在同一連接組件的元素, 組成一棵樹。
查找方法
檢查兩個對象的根是否相同。
連接方法
合并兩個節點P , Q,即將Q的Root(根節點)設置為P的根節點。
上圖中,3的根節點可以追溯的9:
3->4->9->9
5的根節點可以追溯到6:
5->6->6
這個時候,合并 3 和 5:
union(3,5)
把五的根節點設置給3的根幾點,也就是修改三的根節點9為6.
二、算法的具體實現
/*快速合并
*優點:連接的時候時間復雜度為N
*缺點:查找的時候時間復雜度為N(t)
* */
public class QuickUnionUF
{
private int[] id;
public QuickUnionUF(int N)
{
id = new int[N];
for (int i = 0; i < N; i++) id[i] = i;
}
private int root(int i)
{ //查找樹的根
while (i != id[i]) i = id[i];
return i;
}
public boolean connected(int p, int q)
{ //檢查是否在同一棵樹內
return root(p) == root(q);
}
public void union(int p, int q)
{ //合并兩棵樹
int i = root(p);
int j = root(q);
id[i] = j;
}
}
算法分析
初始化
初始化的時候,算法需要遍歷整個數組,時間負載度為$N$
查找
在查找操作中,需要回溯到一棵樹的根節點,程序執行效率的時間復雜度為常數$1$。
合并
合并操作在這一版本的算法中,需要多次回溯一棵樹的根節點,時間復雜度為$Nt$, t為樹的高度。
最壞的情況
按照目前的算法,如果一顆樹過于高,那么尋找root的操作就會浪費大量的時間。這種算法復雜度高于版本1。
改進版本二 垂直化到扁平化
版本二QuickUnion 的缺陷就在于,合并操作很容易一不小心就產生一顆非常高的樹,然后在回溯這棵樹的時候,需要經過大量的節點才能找到root,故此效率低下。
第一步:帶權合并
解決的辦法是降低樹的高度,在合并操作中,如果碰到兩棵樹的合并,將小一點的樹放在大樹的下面,將小樹的根節點作為大樹的根節點的子節點,則可以降低樹的高度。因此我們需要一個額外的數組來記錄每一個樹的尺寸。這些值也就被乘坐權重,改進后的算法被稱作帶權合并。
上圖中,如果按照Quick-union的方法,很有可能得到一顆很高的樹,原因就在于一顆大樹的根節點被當作了小樹的子節點。
加權之后,進行判斷,確保小樹永遠被放置在大樹內,大大降低了樹的高度。
實現
public class WeightedQuickUnionUF
{
private int[] id;
private int[] sz;
//初始化數組
public WeightedQuickUnionUF(int N)
{
id = new int[N];
sz = new int[N];
for (int i = 0; i < N; i++)
{
id[i] = i;
sz[i] = 1;//初始化每顆樹高度都為1
}
}
private int root(int i)
{
while (i != id[i]) i = id[i];
return i;
}
public boolean connected(int p, int q)
{
return root(p) == root(q);
}
public void union(int p, int q)
{
int i = root(p);
int j = root(q);
if (i == j) return;
//比較樹的大小,確定讓小樹追加在大樹內
if(sz[i] < sz[j]) { id[i] = j; sz[j] += sz[i]; }
else { id[j] = i; sz[i] += sz[j]; }
}
}
帶權前后樹的高度對比:
再進一步:壓縮路徑
帶權合并之后樹的高度明顯降低,回溯的代價也就更小了。但是,在回溯的時候我們一就是一個節點一個節點的往上追溯。
在上圖中要找到3的root:
3->4->9 return 9
因為id[4] = 9
如果利用這個特性,在這個時候壓縮一下追查的路徑:
3->id[4] return 9
這樣一來回溯的成本將大大降低,因此可以對追查root的代碼進一步優化:
private int root(int i)
{
while (i == id[i])
{
i = id[id[i]];
id[i] = i;
}
}
效果如下:
壓縮路徑之前:
壓縮路徑之后:
四種算法時間復雜度對比
解決實際問題: 鄉村公路暢通工程
問題描述
某省調查城鎮交通狀況,得到現有城鎮道路統計表,表中列出了每條道路直接連通的城鎮。省政府“暢通工程”的目標是使全省任何兩個城鎮間都可以實現交通(但不一定有直接的道路相連,只要互相間接通過道路可達即可)。現在要求任意給出兩個城鎮,查看其是否連通?
問題解決
通過對問題的建模,發現可以直接套用Union-Find算法即可:
編碼實現:
首先創建一個數組,包含所有鄉鎮并使其對應一個編號,其次使用鄉鎮對應的數組下標創建一個數組
然后使用連接已經連通的鄉鎮,最后進行程序測試。
代碼說明
代碼實現,為了方便輸入,這里使用Python來實現算法:
# -*- coding:utf-8 -*-
#這里的鄉鎮僅作演示,實際問題中可以考慮從文件中讀取
PLACES = ['大王村', '河陽', '蠻荒殿', '流波鎮', '青云山', '萬蝠洞', '鳳凰鎮', '南疆古鎮', '七里侗']
#通過地名獲取編號
def idof(place):
return PLACES.index(place)
#檢查輸入的地名
def cheked(place):
return place in PLACES
#算法實現
class UnionFind:
'''算法的Python實現,實際上邏輯是一樣的'''
def __init__(self, N):
self.sd = []
self.sz = []
for i in range(N):
self.sd.append(i)
self.sz.append(1)
def root(self, i):
while self.sd[i] != i:
i = self.sd[self.sd[i]]
self.sd[i] = i
return i
def connected(self, p, q):
return self.root(p) == self.root(q)
def union(self, p, q):
i = self.root(p)
j = self.root(q)
if i == j:
return
if self.sz[i] < self.sz[j]:
self.sd[i] = j
self.sz[j] += self.sz[i]
else:
self.sd[j] = i
self.sz[i] += self.sz[j]
if __name__ == "__main__":
uf = UnionFind(len(PLACES))
#隨便連接幾個點,以便于測試
uf.union(idof('大王村'), idof('流波鎮'))
uf.union(idof('大王村'), idof('蠻荒殿'))
uf.union(idof('流波鎮'), idof('七里侗'))
#錄入要查尋的地名
place_a = raw_input("請輸入要查詢的第一個地點:")
place_b = raw_input("請輸入要查尋的第二個地點:")
#輸出結果
if cheked(place_a) and cheked(place_b):
result = uf.connected(idof(place_a), idof(place_b))
print "%s 和 %s 之間是否連通?\t %s" % (place_a, place_b, result)
else:
print "輸入地名有誤"
程序運行結果:
連通情況: