https://leetcode.cn/problems/evaluate-division/description/?envType=study-plan-v2&envId=top-interview-150
下面这些内容摘抄自官方的题解,我也不会写[555~]
题目描述
给你一个变量对数组 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 的情况,且不存在任何矛盾的结果。
注意:未在等式列表中出现的变量是未定义的,因此无法确定它们的答案。
示例 1:
输入:equations = [[“a”,“b”],[“b”,“c”]], values = [2.0,3.0], queries = [[“a”,“c”],[“b”,“a”],[“a”,“e”],[“a”,“a”],[“x”,“x”]]
输出:[6.00000,0.50000,-1.00000,1.00000,-1.00000]
解释:
条件:a / b = 2.0, b / c = 3.0
问题:a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ?
结果:[6.0, 0.5, -1.0, 1.0, -1.0 ]
注意:x 是未定义的 => -1.0
示例 2:
输入:equations = [[“a”,“b”],[“b”,“c”],[“bc”,“cd”]], values = [1.5,2.5,5.0], queries = [[“a”,“c”],[“c”,“b”],[“bc”,“cd”],[“cd”,“bc”]]
输出:[3.75000,0.40000,5.00000,0.20000]
示例 3:
输入:equations = [[“a”,“b”]], values = [0.5], queries = [[“a”,“b”],[“b”,“a”],[“a”,“c”],[“x”,“y”]]
输出:[0.50000,2.00000,-1.00000,-1.00000]
提示:
1 <= equations.length <= 20
equations[i].length == 2
1 <= Ai.length, Bi.length <= 5
values.length == equations.length
0.0 < values[i] <= 20.0
1 <= queries.length <= 20
queries[i].length == 2
1 <= Cj.length, Dj.length <= 5
Ai, Bi, Cj, Dj 由小写英文字母与数字组成
解题思路
变量与变量之间有了倍数关系,由于 变量之间的倍数关系具有传递性,处理有传递性关系的问题,可以使用「并查集」,我们需要在并查集的「合并」与「查询」操作中 维护这些变量之间的倍数关系。
说明:请大家注意看一下题目中的「注意」和「数据范围」,例如:每个 Ai 或 Bi 是一个表示单个变量的字符串。所以用例 equation = [“ab”, “cd”] ,这里的 ab 视为一个变量,不表示 a * b。如果面试中遇到这样的问题,一定要和面试官确认清楚题目的条件。还有 1 <= equations.length <= 20 和 values[i] > 0.0 可以避免一些特殊情况的讨论。
方法:并查集
分析示例 1:
我们计算了两个结果,不难知道:可以将题目给出的 equation 中的两个变量所在的集合进行「合并」,同在一个集合中的两个变量就可以通过某种方式计算出它们的比值。具体来说,可以把 不同的变量的比值转换成为相同的变量的比值,这样在做除法的时候就可以消去相同的变量,然后再计算转换成相同变量以后的系数的比值,就是题目要求的结果。统一了比较的标准,可以以 O(1) 的时间复杂度完成计算。
如果两个变量不在同一个集合中, 返回 −1.0。并且根据题目的意思,如果两个变量中 至少有一个 变量没有出现在所有 equations 出现的字符集合中,也返回 −1.0。
构建有向图
通过例 1 的分析,我们就知道了,题目给出的 equations 和 values 可以表示成一个图,equations 中出现的变量就是图的顶点,「分子」于「分母」的比值可以表示成一个有向关系(因为「分子」和「分母」是有序的,不可以对换),并且这个图是一个带权图,values 就是对应的有向边的权值。例 1 中给出的 equations 和 values 表示的「图形表示」、「数学表示」和「代码表示」如下表所示。其中 parent[a] = b 表示:结点 a 的(直接)父亲结点是 b,与之对应的有向边的权重,记为 weight[a] = 2.0,即 weight[a] 表示结点 a 到它的 直接父亲结点 的有向边的权重。
「统一变量」与「路径压缩」的关系
刚刚在分析例 1 的过程中,提到了:可以把一个一个 query 中的不同变量转换成 同一个变量,这样在计算 query 的时候就可以以O(1) 的时间复杂度计算出结果,在「并查集」的一个优化技巧中,「路径压缩」就恰好符合了这样的应用场景。
为了避免并查集所表示的树形结构高度过高,影响查询性能。「路径压缩」就是针对树的高度的优化。「路径压缩」的效果是:在查询一个结点 a 的根结点同时,把结点 a 到根结点的沿途所有结点的父亲结点都指向根结点。如下图所示,除了根结点以外,所有的结点的父亲结点都指向了根结点。特别地,也可以认为根结点的父亲结点就是根结点自己。如下国所示:路径压缩前后,并查集所表示的两棵树形结构等价,路径压缩以后的树的高度为 222,查询性能最好。
由于有「路径压缩」的优化,两个同在一个连通分量中的不同的变量,它们分别到根结点(父亲结点)的权值的比值,就是题目的要求的结果。
如何在「查询」操作的「路径压缩」优化中维护权值变化
如下图所示,我们在结点 a 执行一次「查询」操作。路径压缩会先一层一层向上先找到根结点 d,然后依次把 c、b 、a 的父亲结点指向根结点 d。
- c 的父亲结点已经是根结点了,它的权值不用更改;
- b 的父亲结点要修改成根结点,它的权值就是从当前结点到根结点经过的所有有向边的权值的乘积,因此是 3.0乘以 4.0 也就是 12.0;
- a 的父亲结点要修改成根结点,它的权值就是依然是从当前结点到根结点经过的所有有向边的权值的乘积,但是我们 没有必要把这三条有向边的权值乘起来,这是因为 b 到 c,c 到 d 这两条有向边的权值的乘积,我们在把 b 指向 d 的时候已经计算出来了。因此,a 到根结点的权值就等于 b 到根结点 d 的新的权值乘以 a 到 b 的原来的有向边的权值。
如何在「合并」操作中维护权值的变化
一个容易忽略的细节
接下来还有一个小的细节问题:在合并以后,产生了一棵高度为 3 的树,那么我们在执行查询的时候,例如下图展示的绿色结点和黄色结点,绿色结点并不直接指向根结点,在计算这两个变量的比值的时候,计算边的权值的比值得到的结果是不对的。
但其实不用担心这个问题,并查集的「查询」操作会执行「路径压缩」,所以真正在计算两个变量的权值的时候,绿色结点已经指向了根结点,和黄色结点的根结点相同。因此可以用它们指向根结点的有向边的权值的比值作为两个变量的比值。
我们通过这个细节向大家强调:一边查询一边修改结点指向是并查集的特色。
参考代码
class Solution {
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
int equationsSize = equations.size();
UnionFind unionFind = new UnionFind(2*equationsSize); // 建立一个并查集,因为每一对节点有两个,所以容量要是节点的二倍
// 首先预处理,将变量映射成id,并查集的底层就可以使用数组来实现
Map<String, Integer> hashMap = new HashMap<>(2*equationsSize);
int id = 0;
for(int i=0; i<equationsSize;i++){
List<String> equation = equations.get(i);
String var1 = equation.get(0); // 第一个变量名
String var2 = equation.get(1); // 第二个变量名
if(!hashMap.containsKey(var1)){
// 如果var1还没有加入到集合中
hashMap.put(var1,id); // var1映射成id,存进hashMap
id++;
}
if(!hashMap.containsKey(var2)){
hashMap.put(var2, id);
id++;
}
// 合并两个节点
unionFind.union(hashMap.get(var1), hashMap.get(var2), values[i]);
}
// 第二步:查询
int queriesSize = queries.size();
double[] res = new double[queriesSize];
for(int i = 0; i<queriesSize; i++){
String var1 = queries.get(i).get(0);
String var2 = queries.get(i).get(1);
// 从hashMap中取出对应的id
Integer id1 = hashMap.get(var1);
Integer id2 = hashMap.get(var2);
if(id1==null || id2 == null){
res[i] = -1.0d;
}else{
res[i] = unionFind.isConnected(id1,id2);
}
}
return res;
}
private class UnionFind {
private int[] parent; // 存储父节点,这里每一个节点存储的都是id,所以用的int
private double[] weight; // 指向父节点的权值
public UnionFind(int n){
this.parent = new int[n];
this.weight = new double[n];
for(int i=0; i<n; i++){
parent[i] = i;
weight[i] = 1.0d;
}
}
public void union(int x, int y, double value){
int rootX = find(x);
int rootY = find(y);
if(rootX == rootY){
return ;
}
parent[rootX] = rootY;
weight[rootX] = weight[y]*value / weight[x];
}
/**
路径压缩
*/
public int find(int x){
if(x != parent[x]){
int origin = parent[x];
parent[x] = find(parent[x]);
weight[x] *= weight[origin];
}
return parent[x];
}
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;
}
}
}
}