参考资料
关键术语解释:
连通分量:在一张含有多张图的网状结构中的,互相独立的图的个数。
union(q,p):将q和p连通起来,
find(p):返回p的根节点
connect(q,p):判断p和q是否连通
算法的关键点有 3 个:
1、用 parent 数组记录每个节点的父节点,相当于指向父节点的指针,所以 parent 数组内实际存储着一个森林(若干棵多叉树)。
2、用 size 数组记录着每棵树的重量,目的是让 union 后树依然拥有平衡性,而不会退化成链表,影响操作效率。
3、在 find 函数中进行路径压缩,保证任意树的高度保持在常数,使得 union 和 connected API 时间复杂度为 O(1)。
class UF {
// 连通分量个数,也就是独立的树的个数
private int count;
// 存储一棵树,定义x的父节点是parent[x],parent[x]的父节点是parent[parent[x]]
private int[] parent;
// 记录树的“重量”,平衡性优化
private int[] size;
public UF(int n) {
this.count = n;
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
/**
连接根节点p和q,将两个节点的根节点进行联通
**/
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ)
return;
// 小树接到大树下面,较平衡,否则极端情况下会变成链表,时间复杂度降至On
if (size[rootP] > size[rootQ]) {
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
} else {
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
}
count--;
}
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
/**
1.返回树的根节点
2.定义parent[x]== x 为树的根节点root
**/
private int find(int x) {
while (parent[x] != x) {
// 进行路径压缩,维持树的高度不超过3
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
public int count() {
return count;
}
}
例题应用
130. 被围绕的区域
思路一
DFS
- 从边界开始遍历,进行dfs,将与边界的O相连的标记为#,记录为不标记
- 重新遍历数组,将#还原成O,将O标记为X
代码
class Solution {
public void solve(char[][] board) {
int n = board.length;
int m = board[0].length;
for(int i=0;i<n;i++){
dfs(board,i,0);
dfs(board,i,m-1);
}
for(int j=0;j<m;j++){
dfs(board,0,j);
dfs(board,n-1,j);
}
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(board[i][j]=='O'){
board[i][j]='X';
}else if(board[i][j]=='#'){
board[i][j]='O';
}
}
}
}
private void dfs(char[][] board,int i,int j){
if(i<0||j<0||i>=board.length||j>=board[0].length||board[i][j]!='O'){
return;
}
board[i][j]='#';
dfs(board,i+1,j);
dfs(board,i-1,j);
dfs(board,i,j+1);
dfs(board,i,j-1);
}
}
思路二
并查集
- 边界节点的O不能被回收,我们首先将这些不能被回收的节点,利用并查集的
connect连接起来。 - 将所有O节点连接起来,建立一颗颗树
- 最后遍历二维数组,如果这个O节点跟边界节点没有联通,就替换成X
代码
class Solution {
void solve(char[][] board) {
if (board.length == 0) return;
int m = board.length;
int n = board[0].length;
// 给 dummy 留一个额外位置
UF uf = new UF(m * n + 1);
int dummy = m * n;
// 将首列和末列的 O 与 dummy 连通
for (int i = 0; i < m; i++) {
if (board[i][0] == 'O')
uf.union(i * n, dummy);
if (board[i][n - 1] == 'O')
uf.union(i * n + n - 1, dummy);
}
// 将首行和末行的 O 与 dummy 连通
for (int j = 0; j < n; j++) {
if (board[0][j] == 'O')
uf.union(j, dummy);
if (board[m - 1][j] == 'O')
uf.union(n * (m - 1) + j, dummy);
}
// 方向数组 d 是上下左右搜索的常用手法
int[][] d = new int[][]{{1,0}, {0,1}, {0,-1}, {-1,0}};
for (int i = 1; i < m - 1; i++)
for (int j = 1; j < n - 1; j++)
if (board[i][j] == 'O')
// 将此 O 与上下左右的 O 连通
for (int k = 0; k < 4; k++) {
int x = i + d[k][0];
int y = j + d[k][1];
if (board[x][y] == 'O')
uf.union(x * n + y, i * n + j);
}
// 所有不和 dummy 连通的 O,都要被替换
for (int i = 1; i < m - 1; i++)
for (int j = 1; j < n - 1; j++)
if (board[i][j]=='O'&&!uf.connected(dummy, i * n + j))
board[i][j] = 'X';
}
}
class UF {
// 连通分量个数,也就是独立的树的个数
private int count;
// 存储一棵树,定义x的父节点是parent[x],parent[x]的父节点是parent[parent[x]]
private int[] parent;
// 记录树的“重量”,平衡性优化
private int[] size;
public UF(int n) {
this.count = n;
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
/**
连接根节点p和q,将两个节点的根节点进行联通
**/
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ)
return;
// 小树接到大树下面,较平衡,否则极端情况下会变成链表,时间复杂度降至On
if (size[rootP] > size[rootQ]) {
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
} else {
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
}
count--;
}
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
/**
1.返回树的根节点
2.定义parent[x]== x 为树的根节点root
**/
private int find(int x) {
while (parent[x] != x) {
// 进行路径压缩,维持树的高度不超过3
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
public int count() {
return count;
}
}
等式方程的可满足性
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:“a==b” 或 “a!=b”。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false。
思路一
- 首先将a==b之类的等式单独拎出来,调用
union方法连通,形成并查集。 - 然后对于所有不等式,调用
connect方法判断是否连通,如果连通,说明不等式不成立。
代码
class Solution {
public boolean equationsPossible(String[] equations) {
UF uf = new UF(26);
for(int i=0;i<equations.length;i++){
if(!equations[i].contains("!")){
char c1 = equations[i].charAt(0);
char c2 = equations[i].charAt(equations[i].length()-1);
uf.union(c1-'a',c2-'a');
}
}
for(int i=0;i<equations.length;i++){
if(equations[i].contains("!")){
char c1 = equations[i].charAt(0);
char c2 = equations[i].charAt(equations[i].length()-1);
if(uf.connected(c1-'a',c2-'a')){
return false;
}
}
}
return true;
}
}
class UF {
// 连通分量个数,也就是独立的树的个数
private int count;
// 存储一棵树,定义x的父节点是parent[x],parent[x]的父节点是parent[parent[x]]
private int[] parent;
// 记录树的“重量”,平衡性优化
private int[] size;
public UF(int n) {
this.count = n;
parent = new int[n];
size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
/**
连接根节点p和q,将两个节点的根节点进行联通
**/
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ)
return;
// 小树接到大树下面,较平衡,否则极端情况下会变成链表,时间复杂度降至On
if (size[rootP] > size[rootQ]) {
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
} else {
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
}
count--;
}
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
}
/**
1.返回树的根节点
2.定义parent[x]== x 为树的根节点root
**/
private int find(int x) {
while (parent[x] != x) {
// 进行路径压缩,维持树的高度不超过3
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
public int count() {
return count;
}
}
这篇博客介绍了并查集(Union-Find)数据结构在解决两类问题中的应用:一是130.被围绕的区域问题,通过并查集实现DFS的优化;二是990.等式方程的可满足性问题,通过并查集判断变量之间的等价关系。并查集的关键在于路径压缩和平衡性优化,确保高效操作。在被围绕的区域问题中,边界节点的O与特殊节点dummy连通,避免被替换;而在等式求解中,先将相等的变量连通,再判断不等式是否矛盾。
665

被折叠的 条评论
为什么被折叠?



