并查集主要用来解决元素分组问题。它管理一系列不相交的集合,并支持两种操作:
- 合并(Union):把两个不相交的集合合并为一个集合。
- 查询(Find):查询两个元素是否在同一集合中。
并查集的重要思想在于,用集合中的一个元素代表集合。假如有若干集合,当若干集合中的两个集合合并时,就将任意一个集合设置为另外一个集合的父节点。
初始化
int fa[MAX];
public void Init(int n){
//初始化集合元素,每个元素的父节点是自己
for(int i = 1; i < n; i++){
fa[i] = i;
}
}
查询
查询算法1:思想是通过从下往上依次遍历根节点查找。缺点是当一个集合树过高,依次查询效率不高。
//要判断两个集的元素是否同属于一个集合,检查两个元素的根节点是否一致即可
public void Find(int x){
//用递归从当前节点出发,依次向上查找父节点,直至根节点
if(fa[x] == x){
return x;
}
return Find(fa[x]);
}
查询算法2:按照第一种查询算法,我们发现当一个集合元素过多时查询效率低下。有没有一种方法可以提高查询效率?有的,就是路径压缩,说的通俗一点就是在查询时将遍历的每个元素的父节点指向它的根节点。
public void Find(int x){
//将查询到的元的父节点设置为根节点
return x == fa[x]? x : (fa[x] = find(fa[x]));
}
有些人可能有一个误解,以为路径压缩优化后,并查集始终都是一个菊花图(只有两层的树的俗称)。但其实,由于路径压缩只在查询时进行,也只压缩一条路径,所以并查集最终的结构仍然可能是比较复杂的。
合并
合并算法1:合并基本思路是找到两个集合的代表元素也就是他们的根节点,然后将前者父节点指向另外一个。用图来理解就是:
//这里是伪代码主要阐述合并元素的重要概念。
public void Merge(int i, int j){
fa[find(i)] = find(j);
}
合并算法2:合并算法1并没有很好的处理复杂树和简单树合并的情况。当复杂树合并到简单树上会导致,树的高度增加,从而导致查找路径长度增加,因此要考虑将简单树合并到复杂树上去,尽可能的减少高度。因此可以考虑按秩合并,秩相当于树的深度,具体实现如下。
//对于每个单独的元素集合,他的秩设置为1
public void init(){
for(int i = 0; i <= n; i++){
fa[i] = i;
//rank代表每个元素的秩
rank[i] = 1;
}
}
//按秩合并
public void Merge(int i, int j){
int x = find(i);
int y = find(j);
//秩越小说明树越简单,因此将x的父节点指向y
if(rank[x] <= rank[y])
fa[x] = y;
else
fa[y] = x;
//秩相等,并且根节点不相等,此时任意赋值即可,不需要考虑复杂树和简单树
if(rank[x] == rank[y] && x != y){
fa[x] = y;
rank[y]++;
}
}
真题练习
LeetCode 130.被围绕的区域
给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。
示例 1:
输入:board = [["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'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
解题思路:思路是把被包围的区域划分到一个集合A中,没有被包围的集合划分一个集合B中。通过DFS遍历相邻元素实现集合划分。
public class LCS130
{
public int[] Father;
public int[] Rank;
public int Collect = -1; //收集没有被围绕的区域
public void Solve(char[][] board)
{
int m = board.Length;
int n = board[0].Length;
int size = m * n;
Init(size);
for (int i = 0; i < size; i++)
{
int row = i / n;
int col = i % n;
if (board[row][col] != '*')
{
DFS(board, row, col, i);
}
}
for (int i = 0; i < size; i++)
{
int row = i / n;
int col = i % n;
if (board[row][col] == '*') {
if (Collect == -1)
board[row][col] = 'X';
else
board[row][col] = Find(i) != Find(Collect) ? 'X' : 'O';
}
}
}
private int DFS(char[][] board, int row, int col, int index)
{
//处理边界问题
if(row < 0 || row > board.Length - 1) return -1;
if(col < 0 || col > board[0].Length - 1) return -1;
if(board[row][col] == 'X') return -2;
if(board[row][col] == '*') return -3;
board[row][col] = '*';
int r = DFS(board, row + 1, col, index);
int l = DFS(board, row - 1, col, index);
int u = DFS(board, row, col + 1, index);
int d = DFS(board, row, col - 1, index);
if (r == -1 || l == -1 || u == -1 || d == -1)
{
if (Collect == -1) {
Collect = Find(index);
}
Merge(Collect, row * board[0].Length + col);
}
Merge(index, row * board[0].Length + col);
return 0;
}
private void Init(int n)
{
Father = new int[n];
Rank = new int[n];
for (int i = 0; i < n; i++)
{
Father[i] = i;
Rank[i] = 1;
}
}
public int Find(int x)
{
return x == Father[x] ? x : (Father[x] = Find(Father[x]));
}
public void Merge(int i, int j)
{
int x = Find(i);
int y = Find(j);
if (Rank[x] <= Rank[y])
Father[x] = y;
else
Father[y] = x;
if (Rank[x] == Rank[y] && x != y)
{
Rank[y]++;
Father[x] = y;
}
}
}