题目描述
思路解析动画文字版
笨办法:把每个查询都去等式列表里硬凑乘除,凑不出就放弃——但 a/c 在等式里根本没直接给,硬凑会漏掉「a→b→c」这种间接链。我们需要一个能自动沿着关系链走下去的结构,那就是图。
a/b=2 意味着:从 a 到 b 比值是 2(a→b 权 2),反过来从 b 到 a 就是 1/2(b→a 权 1/2)。只建单向会漏查询——所以每条等式都要正反各建一条边。后面图里把这对正反比值合写成「2 ⇄ 1/2」标在同一条线上。
建图① a/b=2:第一条等式 a/b=2 落地。一条等式同时给了两个方向:a→b 是 2、b→a 是 1/2,互为倒数。边上的「2 ⇄ 1/2」一眼标出这对正反比值。
建图② b/c=3:第二条等式 b/c=3 落地,标「3 ⇄ 1/3」。现在两条关系都在:a–b 是 2 ⇄ 1/2、b–c 是 3 ⇄ 1/3,每条线都同时带着正反两个互为倒数的比值——注意 a 和 c 之间没有直接边。
查询 a/c · 从 a 出发:查询 a/c 开始:站在起点 a,手里的累乘乘积 prod 初始为 1。a 和 c 不直接相连,得靠 DFS 一步步往 c 探。
走第一步 a→b · 乘 2:从 a 看邻居,挑 a→b 这个方向、取正向比值 2,踩上去:prod = 1 × 2 = 2。现在站在 b,把 a 标记成已访问,免得待会儿沿 b→a 又绕回去。
走第二步 b→c · 乘 3:在 b 看邻居:a 已访问跳过,挑 b→c 方向、取正向比值 3,踩上去:prod = 2 × 3 = 6。当前站在 c——正是要找的目标。
到达 c · 返回 6:当前点 c 就是目标 c,DFS 命中!把路径上的比值乘起来:2 × 3 = 6.0,这就是 a/c 的答案。整条链 a→b→c 沿途累乘,间接关系被算出来了。
查不到的情况 · 返回 -1:如果查询里出现没建过的变量(比如 x),或者两点不连通,DFS 走遍邻居也到不了目标。这种查不到的情况,统一返回 -1.0。
一句话本质:把除法等式连成带权图,未知的比值就是图上一条路径的权重乘积。
雷区实演 · 忘了反边:故意只建正边演示后果:箭头只朝右,没有反向的「⇄ 1/v」。查 c/a 时站在 c,c 一条出边都没有,DFS 立刻返回 -1。但真实答案是 1/6——漏建反边就会错。这就是为什么每条等式必须正反各建一条。
面试追问:面试官最爱追的就是「DFS vs 并查集」的取舍——查询多用并查集预处理,查询少 DFS 更直观。
边界三连:三种边界:变量缺席返回 -1、自身相除返回 1(但前提是它在图里)、连通断裂返回 -1。判存在要排在 x==y 之前。
迁移阶梯:练熟这题,去同类迁移:把比值换成距离/汇率/概率(如最大概率路径 1514)就是同一套带权图 DFS/BFS。点开右侧图论专题,或问小欧「这题还能怎么优化」继续深挖。
参考代码
class Solution: def calcEquation(self, equations, values, queries): g = {} # 邻接表 g[节点]=[(邻居, 比值)] for (a, b), v in zip(equations, values): g.setdefault(a, []).append((b, v)) # a→b 权 v g.setdefault(b, []).append((a, 1 / v)) # b→a 权 1/v def dfs(x, y, seen): if x not in g or y not in g: # 变量没出现过 return -1.0 if x == y: # 走到目标 return 1.0 seen.add(x) for nei, w in g[x]: # 看每个邻居 if nei not in seen: sub = dfs(nei, y, seen) if sub != -1.0: # 邻居那条路通 return w * sub # 累乘比值 return -1.0 # 所有邻居都不通 return [dfs(a, b, set()) for a, b in queries]复杂度
- 建图:O(E),E 条等式,每条建正反两条边,一次遍历搞定
- 时间复杂度:O(Q · (V+E)),Q 个查询,每个查询最坏要 DFS 遍历整张图:V 个点、E 条边各访问一次
- 空间复杂度:O(V + E),邻接表存 V 个点、2E 条边;外加 DFS 递归栈和 seen 集合最深 O(V)
可套用的代码模板
记住这个挖空骨架:建图「正反两条边、反边取倒数」+ DFS「三个出口(不存在/到目标/走不通)+ 沿途累乘 + seen 防环」。任何「已知部分两两关系、求间接关系」的题都能套。
# 1) 一条带权关系 → 正反两条边for (a, b), v in zip(relations, values): g.setdefault(a, []).append((b, v)) # 正向 g.setdefault(b, []).append((a, 1 / v)) # 反向取倒数# 2) DFS 沿路累乘,三个出口def dfs(x, y, seen): if x not in g or y not in g: return -1.0 # 出口①不存在 if x == y: return 1.0 # 出口②到目标 seen.add(x) for nei, w in g[x]: if nei in seen: continue # 防环 sub = dfs(nei, y, seen) if sub != -1.0: return w * sub # 累乘并上抛 return -1.0 # 出口③走不通易错点
面试追问把动画讲成自己的话
追问除了 DFS,还能怎么做?
追问查询非常多时怎么优化?
追问为什么不用担心浮点精度爆掉?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
删除无效的括号
LeetCode 301 · 困难 · 沿着 图 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题