欢迎阅读、点赞、转发、订阅,你的举手之间,我的动力源泉。
{:width=“400px”}
定义
并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合( D i s j o i n t Disjoint Disjoint S e t s Sets Sets)的合并及查询问题。常常在使用中以森林来表示。
概念
- 合并( U n i o n Union Union):把两个不相交的集合合并为一个集合
- 查询( F i n d Find Find):查询两个元素是否在同一个集合中
伪代码
class UnionFindSet:
def UnionFindSet(n):
parents = [0,1...n] # 记录每个元素的parent即根节点 先将它们的父节点设为自己
ranks =[0,0...0] # 记录节点的rank值
# 如下图 递归版本 路径压缩(Path Compression)
# 如果当前的x不是其父节点,就找到当前x的父节点的根节点(find(parents[x])) 并将这个值赋值给x的父节点
def find(x):
if ( x !=parents[x]): # 注意这里的if
parents[x] = find(parents[x])
return parents[x]
# 如下图 根据Rank来合并(Union by Rank)
def union(x,y):
rootX = find(x) # 找到x的根节点rootX
rootY = find(y) # 找到y的根节点rootY
#取rank值小的那个挂到大的那个节点下面,此时两个根节点的rank值并没有发生变化,还是原来的值
if(ranks[rootX]>ranks[rootY]): parents[rootY] = rootX
if(ranks[rootX]<ranks[rootY]): parents[rootX] = rootY
# 当两个rank值相等时,随便选择一个根节点挂到另外一个跟节点上,但是被挂的那个根节点的rank值需要+1
if(ranks[rootX] == ranks[rootY] ):
parents[rootY] = rootX
ranks[rootX]++
解释
- p a r e n t s [ x ] parents[x] parents[x]表示的是 x x x的父节点,初始化时,有一些初始化写法是 p a r e n t s [ x ] parents[x] parents[x]= x x x,表示将 x x x的父节点指向自己
非递归版本find(x),如下图
def find(x):
rootX = x # 找到x的根节点
while (rootX!=parents[rootX]):
rootX = parents[rootX]
curr = x # 准备一个curr变量
while (curr!=rootX):
next = parents[curr] # 暂存curr的父节点
parents[curr] = rootX # 将curr节点的父节点设置为rootX
curr = next # curr节点调到下个节点
return rootX
{:width=“400px”}
根据Rank来合并( U n i o n Union Union b y by by R a n k Rank Rank)
{:width=“400px”}
路径压缩( P a t h Path Path C o m p r e s s i o n Compression Compression)
{:width=“400px”}
应用
1.被围绕的区域
{:width=“400px”}
思路
- 准备一个并查集 U n i o n F i n d S e t UnionFindSet UnionFindSet,初始化时,多一个节点设置为哑结点 d u m m y dummy dummy
- 因为是二维矩阵的缘故,可以将其坐标转化为一维矩阵, i ∗ 列 数 + j i * 列数 + j i∗列数+j
- 边缘处的 O O O直接与 d u m m y dummy dummy 进行合并
- 非边缘的 O O O则需要上下左右四个方向探测,进行合并
- 遍历,当发现当前节点与 d u m m y dummy dummy节点的根节点相同,即联通的话,这个点维持不变
并查集
static class UnionFindSet {
int[] parents;
int[] ranks;
public UnionFindSet(int n) {
parents = new int[n];
ranks = new int[n];
for (int i = 0; i < n; i++) {
parents[i] = i;
}
}
public int find(int x) {
if (x != parents[x]) {
parents[x] = find(parents[x]);
}
System.out.println(x + ":" + parents[x]);
return parents[x];
}
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) return;
if (ranks[rootX] > ranks[rootY]) parents[rootY] = rootX;
if (ranks[rootX] < ranks[rootY]) parents[rootX] = rootY;
if (ranks[rootX] == ranks[rootY]) {
parents[rootY] = rootX;
ranks[rootX]++;
}
}
}
主体代码
int m, n;
int[][] directions = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}};
public void solve(char[][] board) {
if (board == null || board.length == 0) return;
m = board.length;
n = board[0].length;
int initValue = m * n + 1;
UnionFindSet unionFindSet = new UnionFindSet(initValue);
int dummy = m * n;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 'O') {
if (i == 0 || i == m - 1 || j == 0 || j == n - 1) {
unionFindSet.union(node(i, j), dummy);
} else {
for (int k = 0; k < directions.length; k++) {
int nextI = i + directions[k][0];
int nextJ = j + directions[k][1];
if ((nextI > 0 || nextI < m || nextJ > 0 || nextJ < n) && board[nextI][nextJ] == 'O') {
unionFindSet.union(node(i, j), node(nextI, nextJ));
}
}
}
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (unionFindSet.find(node(i, j)) == unionFindSet.find(dummy)) {
board[i][j] = 'O';
} else {
board[i][j] = 'X';
}
}
}
}
public int node(int i, int j) {
return i * n + j;
}
2.冗余连接
图传上来就没了,有毒,684题:冗余连接
思路
- 判断节点第一次出现环的边 e d g e edge edge进行返回,如下图,当 1 1 1的根节点是 4 4 4的时候,从 1 − > 2 − > 3 − > 4 1->2->3->4 1−>2−>3−>4出现一条路径,大概 [ 1 , 4 ] [1,4] [1,4]这个 e d g e edge edge进来后,发现 1 1 1可以直接指向 4 4 4,这时候出现了环,这条边是冗余边
{:width=“400px”}
int[] parents;
public int[] findRedundantConnection(int[][] edges) {
if (edges == null || edges.length == 0) return new int[]{0, 0};
int n = edges.length + 1; //注意此处下标多放一个
init(n);
for (int[] edge : edges) {
int x = edge[0], y = edge[1];
if ((!union(x, y))) {//第二次出现了联通的边时,表示已经找到了
return edge;
}
}
return new int[]{0, 0};
}
//初始化parents
public void init(int n) {
parents = new int[n];
for (int i = 0; i < n; i++) {
parents[i] = i;
}
}
//递归版路径压缩,找到x的根节点
public int find(int x) {
if (x != parents[x]) {
parents[x] = find(parents[x]);
}
return parents[x];
}
//改写union方法,第一次当x与y没有联通时,将其设置联通关系,返回ture
//第二次x和y的跟节点发现一致时,他们已经联通了,返回false
public boolean union(int x, int y) {
int rootX = find(x), rootY = find(y);
if (rootX == rootY) return false;
parents[rootX] = rootY;
return true;
}
番外
//非递归版路径压缩
public int find(int x) {
int rootX = x;
while (rootX != parents[rootX]) {
rootX = parents[rootX];
}
int curr = x;
while (curr != rootX) {
int next = parents[curr];
parents[curr] = rootX;
curr = next;
}
return rootX;
}
推荐阅读
推荐阅读
题号 | 链接 |
---|---|
130/684 | 一文掌握并查集算法 |
一文掌握Morris遍历算法 | |
200/- | 岛屿问题之岛屿的数量[Eighty-eight Butterfly] |
493/695 | 岛屿问题之岛屿的周长面积[Morpho Cypris Aphrodite] |
130 | 岛屿问题之被围绕的区域[Cicada] |
-/- | 岛屿问题之不同岛屿的数量[Monarch Butterfly] |
Reference
- 参考资料
- 视频资料