每日一题——LeetCode399(除法求值)
相关知识点:图论,Floyd算法,并查集
题目
链接🔗:https://leetcode-cn.com/problems/evaluate-division/
给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。
另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。
返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -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 ]
示例 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]
解法一:Floyd算法
可以将每一个字母看作图中一个结点,边的权值为两个结点所代表的字母的比值(具有反向边),因为任意两点间的距离是唯一的,因此我们可以用Floyd算法去求得两点间的距离,这样我们就可以在有向图中通过一定的路径去求得某个结点所代表的值。
以第一个例子来说,从a到b,权值为2,从b到a,权值为0.5,从b到c,权值为3,从 c到b,权值为1/3,从a结点出发,经过b到c相当于:a/b * b/c ,就等于a/c
以此类推,就可以得到相应计算式的值
class Solution {
public:
vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
vector<double> res;
//用哈希表存一下有哪些结点
unordered_set<string> v;
//用哈希表存下两点间的距离
unordered_map<string,unordered_map<string,double>> dis;
//枚举所有的方程
for (int i = 0;i < equations.size(); i++) {
auto x = equations[i][0];
auto y = equations[i][1];
auto d = values[i];
//权值
dis[x][y] = d;
dis[y][x] = 1/d;
//哈希表中加入结点
v.insert(x);
v.insert(y);
}
//Floyd算法,求两点间的距离,从b到c的距离等于从b到a的距离✖️从a到c的距离
for (auto i : v) {
for (auto j : v) {
for (auto k : v) {
if (dis[j][i] && dis[k][i]) {
dis[j][k] = dis[j][i] * dis[i][k];
}
}
}
}
//在图中寻找
for (auto q : queries) {
auto a = q[0],b = q[1];
if (dis[a][b]) {
res.push_back(dis[a][b]);
}else {
res.push_back(-1.00000);
}
}
return res;
}
};
解法二:并查集
我们知道,在维护朴素并查集时,只需要维护根结点即可,这道题目需要使用带权并查集,一边维护根结点的同时,一遍维护距离,即当前结点的值除以当前结点的根的商
首先我们需要两个哈希表,去存储当前结点与其根结点的映射关系,另一个就来记录对应映射的权值,在标准的find操作中,我们在进行路径压缩时,更新每个点的权重,最后判断查询中,两个元素的根结点是否相同
对当前结点进行一次查询,先通过递归找到其根结点,然后先更新当前结点指向的父亲结点的权值和结点指向,然后一层一层更新,边更新权值,边路径压缩
- 更新权值的顺序(不能反,否则b的权值未更新就拿来用于更新a的权值了)
- 在find函数里维护权值的过程
- 最后得出结果查找的过程及验证
- 代码
class Solution {
public:
// 哈希表1 建立字符到其祖宗结点的映射
unordered_map<string, string> parents;
// 哈希表2 建立字符到其祖宗结点的权值
unordered_map<string, double> weights;
// 在查的过程合并到根结点的权值
// 此处使用路径压缩,所有结点指向根结点
string find(string a){
if(parents[a] != a){
//以a->b->c为例
string temp = find(parents[a]);
// a->c 的权重 = a->b 的权重 * b->c 的权重
weights[a] = weights[a] * weights[parents[a]];
// a->c 连接
parents[a] = temp;
}
// 返回a的祖宗结点
return parents[a];
}
vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries)
{
vector<double> res;
// 1. 并查集初始化
int n = equations.size();
for(int i=0;i < n; i++)
{
string a = equations[i][0];
string b = equations[i][1];
// 初始化每个字符串的母结点是自己,初始化权重为1.0
parents[a] = a;
parents[b] = b;
weights[a] = 1.0;
weights[b] = 1.0;
}
// 2. 更新并查集权重
for(int i=0;i < n; i++)
{
string a = equations[i][0];
string b = equations[i][1];
// 找到 a 的根结点 a->root_a 并更新指向根结点的权重
string root_a = find(a);
// 建立 root_a -> b 的连接 并更新权重
parents[root_a] = b;
// a->root_a->b
// values[i]: a->b权重(题目给的)
// weights[a]: a->root_a 权重(已知)
// weights[root_a]: root_a->b 权重(待求)
weights[root_a] = values[i] / weights[a];
}
// 3. 计算最终结果
int m = queries.size();
for(int i=0; i<m; i++)
{
string a = queries[i][0];
string b = queries[i][1];
// 如果
// (1) 母结点哈希表不包含查询字符中的任意一个
// (2) 两个字符最终的根节点不相同
//(如 a->c b->d ab不在一个集合中)(但是 a->c->d b->d ab就可以)
// 此两种情况 都是 -1.0
if(!parents.count(a) || !parents.count(b) || find(a)!=find(b))
{
res.push_back(-1.0);
}else{
res.push_back(weights[a]/weights[b]);
}
}
return res;
}
};
还有DFS与BFS的做法,本质上与第一种做法类似就不展示了