之前很少去刷并查集相关的算法题,今天在LeetCode上遇到了,就此对并查集进行一定深度的了解总结。
首先了解一下什么是并查集:并查集就是建立在对不相交集合进行的两种基本操作的基础之上的。操作之一就是:检索某元素属于哪个集合;操作二就是如果两个集合不属于同一个集合则合并这两个集合。查并集这种结构显然可以用链表或森林实现,显然用链表进行查询其时间复杂度应该是O(n)级别的,而使用森林进行查询如果处理的好的话其时间复杂度应该是O(logn)。对于用森林来实现并查集,就是"用树根来标识一个集合"。
并查集存在两个很关键的优化操作:第一:让深度小的数成为深度大的树的子树,这个优化成为启发式合并,这样做之后树的深度为O(logn);第二:找到u所在的树根v以后,把从u到v的路径上所有点的父亲都设置为v,这个优化称作路径压缩。
启发式合并对启发式函数的选取很重要,一般可以用树的深度作为启发函数值,但由于路径压缩优化,树的深度是在不断变化的。这时候便可以引入一个秩,应该就是一棵树收取与其等秩的树的次数,因为合并的最底层操作就是两个独立元素的合并,所以rank越大,树中的元素越多,显然rank的初值都是0。
- 按秩合并的基本思想是使包含较少节点的树的根指向包含较多结点的树的根,而这个树的大小可以抽象为树的高度
- 路径压缩的基本思想就是每个节点直接指向根节点,路径压缩并不改变结点的秩。
下面通过LeetCode上的1319题:连通网络的操作次数来对并查集有更好的了解:
用以太网线缆将 n 台计算机连接成一个网络,计算机的编号从 0 到 n-1。线缆用 connections 表示,其中 connections[i] = [a, b] 连接了计算机 a 和 b。
网络中的任何一台计算机都可以通过网络直接或者间接访问同一个网络中其他任意一台计算机。
给你这个计算机网络的初始布线 connections,你可以拔开任意两台直连计算机之间的线缆,并用它连接一对未直连的计算机。请你计算并返回使所有计算机都连通所需的最少操作次数。如果不可能,则返回 -1 。
示例1:
示例2:
示例3:
示例4:
提示:
public class Solution{
public static void main(String[] args) {
System.out.println(makeConnected(8,
new int[][] {{0,6},{2,3},{2,6},{2,7},{1,7},{2,4},{3,5},{0,2}}));
}
List<Integer>[] edges;
boolean[] used;
//最少操作次数,
public static int makeConnected(int n, int[][] connections) {
if(connections.length<n-1)return -1;
UnionFind uf = new UnionFind(n);
for(int[] conn : connections) {
uf.unite(conn[0], conn[1]);
}
return uf.setCount-1;
}
}
//查并集模板
class UnionFind{
int[] parent;//记录节点的根
int[] size;//记录根节点的深度
int n;//记录多余的连接数
int setCount;//连通分量个数
public UnionFind(int n) {
this.n = n;
this.parent = new int[n];
this.size = new int[n];
this.setCount = n;
for(int i = 0; i < n; ++i) {
parent[i] = i;//初始化自身连接
}
}
//压缩方式:直接指向根节点
public int findset(int x) {
return parent[x] == x ? x : (parent[x] = findset(parent[x]));
}
//判断是否两个顶点需要连通
public boolean unite(int x, int y) {
x = findset(x);
y = findset(y);
if(x == y) {
return false;
}
if(size[x] < size[y]) {
int tmp = x;
x = y;
y = tmp;
}
parent[y] = x;
size[x] += size[y];
--setCount;
return true;
}
public boolean connected(int x, int y) {
x = findset(x);
y = findset(y);
return x == y;
}
}
以上是并查集算法的解题步骤
该题也可以用深度优先搜索算法来解决:
class Solution {
List<Integer>[] edges;
boolean[] used;
//最少操作次数,
public int makeConnected(int n, int[][] connections) {
if(connections.length<n-1) {
return -1;
}
edges=new ArrayList[n];
for(int i=0;i<n;i++)
edges[i]=new ArrayList<>();
for(int[] conn:connections) {
edges[conn[0]].add(conn[1]);
edges[conn[1]].add(conn[0]);
}
used=new boolean[n];
int res=0;
for(int i=0;i<n;i++) {
if(!used[i]) {
dfs(i);
res++;
}
}
return res-1;
}
private void dfs(int n) {
used[n]=true;
for(int u:edges[n]) {
if(!used[u]) {
dfs(u);
}
}
}
}