为什么引入并查集
并查集的引入是为了解决动态连通问题。在动态连通场景中解决给定的两节点,判断它们是否连通,如果连通,不需要给出具体路径。(而对于要给出具体路径的问题可以采用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