LeetCode 399. 除法求值
题目描述


题意分析
已知若干变量之间的除法等式,查询两个变量的比值。可以借助其他变量串联推导,不要求查询对应的等式直接出现在输入中。
若任一变量从未出现在等式中,或者两个已知变量之间没有关系链,结果都是
-1.0。因此未知变量除以自身也不能直接返回1.0。
解法:建图加 BFS
核心思路
[!blue]
把变量作为图的节点。等式
a / b = v给出两条有向边:a → b的权重为v,表示a / b;b → a的权重为1 / v,表示反向比值。两边都建,才能沿任一方向推导。对查询
start / target,从start开始 BFS。定义ratio[x]为已经推导出的start / x,起点有ratio[start] = 1。沿权重为cur / next的边扩展时,有ratio[next] = ratio[cur] * weight,因为中间变量cur会在乘法中约掉。题目保证已知等式不存在矛盾,所以同一个可达变量通过不同路径得到的比值一致。一个变量首次发现后,无需再经其他路径访问;用
ratio是否已有该键兼作访问标记,可以阻止正反边造成的循环。找到目标时,当前路径乘积就是答案;队列耗尽仍未找到,说明目标与起点不连通。
ratio以本次查询的起点为基准,因此每次查询重新建立队列和ratio,只复用图。
解题步骤
- 遍历等式,为两个变量建立邻接表,写入正向比值和反向倒数。
- 对每个查询,先检查两个变量是否都存在;若有未知变量,返回
-1.0。检查通过后,同一变量相除返回1.0。- 起点入队,并令
ratio[start] = 1.0。每次取出一个节点,只扩展尚未记录比例的邻居。- 用当前比例乘边权得到邻居比例;若邻居就是目标,立即返回。否则先记录比例,再将邻居入队,避免重复加入队列。
- 队列清空仍未命中则返回
-1.0,将各次查询结果按原顺序写入答案数组。
代码实现
class Solution {
public double[] calcEquation(
List<List<String>> equations, double[] values, List<List<String>> queries) {
Map<String, Map<String, Double>> graph = new HashMap<>();
for (int i = 0; i < equations.size(); i++) {
String a = equations.get(i).get(0);
String b = equations.get(i).get(1);
double value = values[i];
graph.computeIfAbsent(a, k -> new HashMap<>()).put(b, value);
// 反向边使用倒数,保持比例方向一致
graph.computeIfAbsent(b, k -> new HashMap<>()).put(a, 1.0 / value);
}
double[] ans = new double[queries.size()];
for (int i = 0; i < queries.size(); i++) {
ans[i] = bfs(graph, queries.get(i).get(0), queries.get(i).get(1));
}
return ans;
}
private double bfs(Map<String, Map<String, Double>> graph, String start, String target) {
// 先判断变量存在,未知变量除以自身也无结果
if (!graph.containsKey(start) || !graph.containsKey(target)) {
return -1.0;
}
if (start.equals(target)) {
return 1.0;
}
Queue<String> queue = new ArrayDeque<>();
Map<String, Double> ratio = new HashMap<>();
queue.offer(start);
ratio.put(start, 1.0);
while (!queue.isEmpty()) {
String cur = queue.poll();
double curValue = ratio.get(cur);
for (Map.Entry<String, Double> entry : graph.get(cur).entrySet()) {
String next = entry.getKey();
if (ratio.containsKey(next)) {
continue;
}
double nextValue = curValue * entry.getValue();
if (next.equals(target)) {
return nextValue;
}
// 记录起点到此处的乘积,同时标记已经发现
ratio.put(next, nextValue);
queue.offer(next);
}
}
return -1.0;
}
}
func calcEquation(equations [][]string, values []float64, queries [][]string) []float64 {
graph := make(map[string]map[string]float64)
for i, equation := range equations {
a, b := equation[0], equation[1]
if graph[a] == nil {
graph[a] = make(map[string]float64)
}
if graph[b] == nil {
graph[b] = make(map[string]float64)
}
graph[a][b] = values[i]
// 反向边使用倒数,保持比例方向一致
graph[b][a] = 1.0 / values[i]
}
ans := make([]float64, len(queries))
for i, query := range queries {
ans[i] = eval(graph, query[0], query[1])
}
return ans
}
func eval(graph map[string]map[string]float64, start string, target string) float64 {
// 先判断变量存在,未知变量除以自身也无结果
if graph[start] == nil || graph[target] == nil {
return -1.0
}
if start == target {
return 1.0
}
queue := []string{
start,
}
ratio := map[string]float64{start: 1.0}
for head := 0; head < len(queue); head++ {
cur := queue[head]
for next, value := range graph[cur] {
if _, ok := ratio[next]; ok {
continue
}
nextValue := ratio[cur] * value
if next == target {
return nextValue
}
// 记录起点到此处的乘积,同时标记已经发现
ratio[next] = nextValue
queue = append(queue, next)
}
}
return -1.0
}
复杂度分析
- 时间复杂度:设变量数为 $V$、等式数为 $E$、查询数为 $Q$。建图为 $O(E)$,图中至多有 $2E$ 条有向边,每次查询最多访问所有节点和边,总计 $O(E+Q(V+E))$。
- 空间复杂度:$O(V+E)$ 保存图,单次搜索的队列与比例表另用 $O(V)$;不计返回结果数组。
关键点总结
[!green]
- 路径通过乘法连接比例,反向边必须取倒数。
- BFS 只是寻找一条可达路径,不需要最小化边权或比较不同路径的乘积。
- 图可以复用,访问记录和相对起点的比例每次查询重新建立。
易错点总结
[!yellow]
- 未知变量自除不能直接返回一。
- 把路径权重相加会得到错误比值。
- 无访问记录会沿正反边重复扩展,产生无用搜索。
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!