概念
类比树是一种保留孩子节点的数据结构,并查集是一种保留父亲节点的数据结构。
查找-合并-获取连通分量/集合
适用场景
- 处理元素分组问题;
- 一些不交集的合并及查询问题;
- 连通性问题,如判断图中的两个节点是否属于同一个连通分量/祖先,或判断一个无向图是否是连通图等;
- 传递性问题,如亲戚关系或者带权的乘除法运算。
UnionFind模板代码
class UnionFind<T> {
private Map<T, T> father; // Use generic types for the Map
public UnionFind() {
father = new HashMap<>(); // Initialize the Map
}
//把一个新节点添加到并查集中
public void add(T x) {
if (!father.containsKey(x)) {
father.put(x, null);
}
}
//外部判断两个节点连通/有相同祖先,则合并两个节点
public void merge(T x, T y) {
T rootX = find(x);
T rootY = find(y);
//如果它们不相同,则表示节点 x 和节点 y 不在同一个集合中,需要合并它们
if (!rootX.equals(rootY)) { // Use equals() for object comparison
father.put(rootX, rootY);
}
}
//查找祖先,如果节点的父节点不为空就不断迭代
public T find(T x) {
T root = x;
while (father.get(root) != null) {
root = father.get(root);
}
//路径压缩,最终x指向最远的爹
while (!x.equals(root)) { // Use equals() for object comparison
T originalFather = father.get(x);
father.put(x, root);
x = originalFather;
}
return root;
}
//判断两个节点是否连通
public boolean isConnected(T x, T y) {
return find(x).equals(find(y)); // Use equals() for object comparison
}
}
例题
547.省份数量-m
有
n
个城市,其中一些彼此相连,另一些没有相连。如果城市a
与城市b
直接相连,且城市b
与城市c
直接相连,那么城市a
与城市c
间接相连。省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个
n x n
的矩阵isConnected
,其中isConnected[i][j] = 1
表示第i
个城市和第j
个城市直接相连,而isConnected[i][j] = 0
表示二者不直接相连。返回矩阵中 省份 的数量。
思路:每一个节点先生成一个集合,当两个节点连通时将其合并为一个集合,最后返回集合个数即可。获取集合的方法可以写在Union类中。
优化:矩阵关于主对角线对称,判断是否连通时,可以只遍历一个三角。
class UnionFind {
private Map<Integer,Integer> father;//记录父节点
private int numOfSets = 0;//记录集合的数量
public UnionFind() {//构造函数
father = new HashMap<Integer,Integer>();
numOfSets = 0;//新增的
}
public void add(int x) {//把一个新节点添加到并查集中
if (!father.containsKey(x)) {
father.put(x, null);
numOfSets++;//新增的
}
}
public void merge(int x, int y) {//两个节点连通/有相同祖先,合并两个节点
int rootX = find(x);
int rootY = find(y);
//如果它们不相同,则表示节点 x 和节点 y 不在同一个集合中,需要合并它们
if (rootX != rootY) {
father.put(rootX,rootY);
numOfSets--;//新增的
}
}
public int find(int x) {//查找祖先,如果节点的父节点不为空就不断迭代
int root = x;
while (father.get(root) != null) {
root = father.get(root);
}
//路径压缩
while (x != root) {
int original_father = father.get(x);
father.put(x,root);
x = original_father;
}
return root;
}
public boolean isConnected(int x, int y) {//判断两个节点是否连通
return find(x) == find(y);
}
public int getNumOfSets() {
return numOfSets;
}
}
class Solution {
public int findCircleNum(int[][] isConnected) {
//合并-查找-字典,树的每个节点记录子节点,并查集中的每个节点会记录父节点
UnionFind uf = new UnionFind();
for (int i = 0; i < isConnected.length; i++){
uf.add(i);
for (int j = 0; j < i; j++) {//矩阵是对称的,遍历一个三角即可
if (isConnected[i][j] == 1) {
uf.merge(i,j);
}
}
}
return uf.getNumOfSets();
}
}
时间复杂度:O(n^2logn),n是城市的数量
空间复杂度:O(n)
990.等式方程的可满足性-m
给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程
equations[i]
的长度为4
,并采用两种不同的形式之一:"a==b"
或"a!=b"
。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回
true
,否则返回false
。
思路:两次for循环遍历,第一次遍历,遇见==表明同一个等式中的两个变量属于同一个连通分量,故将其合并;第二次遍历,遇见!=表明同一个不等式中的两个变量不能属于同一个祖先,如果两个变量本身相连则推出矛盾,返回false。
class UnionFind<T> {
private Map<T, T> father; // Use generic types for the Map
public UnionFind() {
father = new HashMap<>(); // Initialize the Map
}
public void add(T x) {
if (!father.containsKey(x)) {
father.put(x, null);
}
}
public void merge(T x, T y) {
T rootX = find(x);
T rootY = find(y);
if (!rootX.equals(rootY)) { // Use equals() for object comparison
father.put(rootX, rootY);
}
}
public T find(T x) {
T root = x;
while (father.get(root) != null) {
root = father.get(root);
}
while (!x.equals(root)) { // Use equals() for object comparison
T originalFather = father.get(x);
father.put(x, root);
x = originalFather;
}
return root;
}
public boolean isConnected(T x, T y) {
return find(x).equals(find(y)); // Use equals() for object comparison
}
}
class Solution {
public boolean equationsPossible(String[] equations) {
UnionFind<Character> uf = new UnionFind();
for (int i = 0; i < equations.length; i++) {
char[] ch = equations[i].toCharArray();
uf.add(ch[0]);
uf.add(ch[3]);
if (ch[1] == '=') {//同一个等式中的两个变量属于同一个连通分量,故将其合并
uf.merge(ch[0], ch[3]);
}
}
for (int i = 0; i < equations.length; i++) {
char[] ch = equations[i].toCharArray();
if (ch[1] == '!') {//同一个不等式中的两个变量不能属于同一个祖先
if(uf.isConnected(ch[0], ch[3])){
return false;
}
}
}
return true;
}
}
时间复杂度:O(n+ClogC),n是equations中的方程数量,C是变量的总数,此题中变量为小写字母,故C <= 26。
空间复杂度:O(C)
399.除法求值-m
给你一个变量对数组
equations
和一个实数值数组values
作为已知条件,其中equations[i] = [Ai, Bi]
和values[i]
共同表示等式Ai / Bi = values[i]
。每个Ai
或Bi
是一个表示单个变量的字符串。另有一些以数组
queries
表示的问题,其中queries[j] = [Cj, Dj]
表示第j
个问题,请你根据已知条件找出Cj / Dj = ?
的结果作为答案。返回 所有问题的答案 。如果存在某个无法确定的答案,则用
-1.0
替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用-1.0
替代这个答案。注意:输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。
注意:未在等式列表中出现的变量是未定义的,因此无法确定它们的答案。
该题是在上一道题的基础上加了权重,难点在于find和merge都涉及到权重的更新。
值得下去再琢磨下这道题。
class UnionFind {
private Map<String, String> father;//记录父节点(除数),son-father
HashMap<String, Double> valueMap = new HashMap<>();// 记录指向父节点的权值
public UnionFind(HashSet<String> StringSet) {//构造函数,传入String的哈希集合
father = new HashMap<>();
for (String s : StringSet) {
father.put(s, s);//初始化父节点为自身
valueMap.put(s, 1.0);//初始权值赋为1.0
}
}
//add在构造函数里实现了
// public void add(int x) {//把一个新节点添加到并查集中
// if (!father.containsKey(x)) {
// father.put(x, null);
// }
// }
public void merge(String x, String y, Double value) {//x/y=value
String rootX = find(x);
String rootY = find(y);
//如果父节点不相同,则表示节点 x 和节点 y 不在同一个集合中,需要合并它们
if (!rootX.equals(rootY)) {
father.put(rootX, rootY);
// 两条路径上的有向边的权值的乘积是一定相等的,a->b->c = a->d->c
valueMap.put(rootX, value * valueMap.get(y) / valueMap.get(x));
}
}
public String find(String x) {//查找祖先
// 祖先不存在就返回空
if (!father.containsKey(x)) return null;
String root = x;
double base = 1;
// 先从x找到根节点,并更新base为x到根节点的权重
while (!root.equals(father.get(root))) {
root = father.get(root);// 最终得到x的最远祖先
base *= valueMap.get(root);
}
// 更新从x到根节点路径上节点的权重
while (!x.equals(root)) {
String original_father = father.get(x);
valueMap.put(x, valueMap.get(x) * base);
base /= valueMap.get(original_father);
father.put(x,root);// 路径压缩
x = original_father;
}
return root;
}
// public double isConnected(int x, int y) {//判断两个节点是否连通
// int rootX = find(x);
// int rootY = find(y);
// if (rootX == rootY) {
// return weight[x] / weight[y];
// } else {
// return -1.0d;
// }
// }
}
class Solution {
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
//并查集:查找-合并-获取连通分量/集合
int n = queries.size();
double[] res = new double[n];
HashSet<String> hs = new HashSet<>();
// 存放所有节点的哈希集合,每个节点都可能作为父节点
for (int i = 0; i < equations.size(); i++) {
hs.add(equations.get(i).get(0));
hs.add(equations.get(i).get(1));
}
// 遍历方程,合并方程中的两个节点
UnionFind uf = new UnionFind(hs);
for (int i = 0; i < equations.size(); i++) {
String x = equations.get(i).get(0);
String y = equations.get(i).get(1);
uf.merge(x, y, values[i]);
}
// 遍历问题
for (int i = 0; i < queries.size(); i++) {
String x = queries.get(i).get(0);
String y = queries.get(i).get(1);
//其中至少有一个节点没有祖先直接返回-1,独立的节点
if (uf.find(x) == null || uf.find(y) == null) {
res[i] = -1;
// 两个节点的祖先相同,直接用指向父节点的权值作比
} else if (uf.find(x).equals(uf.find(y))) {
res[i] = uf.valueMap.get(x) / uf.valueMap.get(y);
// 两个节点的祖先不同,两者没有关系,返回-1
} else {
res[i] = -1;
}
}
return res;
}
}
时间复杂度:O((N+Q)logA),N 为输入方程 equations 的长度,每一次执行【合并】操作的时间复杂度是 O(logA),A 是 equations 里不同字符的个数;Q 为查询数组 queries 的长度,每一次查询时执行「路径压缩」的时间复杂度是 O(logA)。
空间复杂度:O(A),father和valueMap的长度为A。