并查集概念
案例引入:
假设现在有三个程序设计小分队,分别来自广东,广西和海南,其中广东小分队人员的编号为{0,6,7,8}
广西小分队人员编号为{1,4,9},海南小分队人员编号为{2,3,5},每一支队伍的队长编号分别为0、1、2.
那么这里就有三个集合,我们可以用树形图来表示:根节点为各队的队长
如果大家都是同一支队伍的话,肯定是相互认识的,所以上面的树形图还可以用来表示人际交往关系图,只要两个人不是在同一个集合里面,那么就互不认识。
根据上面的树形图我们可以使用数组的形式来表示,首先初始化,每一个元素标记为 -1:
根据树形图,数组可以变成下面:
解释一下:如果元素是负数的话,说明这个元素对应的下标是根结点,相反就是其索引的双亲结点,元素如果是负数,负数的绝对值表示包含自己在内的树一共有多少个结点(例如:-4,说明包含根节点在内的树一共有 4 个结点)
所以在初始化将元素设置为 -1,是将索引的双亲结点默认为自身。
在一些应用问题中,需要将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个单元素集
合,然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一个元素归属于那个集
合的运算。适合于描述这类问题的抽象数据类型称为并查集(union-find set)
并查集实现
并查集底层使用的是数组,该数据结构的思想和树是类似的。
下面代码的 x 表示的是数组的下标索引
提供构造方法
public UnionFindSet(int n) {
elem = new int[n];
Arrays.fill(elem, -1);
}
这里使用了Arrays.fill
方法,这个方法能将数组所有元素都填充为指定值。
查找根结点(重点)
根结点的标志性特征是元素值为负数,如果元素为正数,则需要一直向上寻找根结点,使用循环,先判断 elem[x] 是否为零,如果是则跳出循环,说明找到了最终的根结点,如果不是则需要继续查找,将 x 置为 elem[x]。
//查找双亲结点
public int findRoot(int x) {
if(x < 0 || x >= elem.length) {
throw new IndexOutOfBoundsException("x 不合法");
}
while(elem[x] >= 0) {
x = elem[x];
}
return x;
}
判断二者是否在同一个集合中
这个直接使用上面寻找根结点的方法来判断即可。
//判断两个元素是否在同一个集合中
public boolean isSameSet(int x1, int x2) {
if(findRoot(x1) == findRoot(x2)) {
return true;
}
return false;
}
将两个元素合并到同一个集合中(重点)
首先两个元素如果就已经在同一个集合中的话,则直接返回即可。
如果不再同一个集合的话,我们需要先确定一件事情,并查集是互不相交的集合,意味着不能出现交集,也就是说,合并两个元素的本质其实就是将两个集合合并成一个集合。
既然要将两个集合合并成一个集合的话,我们就需要先找到两个集合的根节点。
然后将一个根节点作为最终集合的根结点,另一个根节点直接合并进去即可
我们以 S1 的根节点最为最终的根节点
那么我们要修改S1 根节点的数值,由于根节点的数值的绝对值表示这个集合一共有多少个结点,所以将原先两个集合的根节点的数值相加就是最终根节点的数值。
最后我们要修改 S2 根节点的数值,将其置为 S1 的根节点的索引值,这样这个结点就变成普通的集合结点了。
//将两个元素合并到同一个集合中
public void union(int x1, int x2) {
if(isSameSet(x1,x2)) {
return;
}
int r1 = findRoot(x1);
int r2 = findRoot(x2);
elem[r1] += elem[r2];
elem[r2] = r1;
}
统计集合个数
集合个数等于所有树的根节点,当元素值为负数时,则其对应的索引值就是根结点,我们直接遍历数组即可。
//统计集合的个数
public int setSizes() {
int count = 0;
for(int x : elem) {
if(x < 0) {
count++;
}
}
return count;
}
并查集最终代码
import java.util.Arrays;
public class UnionFindSet {
public int[] elem;
public UnionFindSet(int n) {
elem = new int[n];
Arrays.fill(elem, -1);
}
//查找双亲结点
public int findRoot(int x) {
if(x < 0 || x >= elem.length) {
throw new IndexOutOfBoundsException("x 不合法");
}
while(elem[x] >= 0) {
x = elem[x];
}
return x;
}
//判断两个元素是否在同一个集合中
public boolean isSameSet(int x1, int x2) {
if(findRoot(x1) == findRoot(x2)) {
return true;
}
return false;
}
//将两个元素合并到同一个集合中
public void union(int x1, int x2) {
if(isSameSet(x1,x2)) {
return;
}
int r1 = findRoot(x1);
int r2 = findRoot(x2);
elem[r1] += elem[r2];
elem[r2] = r1;
}
//统计集合的个数
public int setSizes() {
int count = 0;
for(int x : elem) {
if(x < 0) {
count++;
}
}
return count;
}
}
实战演练
省份数量
解析:
当某一些城市有着直接或者间接连接的时候,那么这些城市可以认定为一个省份,如果把省份当成一个集合的话,那城市就是集合的结点,所以这道题目我们可以使用并查集来解决。
首先并查集创建一个数组,大小为 n (城市的个数),然后开始遍历矩阵,当出现 1 的时候要进行两个元素(i,j)合并。
最后统计集合总数(即为省份总数)
在遍历矩阵的时候,我们其实可以只遍历矩阵沿着主对角线的一半,连对角线的元素也不用遍历,为什么呢?因为 A城市 与 B城市 连接的话 等价于 B城市 与 A城市 连接,在矩阵中沿着主对角线呈对称性,所以可以只遍历一半的矩阵
由于矩阵的主对角线的元素在并查集创建的时候,就已经进集合里了(即自己就是一个集合),所以主对角线也是可以不用遍历的。
class Solution {
public int findRoot(int[] elem, int x) {
while(elem[x] >= 0) {
x = elem[x];
}
return x;
}
public void union(int[] elem, int x1, int x2) {
int r1 = findRoot(elem, x1);
int r2 = findRoot(elem, x2);
if(r1 == r2) {
return;
}
elem[r1] += elem[r2];
elem[r2] = r1;
}
public int findCircleNum(int[][] isConnected) {
int n = isConnected.length;
int[] elem = new int[n];
Arrays.fill(elem, -1);
for(int i = 0; i < n; i++) {
for(int j = i + 1; j < n; j++) {
if(isConnected[i][j] == 1) {
union(elem,i,j);
}
}
}
int count = 0;
for(int x : elem) {
if(x < 0) count++;
}
return count;
}
}
等式方程的可满足性
解析:
当变量存在相等关系的时候,则说明这些变量可以表示同一个整数,如果这些变量又存在不相等的关系的话,则无法进行表示,举个例子:a == b, a != b,你说这个方程有解吗?
如果我们把相等的元素存在集合里,然后再次遍历字符串数组,发现不相等的关系的时候,需要判断这两个元素是否在同一个集合中,如果不在同一个集合中的话,则没有问题,如果在同一个集合中,则说明等式方程存在错误。
class Solution {
public int findRoot(int[] elem, int x) {
while(elem[x] >= 0) {
x = elem[x];
}
return x;
}
public void union(int[] elem, int x1, int x2) {
int r1 = findRoot(elem, x1);
int r2 = findRoot(elem, x2);
if(r1 == r2) {
return;
}
elem[r1] += elem[r2];
elem[r2] = r1;
}
public boolean equationsPossible(String[] equations) {
int[] elem = new int[26];
Arrays.fill(elem, -1);
int n = equations.length;
for(int i = 0; i < n; i++) {
if(equations[i].charAt(1) == '=') {
union(elem, equations[i].charAt(0) - 'a', equations[i].charAt(3) - 'a');
}
}
for(int i = 0; i < n; i++) {
if(equations[i].charAt(1) == '!') {
int r1 = findRoot(elem, equations[i].charAt(0) - 'a');
int r2 = findRoot(elem, equations[i].charAt(3) - 'a');
if(r1 == r2) {
return false;
}
}
}
return true;
}
}