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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

关于 并查集(union find) 算法基本原理 以及 其 在分布式图场景的应用

發(fā)布時間:2023/11/27 生活经验 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 关于 并查集(union find) 算法基本原理 以及 其 在分布式图场景的应用 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

二月的最后一篇水文…想寫一些有意思的東西。

文章目錄

    • 環(huán)檢測在圖數(shù)據(jù)結(jié)構(gòu)中的應(yīng)用
      • 深度/廣度優(yōu)先 檢測環(huán)
    • 并查集數(shù)據(jù)結(jié)構(gòu) (Union-Find)
      • 基本概念
      • 初始化
      • 合并 union
      • 查找祖先
        • 優(yōu)化1: 合并過程 利用 rank 優(yōu)化路徑
        • 優(yōu)化2: 路徑壓縮(Path Compression)
    • 并查集 解決圖中檢測環(huán)問題

環(huán)檢測在圖數(shù)據(jù)結(jié)構(gòu)中的應(yīng)用

我們在圖數(shù)據(jù)結(jié)構(gòu)場景中會有一些判斷是否存在環(huán)的需求,大多數(shù)的判斷場景是在有向圖中:

  1. 比如我們在圖數(shù)據(jù)存儲場景中想要拓撲好友關(guān)系,比如查找某一個人到另一個人的好友關(guān)系鏈,這個檢索過程需要是一個有向無環(huán)圖的檢索過程,是不能出現(xiàn)環(huán)的,需要在檢索過程中能夠檢測到環(huán)的存在。
  2. 再比如 我們在分布式事務(wù)場景中,比如悲觀事務(wù)的實現(xiàn)中往往需要有一個 單key 事務(wù)鎖 在并發(fā)場景的 wait 鎖關(guān)系的構(gòu)造 Rocksb 事務(wù)鎖實現(xiàn) – 死鎖檢測部分,這個時候需要對多個事務(wù)的 wait鎖 之間的互相等待關(guān)系構(gòu)造一個 wait-circle,并且需要在一個元素插入之后檢測改有向圖是否存在環(huán),存在則需要回退這次的插入。

當(dāng)然,實際應(yīng)用到圖存儲/計算的場景還有很多,對環(huán)的檢測需求也都是一直存在的。

深度/廣度優(yōu)先 檢測環(huán)

在這種場景下我們一般檢測環(huán)存在的做法是遍歷圖,主要使用兩種方式 (bfs/dfs) :
這兩種實現(xiàn)方式都比較簡單,利用一個visited 數(shù)組保證訪問過程中除非遇到環(huán),否則不會訪問到自己。
下面這個有向圖在遍歷的過程中會出現(xiàn)環(huán),在分布式事務(wù)的場景下 wait-circle 就是出現(xiàn)死鎖了,這種情況下是必須要檢測出環(huán)的。

如下使用深度優(yōu)先搜索來進行環(huán)的查找,前置條件就是使用鄰接矩陣來標識圖中的頂點,比如坐標[i,j] = 1,標識i --> j 即頂點 i 和頂點 j 連通;為 0 則表示兩個頂點不連通。
查找的算法也很簡單:

  1. 增加輔助訪問數(shù)組 visited,標識頂點 i 被訪問過。
  2. 遍歷鄰接矩陣 且 visited 為 false 的頂點
  3. 深度優(yōu)先搜索 所有以 i 為頂點的 [i,j] = 1 即 i --> j ,i --> k的頂點,如果發(fā)現(xiàn)某一個頂點 visited[j]=true,那么就標識已經(jīng)找到環(huán)了,否則繼續(xù)深度優(yōu)先查找。
// 通過鄰接矩陣 matrix 保存有向圖 
vector<vector<int>> matrix;
bool isCircle({// 初始化visit 數(shù)組,標識已經(jīng)訪問過的節(jié)點int m_size = matrix.size();vector<bool> visited(m_size, false);for (int i = 0;i < m_size; i++) {if (!visited[i]) {// 傳入當(dāng)前訪問的下標,訪問標識數(shù)組,最后就是父節(jié)點(上一個訪問的節(jié)點)// 找到了環(huán),就返回真即可.if (dfs_search(i, visited)) {return true;}}}// 整個矩陣都遍歷完成還是沒有找到,就認為是無環(huán)return false;
}// dfs 查找環(huán)
bool dfs_search(int current_idx, vector<bool>& visited) {visited[current_index] = true;// 查找 current_idx 即當(dāng)前節(jié)點的連通節(jié)點for (int i = 0;i < matrix[current_idx].size(); i++) {// 如果為1,則標識當(dāng)前current_idx 指向 i,那么check一下這個指向的節(jié)點是否訪問過,// 那就是找到環(huán)了。if (matrix[current_idx][i] == 1) {if ( visited[i] && i != current_idx) return true;else {if (dfs_search(i, visited)) return true;}}}return false;
}

廣度優(yōu)先算法也很類似(層次遍歷),同樣是通過visited 輔助數(shù)組來實。
和dfs的差異只是利用隊列來提前將 current_idx 的所有指向的兄弟節(jié)點先添加到隊列中,再進行下一層的查找,如下代碼。

// 通過鄰接矩陣 matrix 保存有向圖 
vector<vector<int>> matrix;
bool isCircle({int m_size = matrix.size();vector<bool> visited(m_size, false);// 保存當(dāng)前訪問的頂點for (int i = 0;i < m_size; i ++) {if (!visited[i]) {if (bfs_search(i, visited)) return true;}}return false;
}bool bfs_search(int curr_idx, vector<bool>& visited) {queue<int> qu;qu.push(curr_idx);visited[curr_idx] = true;while (!qu.empty()) {int size = qu.size();// 按層遍歷for (int j = 0;j < size; j++) {int curr_idx = qu.front();qu.pop();// 需要將 curr_idx 所有的臨接頂點 先check 環(huán)是否存在,不存在則添加到queue中for (int k = 0;k < matrix[curr_idx].size(); k++) {if (matrix[curr_idx][k] == 1) {if (curr_idx != k && visited[k]) return true;else {qu.push(k);// 標記 curr_idx --> k 的 k頂點已經(jīng)被訪問過了visited[k] = true;}}}}}return false;
}

以上兩種實現(xiàn)方式都是非常基礎(chǔ)的圖中的環(huán)的檢測,都是是能夠正確得檢測出環(huán)的存在。
但是性能問題卻很明顯,每一次的檢測都需要對整個圖進行一個大的scan,對于一個 m*n 的超大矩陣 (我們普通的分布式圖存儲集群中往往擁有超過百萬/千萬級別的頂點和 總數(shù)目近萬億的邊),在這樣的圖網(wǎng)絡(luò)中去檢測環(huán),利用上面的方式效率可以說是極低的。當(dāng)然,現(xiàn)在最通用的解決辦法就是限制查找的層數(shù)。

索引好友關(guān)系列表的話也僅僅只會索引3度的出邊列表,在分布式事務(wù)中的wait-circle 中也會限制死鎖檢測的深度;但是當(dāng)我們想要查找兩個無關(guān)頂點之間的最短到達路徑的時候,這個過程中的環(huán)檢測避免不了,那有沒有更加高效的算法,在不用每次插入一個元素都進行一次全鏈路遍歷檢測環(huán)呢?

當(dāng)然有~~~, 聰明的人無處不在,并查集 應(yīng)運而生。

并查集數(shù)據(jù)結(jié)構(gòu) (Union-Find)

基本概念

并查集是一種數(shù)據(jù)結(jié)構(gòu),其支持對不相交的集合(disjoint sets)執(zhí)行如下一些操作:

  1. makeSet(e) 這是 并查集數(shù)據(jù)結(jié)構(gòu)中的一個操作,用來將輸入的元素 e 插入到一個集合中,并返回包含這個元素e 的集合的根節(jié)點。
  2. Union(A,B) 合并兩個集合 A,B
  3. Find(e) 返回包含元素e 的集合

基本概念其實是很晦澀的,直接來看并查集的這幾個操作就好。
并查集中的集合元素組織有兩種形態(tài):鏈表形態(tài) 和 形態(tài)。

原本我們的鏈表/樹 形態(tài),會記錄 parent–>child 或者 prev --> next 這樣的關(guān)系,而并查集中對數(shù)據(jù)集的標識 是記錄 next 的 prev節(jié)點 或者說 是記錄 child 的 parent 節(jié)點信息。

其中樹形態(tài)的并查集 表示 方式其實是鏈表形態(tài)的一種優(yōu)化(路徑壓縮),能夠極大得降低元素查找的層數(shù);當(dāng)然, 在圖的環(huán)優(yōu)化中,這兩種形態(tài)可以分別用于有向圖(鏈表形態(tài),保存了集合中的方向關(guān)系)和 無向圖(不需要方向關(guān)系)的表示。

接下來,我們看看并查集的每個操作實現(xiàn)。

初始化

對集合中的每一個元素,他們的父節(jié)點應(yīng)該為 空 或者 也可以指向自己(指向自己的這種初始化方式就不能應(yīng)用在圖的環(huán)判斷中了,可以用于正常的集合操作)。

本文演示均用-1 表示空

雖然前面說并查集的數(shù)據(jù)結(jié)構(gòu) 有兩種形態(tài), 鏈表或者樹,但是實際我們應(yīng)用的時候,可以采用數(shù)組/無序map 的方式就夠了。

unorder_map<int,int> father;
void Add(int ele) {if (!father.count(ele)) {father[ele] = -1; }
}

合并 union

這個操作主要是check 兩個節(jié)點是否可以連通(祖先是不同的), 如果是連通的,那就要進行合并,讓他們擁有相同的祖先。
這里兩個節(jié)點 誰當(dāng)祖先都是可以的。

void union(int x, int y) {int x_root = find(x);int y_root = find(y);// 祖先不同,則發(fā)現(xiàn)是兩個不同的集合,則可以對他們進行合并if (x_root != y_root) {fater[x_root] = y_root;}
}

查找祖先

如果節(jié)點的父節(jié)點不為空,那就需要持續(xù)查找,直到找到父節(jié)點為空的節(jié)點,就是當(dāng)前輸入的ele 的祖先節(jié)點。

我們想要查找節(jié)點 12 的祖先節(jié)點,就需要不斷的check father節(jié)點,直到father節(jié)點為空(或者是自己)。

int find(int ele) {int root = ele;while (father[root] != -1) {root = father[root];}return root;
}

這里是有可以優(yōu)化的地方,我們希望在并查集中的節(jié)點分布能夠更接近樹形態(tài),而不是鏈表形態(tài)。畢竟樹形查找的復(fù)雜度是小于等于(O(log_n)),也就是我們可以將鏈表形態(tài)的并查集結(jié)構(gòu)轉(zhuǎn)換為樹形態(tài)。
這樣做可行的原因是 對于并查集中的節(jié)點來說連通性是可以傳遞的, 節(jié)點之間互相連通的標記只需要擁有一個相同的祖先就好了。

大體過程如下:

就是將上圖中左側(cè)一段鏈式結(jié)構(gòu)可以合并成右側(cè)的樹形結(jié)構(gòu)。

優(yōu)化1: 合并過程 利用 rank 優(yōu)化路徑

rank 是一個在前面father基礎(chǔ)上額外增加的一個數(shù)據(jù)結(jié)構(gòu),標識當(dāng)前節(jié)點距離祖先節(jié)點的長度,這樣我們的初始化以及合并代碼就變成下面的樣子:

typedef struct UnionFindNode {UnionFindNode* father;int rank;UnionFindNode() : father(nullptr),rank(0) {}bool operator==(const UnionFindNode& lhs) {return father == lhs.father && rank == lhs.rank;}
}Node;void MakeSet(Node& ele) {ele.father = nullptr;ele.rank = 0;
}void Union(const Node x, Node y) {// 找到他們的公共祖先節(jié)點auto xRoot = Find(x);auto yRoot = Find(y);if (xRoot == yRoot) {return;}// 他們不在同一個集合,則需要合并他們的祖先。// 將距離比較短的合并到距離長的祖先上。if (xRoot->rank < yRoot->rank) {xRoot->father = yRoot;} else if (xRoot->rank > yRoot->rank) {yRoot->father = xRoot;} else { //rank 相等,互相指向誰都無所謂,需要增加指向后的被指向節(jié)點的rank(增加了一個元素的深度)。xRoot->father = yRoot;yRoot->rank += 1;}
}

優(yōu)化2: 路徑壓縮(Path Compression)

當(dāng)然,以上優(yōu)化方式也能夠利用rank 達到我們將鏈表轉(zhuǎn)換成樹的目的,但是需要一個額外的rank 字段,每一個節(jié)點都會多消耗4bytes 的內(nèi)存 。
其實還有一種更簡潔優(yōu)雅的優(yōu)化方式,就是 路徑壓縮

rank 的優(yōu)化是在 Union 操作的時候,這里路徑壓縮 則是在 Find的時候。

// 還是繼續(xù)使用基本的 unorder_map 保留信息
unordered_map<int, int> father;// 查找節(jié)點 i 的祖先節(jié)點
int Find(int i) {int root = i;// -1 是我們在最前面 Add 一個新的并查集節(jié)點的時候// 會將這個節(jié)點的父節(jié)點設(shè)置為 -1,標識它目前是一個單獨的集合。while(father[root] != -1) {root = father[root];}// 路徑壓縮的過程while (i != root) {int origin_father = father[i];father[i] = root;// 關(guān)鍵!進行路徑壓縮,將節(jié)點i 的父節(jié)點直接指向祖先節(jié)點。i = origin_father;}return root;
}

比如對于這樣的一個并查集集合,我們想要查找 6。

經(jīng)過路徑壓縮之后, 6 包括整個之前的節(jié)點都會嘗試進行一次路徑壓縮。

關(guān)于路徑壓縮的時間復(fù)雜度證明較為復(fù)雜,這個推演是通過 阿克曼函數(shù) 進行推演的。
總之表示方式是 O(log*n),其中 log*n表示 n 取多少次 log2nlog_2nlog2?n并向下取整之后變成1,可以理解為是 O(1) 級別。
比如 log*2^65526 ,2^65536 在阿克曼函數(shù)中表示的是 A(4,3) 的結(jié)果,基本是人類思維極限的數(shù)字,而 在 log*n 下僅僅只有 5。

關(guān)于路徑壓縮時間復(fù)雜度的推演 以及 證明 可以參考 The math in Union-Find.

并查集 解決圖中檢測環(huán)問題

回到我們最初 圖中檢測環(huán)的問題,我們接下來可以利用并查集的幾個操作輕松解決。

對于 0 --> 1 --> 2,構(gòu)造出來的并查集結(jié)構(gòu) 經(jīng)過路徑壓縮 是 0 --> 2 <-- 1 ;而如果存在環(huán),也就意味著 2 --> 0,對于并查集來說 我們只需要 提前 find (0) 和 find(2) 是否相等,如果相等,則認為當(dāng)前的插入是會造成環(huán)的(0 已經(jīng)存在 且 其祖先節(jié)點是2)。

實現(xiàn)如下:

class UnionFind {
public:// void Add// void Union// int Find// bool IsConnected
private:unordered_map<int,int> father_;
}
// 通過鄰接矩陣 matrix 保存有向圖 
vector<vector<int>> matrix;
bool IsCircle() {UnionFind uf;for (int i = 0;i < matirx.size(); i++) {// 添加每一個節(jié)點到并查集之中uf.Add(i);for (int j = 0;j < i; j++) {if (matrix[i][j]) {int x_root = Find(i);int y_root = Find(j);// 發(fā)現(xiàn)了兩個不同節(jié)點的祖先節(jié)點相等,則找到了環(huán)if (x_root == y_root && i != j) return true;// 否則,是兩個可以連通的節(jié)點,那就需要合并這兩個集合(他們的祖先直接合并就好了)。if (x_root != y_root) uf.Union(x_root,y_root);}}}return false;
}

通過并查集,我們能夠在有節(jié)點更新的情況下非常高效得O(1) 的時間內(nèi)確認這個節(jié)點插入后圖中是否存在環(huán)。

除了檢測環(huán)之外,并查集在圖數(shù)據(jù)結(jié)構(gòu)的其他方向也有非常高效的應(yīng)用,比如確認圖中兩個頂點是否連通,高效合并兩組無關(guān)聯(lián)的圖等等。

總的來說,并查集這個數(shù)據(jù)結(jié)構(gòu) 利用阿克曼函數(shù) 在集合論 以及 圖數(shù)據(jù)結(jié)構(gòu)領(lǐng)域中 能夠非常高效得判斷集合交集 以及 圖節(jié)點連通情況,思想值得學(xué)習(xí)研究。

總結(jié)

以上是生活随笔為你收集整理的关于 并查集(union find) 算法基本原理 以及 其 在分布式图场景的应用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。