⭐️前言⭐️
本篇文章主要介绍与并查集相关的题目。
🍉欢迎点赞 👍 收藏 ⭐留言评论 📝私信必回哟😁
🍉博主将持续更新学习记录收获,友友们有任何问题可以在评论区留言
🍉博客中涉及源码及博主日常练习代码均已上传GitHub
📍内容导读📍
- 🍅并查集
- 🍅map实现并查集
- 🍅数组实现并查集
- 🍅省份数量
- 🍅岛问题
🍅并查集
并查集就是用来对集合进行合并和查询操作的一种数据结构。
合并就是将两个不相交的集合合并成一个集合。
查询就是查询两个元素是否属于同一个集合。
🍅map实现并查集
每个集合都以类似链表的形式(其实都是map的key->value指向)存在,并且链表的代表节点(尾节点指向自己)
判断两个元素是否属于同一个集合,就判断这两个元素对应的代表节点是否是同一个节点即可。
如果不是一个集合想要合并,从优化的角度来做,就让长度小的链表代表节点,指向长度长的链表代表节点。
以下是代码实现,通过两张表来完成这样的一个数据结构:
ancestor表存储节点及对应的代表节点
size表存储代表节点对应集合的元素个数
public class UnionFind_Code {
public static class UnionFind<V> {
public HashMap<V,V> represent;
public HashMap<V,Integer> size;
/*
初始化时,每个节点就是单独一个集合,代表节点就是自己
*/
public UnionFind(List<V> values) {
represent=new HashMap<>();
size=new HashMap<>();
for(V cur:values) {
represent.put(cur,cur);
size.put(cur,1);
}
}
/*
给一个节点,往上到不能再往上,把代表返回(借助栈实现扁平优化,再查询时更快)
*/
public V findRepresent(V cur) {
Stack<V> path=new Stack<>();
while (cur!=represent.get(cur)) {
// 当cur不是代表节点时,记录路径并继续往上
path.push(cur);
cur=represent.get(cur);
}
while (!path.isEmpty()) {
represent.put(path.pop(),cur);
}
return cur;
}
/*
判断是否是同一个集合
*/
public boolean isSameSet(V a,V b) {
return represent.get(a)==represent.get(b);
}
/*
合并集合
*/
public void union(V a,V b) {
V aRepresent=findRepresent(a);
V bRepresent=findRepresent(b);
if(aRepresent!=bRepresent) {
int aSize=size.get(aRepresent);
int bSize=size.get(bRepresent);
if(aSize>=bSize) {
represent.put(bRepresent,aRepresent);
size.put(aRepresent,aSize+bSize);
size.remove(bRepresent);
}else {
represent.put(aRepresent,bRepresent);
size.put(bRepresent,aSize+bSize);
size.remove(aRepresent);
}
}
}
public int sets() {
return size.size();
}
}
}
🍅数组实现并查集
题目:
链接:https://www.nowcoder.com/questionTerminal/e7ed657974934a30b2010046536a5372
来源:牛客网
给定一个没有重复值的整形数组arr,初始时认为arr中每一个数各自都是一个单独的集合。请设计一种叫UnionFind的结构,并提供以下两个操作。
- boolean isSameSet(int a, int b): 查询a和b这两个数是否属于一个集合
- void union(int a, int b): 把a所在的集合与b所在的集合合并在一起,原本两个集合各自的元素以后都算作同一个集合
[要求]
如果调用isSameSet和union的总次数逼近或超过O(N),请做到单次调用isSameSet或union方法的平均时间复杂度为O(1)
public class Code_UnionFind {
// 最大测试数据量
public static int MAXN = 1000001;
// 记录i节点的代表节点
public static int[] represent=new int[MAXN];
// 记录代表节点所在集合的元素个数
public static int[] size=new int[MAXN];
// 辅助数组用于实现扁平优化
public static int[] help=new int[MAXN];
/*
初始化并查集
*/
public static void init(int n) {
for (int i = 0; i < n; i++) {
represent[i]=i;
size[i]=1;
}
}
/*
寻找集合代表点
*/
public static int find(int i) {
int hi=0;
// 当i的代表节点不是i自己时,继续往上找代表节点,并记录路径所有节点
while (i!=represent[i]) {
help[hi++]=i;
i=represent[i];
}
// 扁平优化,把路径上所有节点的代表节点都设为集合的代表节点
while (hi-->=0) {
represent[help[hi]]=i;
}
return i;
}
/*
查询x和y是不是一个集合
*/
public static boolean isSameSet(int x,int y) {
return find(x)==find(y);
}
/*
合并集合
*/
public static void union(int x,int y) {
int fx=find(x);
int fy=find(y);
if(fx!=fy) {
if(size[fx]>=size[fy]) {
size[fx]+=size[fy];
represent[fy]=fx;
}else {
size[fy]+=size[fx];
represent[fx]=fy;
}
}
}
}
🍅省份数量
Leetcode题目:https://leetcode.cn/problems/number-of-provinces/
题解思路:
利用并查集数据结构,直接或者间接相连的城市,都放入一个集合中,最后求一共有几个集合,就是有几个省份。
遍历矩阵完成集合的合并,如果isConnected[i][j]=1,就放入一个集合中,遍历时j=i+1(因为1和3连接,3和1肯定也连接,没有必要再重复操作)。
代码实现:
class Solution {
public int findCircleNum(int[][] isConnected) {
int N=isConnected.length;
UnionFind union=new UnionFind(N);
for(int i=0;i<N;i++) {
for(int j=i+1;j<N;j++) {
if(isConnected[i][j]==1) {
union.union(i,j);
}
}
}
return union.sets();
}
public class UnionFind {
private int[] represent;
private int[] size;
private int[] help;
private int sets;
public UnionFind(int N) {
represent=new int[N];
size=new int[N];
help=new int[N];
sets=N;
for(int i=0;i<N;i++) {
represent[i]=i;
size[i]=1;
}
}
private int find(int i) {
int hi=0;
while(i!=represent[i]) {
help[hi++]=i;
i=represent[i];
}
while(hi-->0) {
represent[help[hi]]=i;
}
return i;
}
public void union(int i,int j) {
int f1=find(i);
int f2=find(j);
if(f1!=f2) {
if(size[f1]>=size[f2]) {
size[f1]+=size[f2];
represent[f2]=f1;
}else {
size[f2]+=size[f1];
represent[f1]=f2;
}
sets--;
}
}
public int sets() {
return sets;
}
}
}
🍅岛问题
题目:
给定一个二维数组matrix,里面的值不是1就是0,上、下、左、右相邻的1认为是一片岛,返回matrix中岛的数量
(递归解法 + 并查集解法1 + 并查集解法2)
1、递归解法
题解思路:
遇到1后,就把该位置上下左右所以能与之相接的位置都变为2,然后通过递归感染的方式,将所有独立范围内的1感染完成,岛屿数+1,当再次遇到1时再重复相同的操作,最后返回岛屿数量。
(该方法也是时间复杂度最优的解法,O(N*M))
代码实现:
class Solution {
public static int numIslands(char[][] board) {
int islands=0;
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if(board[i][j]=='1') {
islands++;
infect(board,i,j);
}
}
}
return islands;
}
// 从(i,j)位置出发,把所有连成一片的"1"字符,变成2
private static void infect(char[][] board, int i, int j) {
if(i<0||i== board.length||j<0||j==board[0].length||board[i][j]!='1') {
return;
}
board[i][j]=2;
infect(board,i-1,j);
infect(board,i+1,j);
infect(board,i,j-1);
infect(board,i,j+1);
}
}
2、并查集解法1
题解思路:
遍历棋盘,把所有与之相连的1合并到一个集合中,最后返回集合数就是岛屿数。
注意:
1.每个位置如果是1,则只需要判断其左边或者上边是否为1即可,如果是就合并
2.因为棋盘中存储都是’1’,合并的话没有办法区分,所以通过dot类来辅助完成。
代码实现:
class Solution {
public static int numIslands(char[][] board) {
int row=board.length;
int col=board[0].length;
Dot[][] dots=new Dot[row][col];
List<Dot> dotList=new ArrayList<>();
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if(board[i][j]=='1') {
dots[i][j]=new Dot();
dotList.add(dots[i][j]);
}
}
}
UnionFind1 unionFind1=new UnionFind1<>(dotList);
for (int j = 1; j < col; j++) {
if(board[0][j-1]=='1'&&board[0][j]=='1') {
unionFind1.union(dots[0][j-1],dots[0][j]);
}
}
for(int i=1;i<row;i++) {
if(board[i-1][0]=='1'&&board[i][0]=='1') {
unionFind1.union(dots[i-1][0],dots[i][0]);
}
}
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if(board[i][j]=='1') {
if(board[i][j-1]=='1') {
unionFind1.union(dots[i][j-1],dots[i][j]);
}
if(board[i-1][j]=='1') {
unionFind1.union(dots[i-1][j],dots[i][j]);
}
}
}
}
return unionFind1.size();
}
public static class Dot {
}
public static class UnionFind1<V> {
public HashMap<V,V> represents;
public HashMap<V,Integer> sizeMap;
public UnionFind1(List<V> values) {
represents=new HashMap<>();
sizeMap=new HashMap<>();
for(V cur:values) {
represents.put(cur,cur);
sizeMap.put(cur,1);
}
}
private V findRepresent(V cur) {
Stack<V> path=new Stack<>();
while (cur!=represents.get(cur)) {
path.push(cur);
cur=represents.get(cur);
}
while (!path.isEmpty()) {
represents.put(path.pop(),cur);
}
return cur;
}
public void union(V a,V b) {
V aRepresent=findRepresent(a);
V bRepresent=findRepresent(b);
if(aRepresent!=bRepresent) {
int aSize=sizeMap.get(aRepresent);
int bSize=sizeMap.get(bRepresent);
if(aSize>=bSize) {
represents.put(bRepresent,aRepresent);
sizeMap.put(aRepresent,aSize+bSize);
sizeMap.remove(bRepresent);
}else {
represents.put(aRepresent,bRepresent);
sizeMap.put(bRepresent,aSize+bSize);
sizeMap.remove(aRepresent);
}
}
}
public int size() {
return sizeMap.size();
}
}
}
3、并查集解法2
题解思路:
该方法其实就是把并查集通过数组的方式来实现,而不再借助map,并且通过将棋盘上的点映射到数组中的位置,便减少了辅助类dot的使用。
最后通过规则合并棋盘中的值,对应合并了数组中的数,返回并查集的集合数即为最终结果。
代码实现:
class Solution {
public int numIslands(char[][] board) {
int row=board.length;
int col=board[0].length;
UnionFind2 unionFind2=new UnionFind2(board);
for (int j = 1; j < col; j++) {
if(board[0][j-1]=='1'&&board[0][j]=='1') {
unionFind2.union(0,j-1,0,j);
}
}
for (int i = 1; i < row; i++) {
if(board[i-1][0]=='1'&&board[i][0]=='1') {
unionFind2.union(i-1,0,i,0);
}
}
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if(board[i][j]=='1') {
if(board[i-1][j]=='1') {
unionFind2.union(i-1,j,i,j);
}
if(board[i][j-1]=='1') {
unionFind2.union(i,j-1,i,j);
}
}
}
}
return unionFind2.sets;
}
public static class UnionFind2 {
private int[] represent;
private int[] size;
private int[] help;
private int col;
private int sets;
public UnionFind2(char[][] board) {
col=board[0].length;
sets=0;
int row= board.length;
int len=row*col;
represent=new int[len];
size=new int[len];
help=new int[len];
for (int r = 0; r < row; r++) {
for (int c = 0; c < col; c++) {
if(board[r][c]=='1') {
int i=index(r,c);
represent[i]=i;
size[i]=i;
sets++;
}
}
}
}
private int index(int r,int c) {
return r*col+c;
}
private int find(int i) {
int hi=0;
while (i!=represent[i]) {
help[hi++]=i;
i=represent[i];
}
while (hi-->0) {
represent[help[hi]]=i;
}
return i;
}
public void union(int r1,int c1,int r2,int c2) {
int i1=index(r1,c1);
int i2=index(r2,c2);
int f1=find(i1);
int f2=find(i2);
if(f1!=f2) {
if(size[f1]>=size[f2]) {
size[f1]+=size[f2];
represent[f2]=f1;
}else {
size[f2]+=size[f1];
represent[f1]=f2;
}
sets--;
}
}
}
}
⭐️最后的话⭐️
总结不易,希望uu们不要吝啬你们的👍哟(^U^)ノ~YO!!如有问题,欢迎评论区批评指正😁