并查集的实现与应用(力扣)
并查集的实现
package com.caoii;/*
*@program:labu-pratice-study
*@package:com.caoii
*@author: Alan
*@Time: 2024/4/12 21:53
*@description: 并查集的实现
*/
public class UnionFind {
// 记录连通分量
private int count;
// 节点X的父节点 是 parent[x]
private int[] parent;
//使用一个size数组 记录每棵树包含的节点数目
//来让两个树合并的时候尽量让小的树接到大的树的下面
//这样每次使用find向上找根节点的复杂度能相对减少
//private int[] size;
// 通过改造find函数 可将每个树都变成 真正高度为2
// 即 每个子节点都直接与最高根节点相连的样式
// 故size就不必再使用了
// 构造函数 n 为 图的节点数目
public UnionFind(int n) {
this.count = n;
// 一开始互不连通 则 连通分量的数目等于节点数目
parent = 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);
// find方法获取两个树的最高根节点
if (rootP == rootQ) {
// 两棵树已经连通则最高根节点一定相同
// 不需要 再次链接 直接退出方法
return;
}
/*
改进find之后不用size了
//两棵树最高根节点不同的时候:
//两棵树合并为一棵树 设置P的根节点为Q
if (size[rootP] > size[rootQ]) {
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
// P树更高 则 把 Q树接在P树下面,让Q的父节点指针指向P
// P的高度增加
// 实际上此时所说的高度不是真的高度而是该树的全部节点个数
} else {
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
}*/
//两棵树最高根节点不同的时候:
//两棵树合并为一棵树 设置P的根节点为Q
parent[rootP] = rootQ;
count--;
// 两个分量合二为一 分量数目减一
}
/*返回某个节点X的最高根节点*/
public int find(int x) {
/*传统方法 逐次向上寻找最高根节点
while (parent[x] != x) {
x = parent[x];
}
return x;
// 若根节点指针指向自己,则返回自己
// 若根节点指针没有指向自己,则把根节点指针指向的元素赋值给X 并循环判断
// 若 3-->5-->6 则 X=3时执行:x=5 ——> 5!=parent[5]=6 ——> x=6 ——> 6=parent[6]=6 ——> return 6*/
// 改进方法 在寻找最高根节点的过程中
// 将树构造为 真实高度为2 所有子节点都与根节点直接相连的形式:
if (parent[x] != x) {
// x的根节点 不是 x 自己
// 则x 存在根节点
parent[x] = find(parent[x]);
// 递归运算
// 最后:
}
return parent[x];
// 递归出口: 递归到最高层根节点 此时 x==parent[x] 所以返回x
// 则 次高层处节点为y, parent[y] = find(parent[y]) = x
// 即次高层处节点的父指针指向最高节点
// 同理 次次高层处节点为z, parent[z] = find(parent[z]) = find(y) = parent[y] = x
// 即次次高层处节点的父指针指向最高节点x
// 以此类推
// 最后结果就是所有子节点都直接与根节点直接相连 树的真是高度为2
}
/*判断 p 和 q 是否连通*/
public boolean connected(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
return rootP == rootQ;
// 若两个树的最高节点相同则p与q连通
}
/*返回图中有多少个连通分量*/
public int count() {
return count;
}
}
力扣323 130 990
package com.caoii;/*
*@program:labu-pratice-study
*@package:com.caoii
*@author: Alan
*@Time: 2024/4/12 23:25
*@description: 并查集相关题目测试
*/
import org.junit.jupiter.api.Test;
public class UFTest {
/*
* 力扣323题
* 给你输入一个包含 n 个节点的图,用一个整数 n 和一个数组 edges 表示,
* 其中 edges[i][j] = [ai, bi] 表示图中节点 ai 和 bi 之间有一条边。
* 请你计算这幅图的连通分量个数。*/
public int countComponents(int n, int[][] enges) {
int count = n;
// 初始时 连通分量个数等于节点个数
UnionFind unionFind = new UnionFind(n);
for (int[] e : enges) {
unionFind.union(e[0], e[1]);
// 链接 ai 与 bi
}
count = unionFind.count();
// 返回 完成全部链接后 最少的连通分量个数
return count;
}
/*测试323题*/
@Test
public void test_01() {
int n = 11;
// un中的parent[] 为 0-10
int[][] enges = {
{0, 6}, {6, 0},
{1, 2}, {1, 3}, {2, 1}, {2, 3}, {2, 4},
{3, 1}, {3, 2}, {3, 5}, {4, 2},
{6, 7}, {7, 6},
{8, 9}, {9, 8}, {9, 10}, {10, 9}
};
System.out.print("该无向图的连通分量个数为: " + countComponents(n, enges));
}
/*力扣130
给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,
找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充
被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。
任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。
如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。*/
public void solve(char[][] board) {
// 用并查集解决
// 将二维数组映射为一维数组
if (board.length == 0) {
return;
}
int m = board.length;
//行数
int n = board[0].length;
//列数
// board[a][b] ==> temp[x*n+y] x从0-(board.length-1)
// temp的index从0-(lengthAll-1)
UnionFind unionFind = new UnionFind(m * n + 1);
// 用一个一维数组设置并查集对象
// 多设置一个空位存储一个虚构的根节点
int dummy = m * n; // 此根节点的索引值为m*n
// 将首列与末列的O与dummy相连
for (int i = 0; i < m; i++) {
if (board[i][0] == 'O') {
unionFind.union((i * n + 0), dummy);
}
if (board[i][n - 1] == 'O') {
unionFind.union((i * n + n - 1), dummy);
}
}
// 将首行与末行的O与dummy 相连
for (int j = 0; j < n; j++) {
if (board[0][j] == 'O') {
unionFind.union((0 * n + j), dummy);
}
if (board[m - 1][j] == 'O') {
unionFind.union((m - 1) * n + j, dummy);
}
}
// 设置方向数组d
int[][] d = new int[][]{
{0, 1},
{1, 0},
{0, -1},
{-1, 0}
};
// 三层循环结束后 与dummy链接到同一个分量的O应该都不被转变为X
for (int i = 1; i < m - 1; i++) {
for (int j = 1; j < n - 1; j++) {
if (board[i][j] == 'O') {
for (int k = 0; k < 4; k++) {
int x = i + d[k][0];
int y = j + d[k][1];
// i 与 j 分别 加(0,1)(1,0)(0,-1)(-1,0)
// 向四个方向探索是否存在O
if (board[x][y] == 'O') {
unionFind.union((x * n + y), i * n + j);
// 四个方向任意一个有相邻的就合并分量
}
}
}
}
}
// 将非dummy集合的O都设置为X
for (int i = 1; i < m - 1; i++) {
for (int j = 1; j < n - 1; j++) {
if (board[i][j] == 'O') {
if (!unionFind.connected(dummy, i * n + j)) {
board[i][j] = 'X';
}
}
}
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
System.out.print(board[i][j] + " ");
}
System.out.println();
}
}
@Test
public void test_02() {
char[][] board = new char[][]{
{'X', 'X', 'X', 'X'},
{'X', 'O', 'O', 'X'},
{'X', 'X', 'O', 'X'},
{'X', 'O', 'X', 'X'}
};
solve(board);
}
/*
力扣 990 题
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,
并采用两种不同的形式之一:"a==b" 或 "a!=b"。
在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。
只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false
示例 1:
输入:["a==b","b!=a"]
输出:false
解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。
没有办法分配变量同时满足这两个方程。
示例 2:
输入:["b==a","a==b"]
输出:true
解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。
示例 3:
输入:["a==b","b==c","a==c"]
输出:true
示例 4:
输入:["a==b","b!=c","c==a"]
输出:false
示例 5:
输入:["c==c","b==d","x!=z"]
输出:true
*/
public boolean equationsPossible(String[] equations) {
UnionFind unionFind = new UnionFind(26);
for (int i = 0; i < equations.length; i++) {
if (equations[i].charAt(1) == equations[i].charAt(2)) {
// == 情况
unionFind.union((int) (equations[i].charAt(0) - 'a'), (int) (equations[i].charAt(3) - 'a'));
}
}
for (int i = 0; i < equations.length; i++) {
if (equations[i].charAt(1) != equations[i].charAt(2)) {
// != 情况
if (unionFind.connected((int) (equations[i].charAt(0) - 'a'), (int) (equations[i].charAt(3) - 'a'))) {
// 如果 在不等于的条件下 发现 他俩已经放入同一个集合了
return false;
}
}
}
return true;
}
@Test
public void test_03() {
String[] equations = {"a==b", "b!=c", "a==c"};
System.out.println(equationsPossible(equations));
}
}