399. 除法求值
399. 除法求值
题目链接(中等)
题目描述
给你一个变量对数组 equations 和一个实数值数组
values 作为已知条件,其中
equations[i] = [A_i, B_i] 和 values[i]
共同表示等式 A_i / B_i = values[i]。每个 A_i
或 B_i 是一个表示单个变量的字符串。
另有一些以数组 queries 表示的问题,其中
queries[j] = [C_j, D_j] 表示第 j
个问题,请你根据已知条件找出 C_j / D_j = ?
的结果作为答案。
返回
所有问题的答案。如果存在某个无法确定的答案,则用
-1.0
替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用
-1.0 替代这个答案。
注意:输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。未在等式列表中出现的变量是未定义的,因此无法确定它们的答案。
数据范围:
1 <= equations.length <= 20equations[i].length == 21 <= A_i.length, B_i.length <= 5values.length == equations.length0.0 < values[i] <= 20.01 <= queries.length <= 20queries[i].length == 21 <= C_j.length, D_j.length <= 5A_i, B_i, C_j, D_j由小写英文字母与数字组成
示例
示例 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]
核心思路
将变量视为图的节点,等式 A / B = k 视为一条从
A 到 B 权值为 k
的有向边,同时反向边 B → A 权值为 1/k。
于是问题转化为:在图中求任意两点之间的路径权值乘积。
- 若两点连通,则路径乘积即为答案;
- 若两点不连通或变量未出现,则答案为
-1.0。
四种解法:
- DFS:对每个查询独立做一次深度优先搜索,实时计算路径乘积;
- BFS:对每个查询独立做一次广度优先搜索,实时计算路径乘积;
- Floyd:预处理所有点对之间的比值,查询时直接查表;
- 带权并查集:维护每个节点到根的比值,查询时通过根节点计算。
方法一:DFS(深度优先搜索)
思路及解法
核心:把除法关系转成带权有向图,查询即寻找路径并累乘边权。
- 构建带权图:
graph[a]存储所有从a出发的(邻居, 权重)对; - 对每个查询
[c, d]:- 若
c或d不在图中,返回-1.0; - 若
c == d,返回1.0; - 否则从
c出发做 DFS,递归查找通往d的路径,沿途累乘边权; - 若找到,返回累积乘积;否则返回
-1.0。
- 若
DFS 递归的设计:
def dfs(start, end, visited):
if start == end:
return 1.0
visited.add(start)
for neighbor, weight in graph[start]:
if neighbor not in visited:
res = dfs(neighbor, end, visited)
if res is not None:
return weight * res
return None- 找到终点时返回
1.0(作为乘积的单位元); - 递归返回后,将当前边权
weight乘上去,逐层累积; - 用
visited防止走回头路。
为什么用 None
表示未找到:因为合法的答案可能是
0.0(虽然本题除数不为 0,但用 None 更安全,与
-1.0 区分)。
示例验证:equations = [["a","b"],["b","c"]], values = [2.0,3.0],图结构:
a → b (2.0)
b → a (0.5)
b → c (3.0)
c → b (1/3)查询 a / c:
- 从
a出发 →b,累积2.0; - 从
b出发 →c,累积2.0 × 3.0 = 6.0; c == c,返回6.0。✓
代码
from collections import defaultdict
class Solution:
def calcEquation(self, equations: list[list[str]], values: list[float], queries: list[list[str]]) -> list[float]:
# 1. 构建带权图
graph = defaultdict(list)
for (a, b), v in zip(equations, values):
graph[a].append((b, v))
graph[b].append((a, 1.0 / v))
# 2. DFS 查找从 start 到 end 的路径,返回累积乘积
def dfs(start, end, visited):
if start == end:
return 1.0
visited.add(start)
for neighbor, weight in graph[start]:
if neighbor not in visited:
res = dfs(neighbor, end, visited)
n
return weight * res
return None
# 3. 处理每个查询
ans = []
for c, d in queries:
if c not in graph or d not in graph:
ans.append(-1.0)
elif c == d:
ans.append(1.0)
else:
res = dfs(c, d, set())
ans.append(res if res is not None else -1.0)
return ans复杂度分析
- 时间复杂度:构建图 ,其中 是等式数量。每个查询做一次 DFS,最坏 ,总查询数 ,总时间 。题目中 ,,非常小。
- 空间复杂度:,存储图。DFS 递归栈深度最多 。
方法二:BFS(广度优先搜索)
思路及解法
与方法一思路完全相同,只是把 DFS 换成 BFS。
- 遍历
equations,用哈希表将每个变量名映射为整数编号; - 构建邻接表:对于每个等式
A / B = v,添加边A → B权重v,B → A权重1/v; - 对于每个查询
[C, D]:- 若
C或D未在映射中,返回-1.0; - 若
C == D,返回1.0; - 否则从
C出发做 BFS,维护从C到各点的累积比值,直到找到D或队列为空; - 返回找到的比值或
-1.0。
- 若
代码
from collections import deque
class Solution:
def calcEquation(self, equations: list[list[str]], values: list[float], queries: list[list[str]]) -> list[float]:
# 1. 变量名映射为整数
var_id = {}
nvars = 0
for a, b in equations:
if a not in var_id:
var_id[a] = nvars
nvars += 1
if b not in var_id:
var_id[b] = nvars
nvars += 1
# 2. 构建邻接表
edges = [[] for _ in range(nvars)]
for (a, b), v in zip(equations, values):
ia, ib = var_id[a], var_id[b]
edges[ia].append((ib, v))
edges[ib].append((ia, 1.0 / v))
# 3. 处理查询
result = []
for c, d in queries:
if c not in var_id or d not in var_id:
result.append(-1.0)
continue
ic, id_ = var_id[c], var_id[d]
if ic == id_:
result.append(1.0)
continue
# BFS
ratios = [-1.0] * nvars
ratios[ic] = 1.0
q = deque([ic])
while q and ratios[id_] < 0:
x = q.popleft()
for y, val in edges[x]:
if ratios[y] < 0:
ratios[y] = ratios[x] * val
q.append(y)
result.append(ratios[id_])
return result复杂度分析
- 时间复杂度:,其中 为边数, 为查询数, 为字符串平均长度。构建图 ,每次 BFS 最多遍历 条边,共 次。
- 空间复杂度:, 为变量数, 为边数。
方法三:Floyd 算法
思路及解法
预先计算任意两点之间的比值。
- 同样建立变量到整数的映射;
- 初始化二维矩阵
graph,大小为nvars × nvars,初始值为-1.0; - 对于每个等式
A / B = v,设置graph[ia][ib] = v,graph[ib][ia] = 1/v;对角线graph[i][i] = 1.0; - 三重循环 Floyd:若
graph[i][k] > 0且graph[k][j] > 0,则graph[i][j] = graph[i][k] * graph[k][j]; - 查询时直接查表,若值
> 0则返回,否则-1.0。
代码
class Solution:
def calcEquation(self, equations: list[list[str]], values: list[float], queries: list[list[str]]) -> list[float]:
# 变量映射
var_id = {}
nvars = 0
for a, b in equations:
if a not in var_id:
var_id[a] = nvars
nvars += 1
if b not in var_id:
var_id[b] = nvars
nvars += 1
# 初始化矩阵
graph = [[-1.0] * nvars for _ in range(nvars)]
for i in range(nvars):
graph[i][i] = 1.0
for (a, b), v in zip(equations, values):
ia, ib = var_id[a], var_id[b]
graph[ia][ib] = v
graph[ib][ia] = 1.0 / v
# Floyd 预处理
for k in range(nvars):
for i in range(nvars):
for j in range(nvars):
if graph[i][k] > 0 and graph[k][j] > 0:
graph[i][j] = graph[i][k] * graph[k][j]
# 查询
result = []
for c, d in queries:
if c not in var_id or d not in var_id:
result.append(-1.0)
else:
val = graph[var_id[c]][var_id[d]]
result.append(val if val > 0 else -1.0)
return result复杂度分析
- 时间复杂度:,构建图 ,Floyd ,查询 。
- 空间复杂度:。
方法四:带权并查集
思路及解法
每个节点维护两个信息:
parent[x]:父节点;weight[x]:节点x的值与父节点值的比值,即v[x] / v[parent[x]]。
查找(find):路径压缩时更新
weight,使其始终表示 x 到根的比值。
合并(union):对于等式 A / B = k,找到
A 和 B 的根
ra、rb,将 ra 挂到
rb 下,并更新 weight[ra]。
查询:若两个变量根相同,则
A / B = weight[A] / weight[B](因为 weight
是到根的比值)。
代码
class Solution:
def calcEquation(self, equations: list[list[str]], values: list[float], queries: list[list[str]]) -> list[float]:
# 变量映射
var_id = {}
nvars = 0
for a, b in equations:
if a not in var_id:
var_id[a] = nvars
nvars += 1
if b not in var_id:
var_id[b] = nvars
nvars += 1
parent = list(range(nvars))
weight = [1.0] * nvars # weight[x] = v[x] / v[parent[x]]
def find(x: int) -> int:
if parent[x] != x:
root = find(parent[x])
weight[x] *= weight[parent[x]]
parent[x] = root
return parent[x]
def union(x: int, y: int, val: float) -> None:
# val = v[x] / v[y]
rx, ry = find(x), find(y)
if rx == ry:
return
parent[rx] = ry
# 需要满足 v[x] / v[y] = val
# v[x] = weight[x] * v[rx]
# v[y] = weight[y] * v[ry]
# 所以 weight[rx] = v[rx] / v[ry] = (v[x]/weight[x]) / (v[y]/weight[y])
# = (v[x]/v[y]) * (weight[y]/weight[x])
weight[rx] = val * weight[y] / weight[x]
# 构建并查集
for (a, b), v in zip(equations, values):
union(var_id[a], var_id[b], v)
# 查询
result = []
for c, d in queries:
if c not in var_id or d not in var_id:
result.append(-1.0)
else:
ic, id_ = var_id[c], var_id[d]
if find(ic) == find(id_):
result.append(weight[ic] / weight[id_])
else:
result.append(-1.0)
return result复杂度分析
- 时间复杂度:,并查集操作近似常数。
- 空间复杂度:。
四种方法对比
| 方法 | 时间 | 空间 | 特点 |
|---|---|---|---|
| DFS | 最直观,代码最短,本题首选 | ||
| BFS | 与 DFS 等价,换成队列 | ||
| Floyd | 预处理所有点对,适合多次查询 | ||
| 带权并查集 | 近 | 效率最高,但实现稍复杂 |
推荐:
- 面试:首选 DFS,代码最短、思路最直观,本题数据规模小完全够用;
- 查询很多:用 Floyd 或 带权并查集;
- 追求最优:带权并查集,时间空间都最优。
关键细节
1. 图建模
等式 A / B = k 对应两条有向边:
A → B权重k;B → A权重1/k。
路径上权值相乘,即可得到两端变量的比值。
2. DFS 中为什么用
None 而不是 -1.0 表示未找到
因为答案可能是合法的 0.0(虽然本题除数不为 0,但用
None 更安全),避免和「找不到」混淆。
3. BFS 中的 ratios 数组
ratios[i] 表示从起点 C 到节点
i 的累积比值。初始 ratios[C] = 1.0,未访问为
-1.0。BFS 扩展时
ratios[y] = ratios[x] * val。
4. Floyd 的对角线
graph[i][i] = 1.0 表示自己除以自己等于 1。Floyd
更新时只考虑 > 0 的边,避免无效的 -1.0
参与乘法。
5. 带权并查集的 weight
含义
weight[x] = v[x] / v[parent[x]]。路径压缩时,weight[x] *= weight[parent[x]],使其指向根。合并时推导:
设 x 的根为 rx,y 的根为
ry,已知 v[x] / v[y] = val。
v[x] = weight[x] * v[rx],v[y] = weight[y] * v[ry]。
所以
weight[rx] = v[rx] / v[ry] = val * weight[y] / weight[x]。
6. 未定义变量的处理
如果查询中的变量从未在 equations 中出现,直接返回
-1.0。
总结
- 本质:带权图上的路径乘积问题;
- DFS:每次查询独立搜索,代码最短,本题首选;
- BFS:与 DFS 等价,用队列实现;
- Floyd:预处理所有点对,适合多次查询;
- 带权并查集:利用比值关系维护连通分量,效率最高;
- 通用套路:变量比值 → 图建模 → 路径乘积 / 带权并查集。
相关题目
- LC 990. 等式方程的可满足性(并查集)
- LC 547. 省份数量(并查集求连通分量)
- LC 200. 岛屿数量(图搜索)
- LC 133. 克隆图(图的遍历)