并查集是一个支持集合快速合并的结构
对外提供两个方法:
(1)合并操作 将两个结点所在的集合合并 void union(a,b)
(2)查询两个结点是否属于同一个集合 bool find(a,b),或者说查询某个结点是否属于某个集合
如果一个结点a连接到了结点b,我们就说a的父结点就是b
我们用一个数组记录每个结点的父节点
find
和union
操作的时间复杂度为O(logn)
,n为节点总数。
public class UnionFind
{
private int count; // 连通分量的个数
private int[] parent; // 记录每个节点的父节点,父节点为自身的是根节点
private int[] size; // 记录每个连通分量的大小
public UnionFind(int n)
{
this.count = n;
this.parent = new int[n];
this.size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
public int count() {
return this.count;
}
// 查询所属连通分量
public int find(int p) {
int root = p;
while (root != parent[root]) {
root = parent[root];
}
// 路径压缩
while (p != root) {
int next = parent[p];
parent[p] = root;
p = next;
}
return root;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
// 合并所属的两个连通分量
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) return;
// 总是将较小的连通分量连接到较大的连通分量上
if (size[rootP] < size[rootQ]) {
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
} else {
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
}
count--; // 合并两个连通分量后,连通分量数量减1
}
}
1.将两个集合合并
2.询问两个元素是否在一个集合里面
基本原理:每个集合用一棵树来表示,树根的编号就是整个集合的编号,p[x]表示x的父节点
树根的话p[x]=x
如何求x的集合编号::只要x不是树根,就一直找父节点,直到找到根节点
while(p[x]!=x)
{
x=p[x];
}
如何合并两个集合:
将一个集合的根节点成为另一个集合根节点的孩子节点
例如;p[x]是x的集合编号,p[y]是y的集合,p[x]=y,也就是说x的父节点变成y
一共有n个数,编号分别是1~n
最开始每个数各自在一个集合中,
并查集代码最核心的操作就是find函数
public class Main6
{
public static int find(int x)//返回x所在集合的编号,或者说返回x的祖宗结点
{
if(p[x]!=x)
{
p[x]=find(p[x]);
}
return p[x];
}
public static void main(String[] args)
{
int[] p = new int[100010];
int n = 100;
//p[i]=i,表示这个结点自己本身就是根节点
for (int i = 1; i <= n; i++) {
p[i] = i;
}
int a = 5, b = 8;
//将a,b两个集合所在的集合合并,如果本身这两个元素就在一个集合,就不用合并
p[find(a)] = find(b);
//find函数是找出一个节点的根节点,这句代码的含义就是让a的根节点成为b根节点的孩子节点
//查询两个数字是不是在同一个集合
if(find(a)==find(b)) System.out.println("YES");
}
}
也就是说亲戚关系具有传递性
最后你要查询两个人是否有亲戚关系,只需查询最后两人的集合号是否相同即可
典型并查集题目:
Leetcode 547 省份数量
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
示例1:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
示例2
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3
将同一省份的城市都加入到同一个集合,最终有多少个省份就会有多少个不相交集合
这个例子中答案是2,三个元素会形成两个不相交的集合
class Solution
{
public int findCircleNum(int[][] isConnected)
{
int n = isConnected.length;
UnionFind uf = new UnionFind(n);
for (int i = 0; i < n; i++)
{
for (int j = i + 1; j < n; j++)
{
if (isConnected[i][j] == 1)
{
uf.union(i, j);
}
}
}
return uf.count();
}
}
class UnionFind {
private int count; // 连通分量的个数
private int[] parent; // 记录每个节点的父节点,父节点为自身的是根节点
private int[] size; // 记录每个连通分量的大小
public UnionFind(int n) {
this.count = n;
this.parent = new int[n];
this.size = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
public int count() {
return this.count;
}
// 查询所属连通分量
public int find(int p) {
int root = p;
while (root != parent[root]) {
root = parent[root];
}
// 路径压缩
while (p != root) {
int next = parent[p];
parent[p] = root;
p = next;
}
return root;
}
public boolean connected(int p, int q) {
return find(p) == find(q);
}
// 合并所属的两个连通分量
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
if (rootP == rootQ) return;
// 总是将较小的连通分量连接到较大的连通分量上
if (size[rootP] < size[rootQ]) {
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
} else {
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
}
count--;
}
}