Union-Find 算法应用
Union-Find 算法應用
并查集詳解
 
文章目錄
- Union-Find 算法應用
 - 一、回顧Union-Find的框架
 - 二、Union-Find 算法解決DFS問題
 - 1.題目描述:被圍繞的區域
 - 2.分析
 - 3.代碼
 - 1.題目描述:判定合法算式
 - 2.分析
 - 3.代碼
 
一、回顧Union-Find的框架
class UF {// 記錄連通分量個數private int count;// 存儲若干棵樹private int[] parent;、// 記錄樹的“重量”private int[] size;public UF(int n) {this.count = n;parent = new int[n];size = new int[n];for (int i = 0; i < n; i++) {parent[i] = i;size[i] = 1;}}/* 將 p 和 q 連通 */public void union(int p, int q) {int rootP = find(p);int rootQ = find(q);if (rootP == rootQ)return;// 小樹接到大樹下面,較平衡if (size[rootP] > size[rootQ]) {parent[rootQ] = rootP;size[rootP] += size[rootQ];} else {parent[rootP] = rootQ;size[rootQ] += size[rootP];}count--;}/* 判斷 p 和 q 是否互相連通 */public boolean connected(int p, int q) {int rootP = find(p);int rootQ = find(q);// 處于同一棵樹上的節點,相互連通return rootP == rootQ;}/* 返回節點 x 的根節點 */private int find(int x) {while (parent[x] != x) {// 進行路徑壓縮parent[x] = parent[parent[x]];x = parent[x];}return x;}public int count() {return count;} }算法的關鍵點有 3 個:
- 1、用parent數組記錄每個節點的父節點,相當于指向父節點的指針,所以parent數組內實際存儲著一個森林(若干棵多叉樹)。
 - 2、用size數組記錄著每棵樹的重量,目的是讓union后樹依然擁有平衡性,而不會退化成鏈表,影響操作效率。
 - 3、在find函數中進行路徑壓縮,保證任意樹的高度保持在常數,使得union和connectedAPI 時間復雜度為 O(1)。
 
有的菇涼們可能會問,既然有了路徑壓縮,size數組的重量平衡還需要嗎?這個問題很有意思,因為路徑壓縮保證了樹高為常數(不超過 3),那么樹就算不平衡,高度也是常數,基本沒什么影響。
我認為,論時間復雜度的話,確實,不需要重量平衡也是 O(1)。但是如果加上size數組輔助,效率還是略微高一些,比如下面這種情況:
- 如果帶有重量平衡優化,一定會得到情況一,而不帶重量優化,可能出現情況二。高度為 3 時才會觸發路徑壓縮那個while循環,所以情況一根本不會觸發路徑壓縮,而情況二會多執行很多次路徑壓縮,將第三層節點壓縮到第二層。
 - 也就是說,去掉重量平衡,雖然對于單個的find函數調用,時間復雜度依然是 O(1),但是對于 API 調用的整個過程,效率會有一定的下降。當然,好處就是減少了一些空間,不過對于 Big O 表示法來說,時空復雜度都沒變。
 
下面言歸正傳,來看看這個算法有什么實際應用。
二、Union-Find 算法解決DFS問題
很多使用 DFS 深度優先算法解決的問題,也可以用 Union-Find 算法解決。
1.題目描述:被圍繞的區域
給定一個二維的矩陣,包含 'X' 和 'O'(字母 O)。
找到所有被 ‘X’ 圍繞的區域,并將這些區域里所有的 ‘O’ 用 ‘X’ 填充。
示例:
X X X X X O O X X X O X X O X X運行你的函數后,矩陣變為:
X X X X X X X X X X X X X O X X解釋:
被圍繞的區間不會存在于邊界上,換句話說,任何邊界上的 'O' 都不會被填充為 'X'。 任何不在邊界上,或不與邊界上的 'O' 相連的 'O' 最終都會被填充為 'X'。如果兩個 元素在水平或垂直方向相鄰,則稱它們是“相連”的。2.分析
注意哦,必須是完全被圍的O才能被換成X,也就是說邊角上的O一定不會被圍,進一步,與邊角上的O相連的O也不會被X圍四面,也不會被替換:
- 解決這個問題的傳統方法也不困難,先用 for 循環遍歷棋盤的四邊,用 DFS算法把那些與邊界相連的O換成一個特殊字符,比如#;然后再遍歷整個棋盤,把剩下的O換成X,把#恢復成O。這樣就能完成題目的要求,時間復雜度O(MN)。
 - 這個問題也可以用 Union-Find 算法解決,雖然實現復雜一些,甚至效率也略低,但這是使用 Union-Find算法的通用思想,值得一學。
 - 你可以把那些不需要被替換的O看成一個擁有獨門絕技的門派,它們有一個共同祖師爺叫dummy,這些O和dummy互相連通,而那些需要被替換的O與dummy不連通。
 
這就是 Union-Find 的核心思路,明白這個圖,就很容易看懂代碼了:
- 首先要解決的是,根據我們的實現,Union-Find底層用的是一維數組,構造函數需要傳入這個數組的大小,而題目給的是一個二維棋盤。
 - 這個很簡單,二維坐標(x,y)可以轉換成x * n + y這個數(m是棋盤的行數,n是棋盤的列數)。敲黑板,這是將二維坐標映射到一維的常用技巧。
 - 其次,我們之前描述的「祖師爺」是虛構的,需要給他老人家留個位置。索引[0.. m*n-1]都是棋盤內坐標的一維映射,那就讓這個虛擬的dummy節點占據索引m*n好了。
 
3.代碼
傳統DFS解法:
class Solution { public:void solve(vector<vector<char>>& board) {if(board.size()<3)return ;int m=board.size();int n=board[0].size();for(int i=0;i<m;i++){dfs(i,0,board);dfs(i,n-1,board);}for(int i=0;i<n;i++){dfs(0,i,board);dfs(m-1,i,board);}for(int i=0;i<m;i++){for(int j=0;j<n;j++){if(board[i][j]=='O')board[i][j]='X';if(board[i][j]=='*')board[i][j]='O';}}return ;}void dfs(int x,int y,vector<vector<char>>&board){if(x<0||y<0||x>=board.size()||y>=board[0].size()||board[x][y]!='O')return ;board[x][y]='*';dfs(x+1,y,board);dfs(x-1,y,board);dfs(x,y+1,board);dfs(x,y-1,board);return ;} };并查集解法:
void solve(char[][] board) {if (board.length == 0) return;int m = board.length;int n = board[0].length;// 給 dummy 留一個額外位置UF uf = new UF(m * n + 1);int dummy = m * n;// 將首列和末列的 O 與 dummy 連通for (int i = 0; i < m; i++) {if (board[i][0] == 'O')uf.union(i * n, dummy);if (board[i][n - 1] == 'O')uf.union(i * n + n - 1, dummy);}// 將首行和末行的 O 與 dummy 連通for (int j = 0; j < n; j++) {if (board[0][j] == 'O')uf.union(j, dummy);if (board[m - 1][j] == 'O')uf.union(n * (m - 1) + j, dummy);}// 方向數組 d 是上下左右搜索的常用手法int[][] d = new int[][]{{1,0}, {0,1}, {0,-1}, {-1,0}};for (int i = 1; i < m - 1; i++) for (int j = 1; j < n - 1; j++) if (board[i][j] == 'O')// 將此 O 與上下左右的 O 連通for (int k = 0; k < 4; k++) {int x = i + d[k][0];int y = j + d[k][1];if (board[x][y] == 'O')uf.union(x * n + y, i * n + j);}// 所有不和 dummy 連通的 O,都要被替換for (int i = 1; i < m - 1; i++) for (int j = 1; j < n - 1; j++) if (!uf.connected(dummy, i * n + j))board[i][j] = 'X'; }這段代碼很長,其實就是剛才的思路實現,只有和邊界O相連的O才具有和dummy的連通性,他們不會被替換。
說實話,Union-Find 算法解決這個簡單的問題有點殺雞用牛刀,它可以解決更復雜,更具有技巧性的問題,主要思路是適時增加虛擬節點,想辦法讓元素「分門別類」,建立動態連通關系。
1.題目描述:判定合法算式
這個問題用 Union-Find 算法就顯得十分優美了。題目是這樣:
給你一個數組equations,裝著若干字符串表示的算式。每個算式equations[i]長度都是 4,而且只有這兩種情況:a==b或者a!=b,其中a,b可以是任意小寫字母。你寫一個算法,如果equations中所有算式都不會互相沖突,返回 true,否則返回 false。
例:
比如說,輸入["a==b","b!=c","c==a"],算法返回 false, 因為這三個算式不可能同時正確。 再比如,輸入["c==c","b==d","x!=z"],算法返回 true, 因為這三個算式并不會造成邏輯沖突。2.分析
我們前文說過,動態連通性其實就是一種等價關系,具有「自反性」「傳遞性」和「對稱性」,其實==關系也是一種等價關系,具有這些性質。所以這個問題用 Union-Find 算法就很自然。
核心思想是,將equations中的算式根據 == 和 != 分成兩部分,先處理==算式,使得他們通過相等關系各自勾結成門派;然后處理!=算式,檢查不等關系是否破壞了相等關系的連通性。
3.代碼
boolean equationsPossible(String[] equations) {// 26 個英文字母UF uf = new UF(26);// 先讓相等的字母形成連通分量for (String eq : equations) {if (eq.charAt(1) == '=') {char x = eq.charAt(0);char y = eq.charAt(3);uf.union(x - 'a', y - 'a');}}// 檢查不等關系是否打破相等關系的連通性for (String eq : equations) {if (eq.charAt(1) == '!') {char x = eq.charAt(0);char y = eq.charAt(3);// 如果相等關系成立,就是邏輯沖突if (uf.connected(x - 'a', y - 'a'))return false;}}return true; }總結
以上是生活随笔為你收集整理的Union-Find 算法应用的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: Union-Find 并查集算法详解
 - 下一篇: 实现 LRU 缓存机制