想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。
📖 第92课:除法求值
模块:图论 | 难度:Medium ⭐ LeetCode 链接:leetcode.cn/problems/ev… 前置知识:第89课(岛屿数量 - DFS/BFS基础)、第91课(课程表 - 图的遍历) 预计学习时间:30分钟
🎯 题目描述
给定一些变量之间的除法关系,以及对应的除法结果。根据这些已知关系,计算一些未知的除法结果。
具体地:
- 给定
equations数组,其中equations[i] = [Ai, Bi]表示变量Ai / Bi - 给定
values数组,其中values[i]表示Ai / Bi的结果 - 给定
queries数组,其中queries[j] = [Cj, Dj]表示需要计算Cj / Dj的值
返回所有查询的答案。如果某个查询无法计算,返回 -1.0。
示例 1:
输入:
equations = [["a","b"],["b","c"]]
values = [2.0,3.0]
queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]
输出:[6.0, 0.5, -1.0, 1.0, -1.0]
解释:
已知:a / b = 2.0, b / c = 3.0
推导:
a / c = (a/b) * (b/c) = 2.0 * 3.0 = 6.0
b / a = 1 / (a/b) = 1 / 2.0 = 0.5
a / e = -1.0 (e不存在)
a / a = 1.0 (自己除自己)
x / x = -1.0 (x不存在)
约束条件:
1 <= equations.length <= 20— 已知关系不多equations[i].length == 2— 每个关系包含两个变量1 <= Ai.length, Bi.length <= 5— 变量名长度不超过5values.length == equations.length— 每个关系都有对应值0.0 < values[i] <= 20.0— 除法结果为正数1 <= queries.length <= 20— 查询数量不多- 所有变量名由小写字母组成
🧪 边界用例(面试必考)
| 用例类型 | 输入 | 期望输出 | 考察点 |
|---|---|---|---|
| 直接关系 | equations=[["a","b"]], queries=[["a","b"]] | [2.0] | 已知关系直接返回 |
| 反向关系 | equations=[["a","b"]], queries=[["b","a"]] | [0.5] | 倒数关系 |
| 链式推导 | equations=[["a","b"],["b","c"]], queries=[["a","c"]] | [6.0] | 路径相乘 |
| 不存在变量 | queries=[["x","y"]] | [-1.0] | 变量未定义 |
| 自除 | queries=[["a","a"]] | [1.0] | 自己除自己=1 |
| 无路径 | equations=[["a","b"],["c","d"]], queries=[["a","d"]] | [-1.0] | 两个独立子图 |
💡 思路引导
生活化比喻
想象你在做汇率换算。
🐌 笨办法:别人告诉你"1美元=7人民币","1人民币=0.15欧元",你想知道"1美元能换多少欧元",你得拿出草稿纸:先算1美元→人民币,再算人民币→欧元,层层递推,非常麻烦。
🚀 聪明办法:把所有汇率关系画成一张带权图:
- 每个货币是一个节点
- 如果知道"A换B的比率",就画一条A→B的边,权重是比率
- 要计算"A换C的比率",就在图中找一条A到C的路径,把路径上所有权重相乘
- 如果找不到路径,说明这两个货币没有换算关系
关键洞察
将除法关系建模为带权有向图,除法计算 = 图中路径权重的乘积
🧠 解题思维链
这一节模拟你在面试中"从零开始思考"的过程。
Step 1:理解题目 → 锁定输入输出
- 输入:已知除法关系
equations和结果values,待查询关系queries - 输出:每个查询的除法结果,不存在返回
-1.0 - 限制:变量数量不多(最多40个),关系数量不多(最多20条)
Step 2:先想笨办法(暴力法)
对每个查询 [C, D],尝试用已知关系 [A, B] 拼凑:
-
如果
C == A且D == B,直接返回value -
如果
C == B且D == A,返回1/value -
否则尝试找中间变量E,使得
C/E和E/D都已知 -
时间复杂度:O(Q * E^N) — Q个查询,每个查询可能需要尝试指数级的组合
-
瓶颈在哪:没有系统化的路径搜索方法,重复计算很多
Step 3:瓶颈分析 → 优化方向
暴力法的问题是:
- 没有结构化存储关系,查找效率低
- 没有系统的路径搜索算法
优化思路:
- 图建模:用带权图存储除法关系
- 节点:变量
- 边:除法关系(A→B权重为A/B的值)
- 路径搜索:用DFS或BFS在图中搜索从起点到终点的路径
- 权重计算:路径上所有边的权重相乘
Step 4:选择武器
- 选用:带权有向图 + DFS路径搜索
- 理由:
- 图结构天然适合表达"关系网络"
- DFS能系统地搜索所有可能路径
- 变量数量少,DFS性能足够
🔑 模式识别提示:当题目涉及"关系传递"、"路径推导"、"依赖链",优先考虑图遍历(DFS/BFS)
🔑 解法一:DFS暴力搜索(朴素解法)
思路
- 构建带权有向图:对每个
equations[i] = [A, B],创建两条边:- A → B,权重为
values[i] - B → A,权重为
1/values[i](反向关系)
- A → B,权重为
- 对每个查询
[C, D],从C开始DFS搜索到D的路径 - 路径上所有权重相乘得到结果
图解过程
示例:equations = [["a","b"],["b","c"]], values = [2.0,3.0]
Step 1: 构建带权图
a --2.0--> b --3.0--> c
a <-0.5--- b <-0.33-- c
图表示(邻接表):
a: {b: 2.0}
b: {a: 0.5, c: 3.0}
c: {b: 0.333}
Step 2: 查询 a / c
DFS从a开始:
访问a (当前乘积=1.0)
→ 访问b (当前乘积=1.0*2.0=2.0)
→ 访问c (当前乘积=2.0*3.0=6.0) ✓ 到达目标!
返回6.0
Step 3: 查询 b / a
DFS从b开始:
访问b (当前乘积=1.0)
→ 访问a (当前乘积=1.0*0.5=0.5) ✓ 到达目标!
返回0.5
Step 4: 查询 a / e
e不在图中 → 返回-1.0
Python代码
from typing import List
from collections import defaultdict
def calcEquation(equations: List[List[str]], values: List[float],
queries: List[List[str]]) -> List[float]:
"""
解法一:DFS暴力搜索
思路:构建带权图,对每个查询DFS搜索路径并计算权重乘积
"""
# 1. 构建带权有向图
graph = defaultdict(dict)
for (A, B), value in zip(equations, values):
graph[A][B] = value # A -> B
graph[B][A] = 1.0 / value # B -> A (反向)
def dfs(start, end, visited):
"""
从start搜索到end,返回路径权重乘积
返回-1.0表示无法到达
"""
# 边界情况
if start not in graph or end not in graph:
return -1.0
if start == end:
return 1.0
visited.add(start)
# 遍历所有邻居
for neighbor, weight in graph[start].items():
if neighbor in visited:
continue
# 递归搜索neighbor到end的路径
sub_result = dfs(neighbor, end, visited)
if sub_result != -1.0:
return weight * sub_result # 当前边权重 * 子路径权重
return -1.0 # 所有路径都走不通
# 2. 处理所有查询
results = []
for C, D in queries:
results.append(dfs(C, D, set()))
return results
# ✅ 测试
equations = [["a", "b"], ["b", "c"]]
values = [2.0, 3.0]
queries = [["a", "c"], ["b", "a"], ["a", "e"], ["a", "a"], ["x", "x"]]
print(calcEquation(equations, values, queries))
# 期望输出:[6.0, 0.5, -1.0, 1.0, -1.0]
复杂度分析
-
时间复杂度:O(Q * (V + E)) — Q个查询,每个查询最坏DFS整个图
- V是变量数量(最多40),E是边数(最多40*2=80)
- 具体地说:20个查询,每个查询最多访问40个节点和80条边,约1600次操作
-
空间复杂度:O(V + E) — 邻接表O(E) + DFS递归栈O(V)
优缺点
- ✅ 逻辑直观,易于实现
- ✅ 处理了双向关系(正向和反向)
- ❌ 每个查询都重新DFS,没有利用之前的计算结果
- ❌ 变量不存在时仍会尝试搜索
🏆 解法二:优化DFS + 提前判断(最优解)
优化思路
在解法一的基础上增加优化:
- 提前判断:查询前先检查起点和终点是否存在于图中
- 特殊情况快速返回:如果起点==终点,直接返回1.0
💡 关键想法:大部分优化来自"快速排除无效查询",而不是改变DFS算法本身。对于此题数据规模,DFS已经足够高效。
Python代码
def calcEquation_optimized(equations: List[List[str]], values: List[float],
queries: List[List[str]]) -> List[float]:
"""
解法二:优化DFS + 提前判断(最优解)
思路:在DFS基础上增加边界检查和特殊情况处理
"""
# 构建图
graph = defaultdict(dict)
for (A, B), value in zip(equations, values):
graph[A][B] = value
graph[B][A] = 1.0 / value
def dfs(start, end, visited):
"""DFS搜索路径,返回权重乘积"""
# 提前判断:变量不存在
if start not in graph or end not in graph:
return -1.0
# 特殊情况:自己除自己
if start == end:
return 1.0
visited.add(start)
# 遍历邻居
for neighbor, weight in graph[start].items():
if neighbor in visited:
continue
sub_result = dfs(neighbor, end, visited)
if sub_result != -1.0:
return weight * sub_result
return -1.0
# 处理查询
results = []
for C, D in queries:
results.append(dfs(C, D, set()))
return results
# ✅ 测试
print(calcEquation_optimized(equations, values, queries))
# 期望输出:[6.0, 0.5, -1.0, 1.0, -1.0]
复杂度分析
-
时间复杂度:O(Q * (V + E)) — 与解法一相同,但实际运行更快
- 提前判断避免了无效的DFS
-
空间复杂度:O(V + E) — 相同
为什么是最优解
- 时间复杂度已达理论最优 — 必须至少遍历一次查询路径才能计算结果
- 空间复杂度合理 — O(V+E)用于存储图,无法避免
- 实现简洁 — 代码清晰,不易出错
- 边界处理完善 — 覆盖了所有特殊情况
- 对于此题数据规模(最多20个查询,40个变量)已足够高效
⚡ 解法三:BFS路径搜索(可选)
思路
用BFS代替DFS搜索路径,每一层记录累积的权重。
from collections import deque
def calcEquation_bfs(equations: List[List[str]], values: List[float],
queries: List[List[str]]) -> List[float]:
"""
解法三:BFS路径搜索
思路:用BFS层序遍历搜索路径
"""
graph = defaultdict(dict)
for (A, B), value in zip(equations, values):
graph[A][B] = value
graph[B][A] = 1.0 / value
def bfs(start, end):
if start not in graph or end not in graph:
return -1.0
if start == end:
return 1.0
queue = deque([(start, 1.0)]) # (节点, 累积权重)
visited = {start}
while queue:
node, product = queue.popleft()
# 遍历邻居
for neighbor, weight in graph[node].items():
if neighbor in visited:
continue
new_product = product * weight
if neighbor == end:
return new_product
visited.add(neighbor)
queue.append((neighbor, new_product))
return -1.0
return [bfs(C, D) for C, D in queries]
BFS和DFS性能相近,选择哪个取决于个人喜好。DFS代码稍简洁,BFS更直观。
🐍 Pythonic 写法
利用字典推导和三元表达式简化:
def calcEquation_pythonic(equations, values, queries):
from collections import defaultdict
# 一行构建图
graph = defaultdict(dict)
for (A, B), val in zip(equations, values):
graph[A][B], graph[B][A] = val, 1 / val
def dfs(x, y, visited):
return (
-1.0 if x not in graph or y not in graph else
1.0 if x == y else
next((w * dfs(n, y, visited | {x})
for n, w in graph[x].items()
if n not in visited and (res := dfs(n, y, visited | {x})) != -1.0),
-1.0)
)
return [dfs(C, D, set()) for C, D in queries]
⚠️ 面试建议:Pythonic写法可读性较差,面试中推荐清晰版本。
📊 解法对比
| 维度 | 解法一:DFS暴力 | 🏆 解法二:优化DFS(最优) | 解法三:BFS |
|---|---|---|---|
| 时间复杂度 | O(Q*(V+E)) | O(Q(V+E))* ← 同,但更快 | O(Q*(V+E)) |
| 空间复杂度 | O(V+E) | O(V+E) | O(V+E) |
| 代码难度 | 简单 | 简单 | 简单 |
| 边界处理 | 一般 | 完善 ← 提前判断 | 完善 |
| 面试推荐 | ⭐⭐ | ⭐⭐⭐ ← 首选 | ⭐⭐ |
| 适用场景 | 基础版本 | 通用,面试标准解 | 偏好迭代思维 |
为什么解法二是最优:
- 保持了O(Q*(V+E))的理论复杂度
- 增加了提前判断,实际运行更快
- 代码简洁,边界情况处理完善
- 对于此题数据规模已足够高效,无需复杂优化
面试建议:
- 先花30秒分析问题:"这是一个带权图的路径搜索问题"
- 说明建图思路:"每个除法关系对应两条有向边(正向和反向)"
- 实现🏆优化DFS:"用DFS搜索路径,路径权重相乘"
- 强调边界处理:"提前判断变量是否存在,特殊处理自除"
- 手动测试边界用例:变量不存在、自除、链式推导
🎤 面试现场
模拟面试中的完整对话流程,帮你练习"边想边说"。
面试官:请你解决一下这道除法求值问题。
你:(审题30秒)好的,这道题给定一些除法关系,要根据这些关系计算新的除法结果。
让我分析一下...这本质上是一个带权图的路径搜索问题:
- 每个变量是图的一个节点
- 如果知道
A / B = x,就建两条边:A→B权重x,B→A权重1/x - 计算
C / D,就是在图中找C到D的路径,路径上所有权重相乘
我的思路是:
- 用邻接表构建带权有向图
- 对每个查询,用DFS从起点搜索到终点
- 路径上的权重相乘得到结果
- 找不到路径或变量不存在返回-1.0
面试官:很好,请写一下代码。
你:(边写边说)
# 1. 构建图
graph = defaultdict(dict)
for (A, B), value in zip(equations, values):
graph[A][B] = value # 正向边
graph[B][A] = 1.0 / value # 反向边(倒数)
def dfs(start, end, visited):
# 边界:变量不存在
if start not in graph or end not in graph:
return -1.0
# 特殊情况:自己除自己
if start == end:
return 1.0
visited.add(start)
# 遍历邻居
for neighbor, weight in graph[start].items():
if neighbor in visited:
continue
sub_result = dfs(neighbor, end, visited)
if sub_result != -1.0:
return weight * sub_result # 累乘权重
return -1.0
# 2. 处理所有查询
return [dfs(C, D, set()) for C, D in queries]
面试官:为什么要建双向边?
你:因为如果知道 a/b=2,就能推导出 b/a=1/2。建双向边可以同时支持正向和反向查询,避免在DFS中额外处理反向关系。
面试官:测试一下?
你:用示例走一遍 a/c:
- 图:a→b(2.0), b→a(0.5), b→c(3.0), c→b(0.33)
- DFS从a开始:
- 访问a → 访问邻居b → 权重2.0
- 从b继续 → 访问邻居c → 权重3.0
- 到达目标c → 返回 2.0 * 3.0 = 6.0 ✓
再测 a/e:
- e不在graph中 → 提前返回-1.0 ✓
面试官:复杂度是多少?
你:
- 时间:O(Q * (V+E)),Q个查询,每个查询DFS最坏遍历所有节点和边
- 空间:O(V+E),邻接表O(E),DFS栈O(V)
- 对于此题:最多20查询 * (40节点+80边) ≈ 2400次操作,非常高效
高频追问
| 追问 | 应答策略 |
|---|---|
| "能用BFS做吗?" | "可以。BFS用队列存储(节点,累积权重)对,层序遍历到目标。性能与DFS相近,选哪个看个人偏好。" |
| "如果查询量很大呢?" | "可以用Floyd-Warshall预处理所有点对最短路径,将查询优化到O(1)。但此题查询不多,预处理O(V³)不划算。" |
| "权重可能是负数吗?" | "此题保证正数。如果有负数,DFS/BFS仍适用(只是乘积可能为负),但要小心负环(乘积趋向0或∞)。" |
| "如果有环怎么办?" | "visited集合会阻止重复访问,避免无限循环。环本身不影响结果,因为我们只关心路径权重乘积。" |
| "实际应用?" | "货币汇率换算、单位转换(米→厘米→英寸)、知识图谱推理(关系传递)。" |
🎓 知识点总结
Python技巧卡片 🐍
# 技巧1:defaultdict(dict)嵌套字典 — 带权图存储
from collections import defaultdict
graph = defaultdict(dict)
graph['a']['b'] = 2.0 # a->b权重2.0
# 技巧2:zip并行遍历两个列表
for (A, B), value in zip(equations, values):
graph[A][B] = value
# 技巧3:set作为visited避免重复访问
visited = set()
visited.add('a')
if 'a' in visited: # O(1)判断
pass
💡 底层原理(选读)
为什么除法可以用乘法路径表示?
数学原理:除法的传递性
- 如果 a/b = x, b/c = y
- 则 a/c = (a/b) * (b/c) = x * y
为什么反向边是倒数?
- 如果 a/b = x
- 则 b/a = 1 / (a/b) = 1/x
为什么路径权重相乘?
- 路径 a → b → c 表示 a/c = (a/b) * (b/c)
- 每条边的权重是除法结果,连乘得到最终结果
算法模式卡片 📐
- 模式名称:带权图路径搜索(DFS/BFS)
- 适用条件:关系可传递,需要路径推导
- 识别关键词:"汇率换算"、"单位转换"、"关系推导"、"依赖链计算"
- 核心要素:
- 带权有向图(邻接表存储)
- DFS/BFS路径搜索
- 路径权重累积(相乘或相加)
- 模板代码:
# 带权图DFS模板
def dfs_weighted_path(graph, start, end, visited, product=1.0):
if start == end:
return product
visited.add(start)
for neighbor, weight in graph[start].items():
if neighbor not in visited:
result = dfs_weighted_path(graph, neighbor, end, visited, product * weight)
if result != -1: # 找到路径
return result
return -1 # 无路径
易错点 ⚠️
-
忘记建反向边
- 错误:只建
A → B,导致无法计算B / A - 正确:同时建
A → B和B → A,权重互为倒数
- 错误:只建
-
visited未传递或未回溯
- 错误:每次DFS用全局visited,导致后续查询受影响
- 正确:每个查询用独立的
set(),或在回溯时remove
-
权重计算错误
- 错误:路径权重相加而非相乘
- 正确:除法的传递是乘法,路径权重必须相乘
🏗️ 工程实战(选读)
这个算法思想在真实项目中的应用,让你知道"学了有什么用"。
-
场景1:货币汇率系统
- 金融系统中多币种汇率转换
- 用带权图存储汇率,支持间接换算
-
场景2:单位转换库
- 物理量单位转换(米→厘米→英寸→英尺)
- 用图表示转换关系,自动推导间接转换
-
场景3:知识图谱推理
- 关系数据库中实体关系推导
- A是B的父亲,B是C的父亲 → A是C的祖父
-
场景4:API依赖分析
- 微服务间调用链路分析
- 服务A调用B,B调用C → A间接依赖C
🏋️ 举一反三
完成本课后,试试这些同类题目来巩固知识:
| 题目 | 难度 | 相关知识点 | 提示 |
|---|---|---|---|
| LeetCode 1162. 地图分析 | Medium | BFS多源最短路径 | 从所有陆地同时BFS扩散 |
| LeetCode 785. 判断二分图 | Medium | DFS/BFS染色 | 用两种颜色标记,冲突则非二分图 |
| LeetCode 765. 情侣牵手 | Hard | 并查集/图环计数 | 找错位情侣形成的环 |
| LeetCode 133. 克隆图 | Medium | DFS/BFS + 哈希表 | 遍历同时复制节点和边 |
📝 课后小测
试试这道变体题,不要看答案,自己先想5分钟!
题目:给定除法关系,判断是否存在矛盾(如 a/b=2, b/c=3, a/c=5,但2*3≠5)。
💡 提示(实在想不出来再点开)
用DFS遍历图,对于每条边 u → v,检查是否存在另一条路径从u到v,如果存在且两条路径的权重不同,说明矛盾。
✅ 参考答案
def hasContradiction(equations, values):
graph = defaultdict(dict)
for (A, B), val in zip(equations, values):
# 检查是否已有A到B的路径
existing = dfs_find_path(graph, A, B)
if existing != -1 and abs(existing - val) > 1e-5:
return True # 矛盾!
graph[A][B] = val
graph[B][A] = 1 / val
return False
def dfs_find_path(graph, start, end, visited=None):
if visited is None:
visited = set()
if start not in graph or end not in graph:
return -1
if start == end:
return 1.0
visited.add(start)
for neighbor, weight in graph[start].items():
if neighbor not in visited:
sub = dfs_find_path(graph, neighbor, end, visited)
if sub != -1:
return weight * sub
return -1
核心思路:每加入一条新边前,先检查图中是否已有从A到B的路径,如果有且值不同,说明矛盾。
如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。