日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Union-Find 并查集算法详解

發布時間:2024/4/11 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Union-Find 并查集算法详解 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

Union-Find 并查集算法詳解

文章目錄

    • Union-Find 并查集算法詳解
    • 一、問題介紹
    • 二、基本思路
    • 三、平衡性優化
    • 四、路徑壓縮
    • 五、總結
    • 六、例題

一、問題介紹

簡單說,動態連通性其實可以抽象成給一幅圖連線。比如下面這幅圖,總共有 10 個節點,他們互不相連,分別用 0~9 標記:

現在我們的 Union-Find 算法主要需要實現這兩個 API:

class UF {/* 將 p 和 q 連接 */public void union(int p, int q);/* 判斷 p 和 q 是否連通 */public boolean connected(int p, int q);/* 返回圖中有多少個連通分量 */public int count(); }

這里所說的 「連通」 是一種等價關系,也就是說具有如下三個性質:

  • 1、自反性:節點p和p是連通的。
  • 2、對稱性:如果節點p和q連通,那么q和p也連通。
  • 3、傳遞性:如果節點p和q連通,q和r連通,那么p和r也連通。

比如說之前那幅圖,0~9 任意兩個不同的點都不連通,調用connected都會返回 false,連通分量為 10 個。

如果現在調用union(0, 1),那么 0 和 1 被連通,連通分量降為 9 個。

再調用union(1, 2),這時 0,1,2 都被連通,調用connected(0, 2)也會返回 true,連通分量變為 8 個。


判斷這種「等價關系」非常實用,比如說編譯器判斷同一個變量的不同引用,比如社交網絡中的朋友圈計算等等。

這樣,你應該大概明白什么是動態連通性了,Union-Find 算法的關鍵就在于union和connected函數的效率。那么用什么模型來表示這幅圖的連通狀態呢?用什么數據結構來實現代碼呢?

二、基本思路

注意我剛才把「模型」和具體的「數據結構」分開說,這么做是有原因的。因為我們使用森林(若干棵樹)來表示圖的動態連通性,用數組來具體實現這個森林

怎么用森林來表示連通性呢?我們設定樹的每個節點有一個指針指向其父節點,如果是根節點的話,這個指針指向自己。

比如說剛才那幅 10 個節點的圖,一開始的時候沒有相互連通,就是這樣:

class UF {// 記錄連通分量private int count;// 節點 x 的節點是 parent[x]private int[] parent;/* 構造函數,n 為圖的節點總數 */public UF(int n) {// 一開始互不連通this.count = n;// 父節點指針初始指向自己parent = new int[n];for (int i = 0; i < n; i++)parent[i] = i;}/* 其他函數 */ }


如果某兩個節點被連通,則讓其中的(任意)一個節點的根節點接到另一個節點的根節點上

public void union(int p, int q) {int rootP = find(p);int rootQ = find(q);if (rootP == rootQ)return;// 將兩棵樹合并為一棵parent[rootP] = rootQ;// parent[rootQ] = rootP 也一樣count--; // 兩個分量合二為一 }/* 返回某個節點 x 的根節點 */ private int find(int x) {// 根節點的 parent[x] == xwhile (parent[x] != x)x = parent[x];return x; }/* 返回當前的連通分量個數 */ public int count() { return count; }


這樣,如果節點p和q連通的話,它們一定擁有相同的根節點:

public boolean connected(int p, int q) {int rootP = find(p);int rootQ = find(q);return rootP == rootQ; }


至此,Union-Find 算法就基本完成了。是不是很神奇?竟然可以這樣使用數組來模擬出一個森林,如此巧妙的解決這個比較復雜的問題!

那么這個算法的復雜度是多少呢?我們發現,主要 APIconnected和union中的復雜度都是find函數造成的,所以說它們的復雜度和find一樣

find主要功能就是從某個節點向上遍歷到樹根,其時間復雜度就是樹的高度。我們可能習慣性地認為樹的高度就是logN,但這并不一定。logN的高度只存在于平衡二叉樹,對于一般的樹可能出現極端不平衡的情況,使得「樹」幾乎退化成「鏈表」,樹的高度最壞情況下可能變成N。

所以說上面這種解法,find,union,connected的時間復雜度都是 O(N)。這個復雜度很不理想的,你想圖論解決的都是諸如社交網絡這樣數據規模巨大的問題,對于union和connected的調用非常頻繁,每次調用需要線性時間完全不可忍受。

問題的關鍵在于,如何想辦法避免樹的不平衡呢?只需要略施小計即可。

三、平衡性優化

我們要知道哪種情況下可能出現不平衡現象,關鍵在于union過程:

public void union(int p, int q) {int rootP = find(p);int rootQ = find(q);if (rootP == rootQ)return;// 將兩棵樹合并為一棵parent[rootP] = rootQ;// parent[rootQ] = rootP 也可以count--; }

我們一開始就是 簡單粗暴的把p所在的樹接到q所在的樹的根節點下面那么這里就可能出現「頭重腳輕」的不平衡狀況,比如下面這種局面


長此以往,樹可能生長得很不平衡。我們其實是希望,小一些的樹接到大一些的樹下面,這樣就能避免頭重腳輕,更平衡一些。解決方法是額外使用一個size數組,記錄每棵樹包含的節點數,我們不妨稱為「重量」

class UF {private int count;private int[] parent;// 新增一個數組記錄樹的“重量”private int[] size;public UF(int n) {this.count = n;parent = new int[n];// 最初每棵樹只有一個節點// 重量應該初始化 1size = new int[n];for (int i = 0; i < n; i++) {parent[i] = i;size[i] = 1;}}/* 其他函數 */ }

比如說size[3] = 5表示,以節點3為根的那棵樹,總共有5個節點。這樣我們可以修改一下union方法:

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--; }

這樣,通過比較樹的重量,就可以保證樹的生長相對平衡,樹的高度大致在logN這個數量級,極大提升執行效率。

此時,find,union,connected的時間復雜度都下降為 O(logN),即便數據規模上億,所需時間也非常少。

四、路徑壓縮

這步優化特別簡單,所以非常巧妙。我們能不能進一步壓縮每棵樹的高度,使樹高始終保持為常數?


這樣find就能以 O(1) 的時間找到某一節點的根節點,相應的,connected和union復雜度都下降為 O(1)。

要做到這一點,非常簡單,只需要在find中加一行代碼:

private int find(int x) {while (parent[x] != x) {// 進行路徑壓縮parent[x] = parent[parent[x]];x = parent[x];}return x; }

可見,調用find函數每次向樹根遍歷的同時,順手將樹高縮短了,最終所有樹高都不會超過 3(union的時候樹高可能達到 3)。

五、總結

我們先來看一下完整代碼:

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;}}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--;}public boolean connected(int p, int q) {int rootP = find(p);int rootQ = find(q);return rootP == rootQ;}private int find(int x) {while (parent[x] != x) {// 進行路徑壓縮parent[x] = parent[parent[x]];x = parent[x];}return x;} }

Union-Find 算法的復雜度可以這樣分析:構造函數初始化數據結構需要 O(N) 的時間和空間復雜度;連通兩個節點union、判斷兩個節點的連通性connected、計算連通分量count所需的時間復雜度均為 O(1)。

  • C++代碼
#include <iostream> #include <vector> using namespace std;class UnionFind { public: UnionFind(size_t N) { _arr.resize(N,-1);} //找根int FindRoot(int x){ while(_arr[x] >= 0){ x = _arr[x]; }return x;}//合并并查集void Union(int x1,int x2){int root1 = FindRoot(x1);int root2 = FindRoot(x2);if(root1 != root2){_arr[root1] += _arr[root2];_arr[root2] = root1;}} size_t SetSize(){size_t n = 0;for(size_t i = 0;i < _arr.size();i++){if(_arr[i] < 0)n++;}return n;}private: vector<int> _arr; };

六、例題


class UnionFind { public: UnionFind(size_t N) { _arr.resize(N,-1);} //找根int FindRoot(int x){ while(_arr[x] >= 0){ x = _arr[x]; }return x;}//合并并查集void Union(int x1,int x2){int root1 = FindRoot(x1);int root2 = FindRoot(x2);if(root1 != root2){_arr[root1] += _arr[root2];_arr[root2] = root1;}} size_t SetSize(){size_t n = 0;for(size_t i = 0;i < _arr.size();i++){if(_arr[i] < 0)n++;}return n;}private: vector<int> _arr; }; class Solution { public:int findCircleNum(vector<vector<int>>& M) {UnionFind ufs(M.size());for(size_t i = 0;i < M.size();i++){for(size_t j = 0;j < M[0].size();j++){if(i == j){break;}if(M[i][j])ufs.Union(i,j);}}return ufs.SetSize();} };



  • 我們可以將每一個變量看作圖中的一個節點,把相等的關系 == 看作是連接兩個節點的邊
  • 那么由于表示相等關系的等式方程具有傳遞性,即如果 a==b 和 b==c 成立,則 a==c 也成立。
  • 也就是說,所有相等的變量屬于同一個連通分量。因此,我們可以使用并查集來維護這種連通分量的關系。
  • 首先遍歷所有的等式,構造并查集。同一個等式中的兩個變量屬于同一個連通分量,因此將兩個變量進行合并。
  • 然后遍歷所有的不等式。同一個不等式中的兩個變量不能屬于同一個連通分量,因此對兩個變量分別查找其所在的連通分量,如果兩個變量在同一個連通分量中,則產生矛盾,返回 false。
  • 如果遍歷完所有的不等式沒有發現矛盾,則返回 true。
class UnionFind { public: UnionFind(size_t N) { _arr.resize(N,-1);} //找根int FindRoot(int x){ while(_arr[x] >= 0){ x = _arr[x]; }return x;}//合并并查集void Union(int x1,int x2){int root1 = FindRoot(x1);int root2 = FindRoot(x2);if(root1 != root2){_arr[root1] += _arr[root2];_arr[root2] = root1;}} size_t SetSize(){size_t n = 0;for(size_t i = 0;i < _arr.size();i++){if(_arr[i] < 0)n++;}return n;}private: vector<int> _arr; }; class Solution { public:bool equationsPossible(vector<string>& equations) {UnionFind ufs(26);for(auto& e : equations){if(e[1] == '='){char ch1 = e[0];char ch2 = e[3];ufs.Union(ch1 - 'a',ch2 - 'a');}}for(auto& e : equations){if(e[1] == '!'){char ch1 = e[0];char ch2 = e[3];int root1 = ufs.FindRoot(ch1 - 'a');int root2 = ufs.FindRoot(ch2 - 'a');if(root1 == root2){return false;}}}return true; } };

總結

以上是生活随笔為你收集整理的Union-Find 并查集算法详解的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。