並查集(UnionFind)
阿新 • • 發佈:2018-11-06
為什麼引入並查集
並查集的引入是為了解決動態連通問題。在動態連通場景中解決給定的兩節點,判斷它們是否連通,如果連通,不需要給出具體路徑。(而對於要給出具體路徑的問題可以採用DFS).
什麼是並查集
在電腦科學中,並查集是一種樹型的資料結構,其保持著用於處理一些不相交集合(Disjoint Sets)的合併及查詢問題。一般該資料結構定義了兩種操作:
- Find:確定一個元素屬於哪個子集
- Union:將兩個子集合併成一個集合
並查集是一種樹形結構:初始讓所有元素獨立成樹,然後根據需要將連通元素合併成一棵樹,合併方式為將一棵樹的父節點索引到另一顆樹上。
並查集的詳解
並查集的基本操作有:
1、MakeSet(x):把每一個元素初始化為一個集合,初始化後每一個元素的父節點就是它自己
int parent[MAX];
int rank[MAX];
void MakeSet(int x)
{
father[x] = x;
rank[x] = 0;
}
2、FindSet(x):查詢一個元素所在的集合。不斷向上查詢,直到根節點。
int FindSet(int x)
{
int r = x, tmp;
while (parent[r] != r) //找到根節點
r = parent[r];
//路徑壓縮
while (x != r)
{
tmp = parent[x];
parent[x] = r;
x = tmp;
}
return x;
}
3、Union(x,y):合併兩個集合。關鍵是將一個集合的根節點變成另一個集合的根節點。
void Union(int x, int y)
{
x = FindSet(x);
y = FindSet(y);
if (x == y) //有相同的根節點,不用合併啦
return;
if (rank[x] > rank[y])
parent [y] = x; //x所在的樹高度比y高,讓x成為y的根節點
else
{
parent[x] = y;
if (rank[x] == rank[y])
rank[y]++;
}
}
並查集的優化方法:
- FindSet(x)的路徑壓縮:當尋找根節點時,最壞的情況是一棵樹變為一條鏈,這樣時間複雜度為O(n);因此這裡採用路徑壓縮,即每次遞推找到根節點的後,回溯的時候將它的子孫節點都指向根節點。這樣FindSet(x)複雜度變為O(1)了。
- Union(x,y)合併時按照元素少的合併到元素多的集合,這樣合併後樹的高度相對會小。
並查集實現及例子
/*
leetcode 130. Surrounded Regions
Given a 2D board containing 'X' and 'O' (the letter O), capture all regions
surrounded by 'X'.
A region is captured by flipping all 'O's into 'X's in that surrounded region.
For example,
X X X X
X O O X
X X O X
X O X X
After running your function, the board should be:
X X X X
X X X X
X X X X
X O X X
題目大意:將被'X'包圍的'O'換為'X'
解題思路:一定要讀懂題目的意思。這裡被'X'包圍的'O'的對立一面就是在邊緣上的'O'以及
和邊緣連通的'O'
*/
#include <iostream>
#include <vector>
using namespace std;
class Solution {
private:
vector<int> root; //並查集的根節點
vector<int> isEdge; //是否為邊緣上的'O'或其連通的
vector<int> rank; //樹的高度,為了加速
//初始化並查集
void MakeSet(int len)
{
root = vector<int>(len, 0);
isEdge = vector<int>(len, 0);
rank = vector<int>(len, 0);
for (int i = 0; i < len; ++i)
{
root[i] = i;
}
}
//查詢根節點
int Find(int p)
{
int r = p;
while (root[r] != r)
r = root[r]; //find the root
int tmp;
while (p != r) //路徑壓縮
{
tmp = root[p];
root[p] = r;
p = tmp;
}
return r;
}
//合併:這裡要注意,合併時邊緣也需要改變
void Union(int p, int q)
{
p = Find(p);
q = Find(q);
if (p == q)
return;
if (rank[p] >= rank[q])
{
root[q] = p;
if (isEdge[q])
isEdge[p] = 1;
if (rank[p] == rank[q])
++rank[p];
}
else//(rank[q] > rank[p])
{
root[p] = q;
if (isEdge[p])
isEdge[q] = 1;
}
}
public:
void solve(vector<vector<char>>& board) {
int rows = board.size();
if (rows == 0)
return;
int cols = board[0].size();
if (rows < 3 || cols < 3)
return;
int len = rows*cols;
//初始化
MakeSet(len);
for (int i = 0; i < rows; ++i)
{
for (int j = 0; j < cols; ++j)
{
if ((i == 0 || i == rows - 1 || j == 0 || j == cols - 1) && (board[i][j] == 'O'))
{
isEdge[i*cols + j] = 1;
}
}
}
//合併連通域
for (int i = 0; i < rows; ++i)
{
for (int j = 0; j < cols; ++j)
{
//cout << "(" << i << "," << j << ")" << endl;
if (board[i][j] == 'O' && i < rows - 1 && board[i][j] == board[i + 1][j] )
Union(i*cols + j, (i + 1)*cols + j);
if (board[i][j] == 'O' && j < cols - 1 && board[i][j] == board[i][j + 1])
Union(i*cols + j, i*cols + j + 1);
}
}
//替換
for (int i = 0; i < rows; ++i)
{
for (int j = 0; j < cols; ++j)
{
if (board[i][j] == 'O' && !isEdge[Find(i*cols + j)])
board[i][j] = 'X';
}
}
}
};
void TEST()
{
/*vector<vector<char>>v {
{'X', 'X', 'X', 'X'},
{'X', 'O', 'O', 'X'},
{'X', 'X', 'O', 'X'},
{'X', 'O', 'X', 'X'}
};*/
vector<vector<char>>v{
{ 'X', 'O', 'X', 'X' },
{ 'O', 'X', 'O', 'X' },
{ 'X', 'O', 'X', 'O' },
{ 'O', 'X', 'O', 'X' },
{ 'X', 'O', 'X', 'O' },
{ 'O', 'X', 'O', 'X' }
};
Solution sol;
sol.solve(v);
for (auto vec : v)
{
for (auto ch : vec)
cout << ch << " ";
cout << endl;
}
}
int main()
{
TEST();
return 0;
}
例子2:
poj1611 http://blog.csdn.net/charles1e/article/details/59536384