📖 第92课:除法求值

3 阅读16分钟

想系统提升编程能力、查看更完整的学习路线,欢迎访问 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 — 变量名长度不超过5
  • values.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 == AD == B,直接返回 value

  • 如果 C == BD == A,返回 1/value

  • 否则尝试找中间变量E,使得 C/EE/D 都已知

  • 时间复杂度:O(Q * E^N) — Q个查询,每个查询可能需要尝试指数级的组合

  • 瓶颈在哪:没有系统化的路径搜索方法,重复计算很多

Step 3:瓶颈分析 → 优化方向

暴力法的问题是:

  • 没有结构化存储关系,查找效率低
  • 没有系统的路径搜索算法

优化思路:

  • 图建模:用带权图存储除法关系
    • 节点:变量
    • 边:除法关系(A→B权重为A/B的值)
  • 路径搜索:用DFS或BFS在图中搜索从起点到终点的路径
  • 权重计算:路径上所有边的权重相乘

Step 4:选择武器

  • 选用:带权有向图 + DFS路径搜索
  • 理由:
    • 图结构天然适合表达"关系网络"
    • DFS能系统地搜索所有可能路径
    • 变量数量少,DFS性能足够

🔑 模式识别提示:当题目涉及"关系传递"、"路径推导"、"依赖链",优先考虑图遍历(DFS/BFS)


🔑 解法一:DFS暴力搜索(朴素解法)

思路

  1. 构建带权有向图:对每个 equations[i] = [A, B],创建两条边:
    • A → B,权重为 values[i]
    • B → A,权重为 1/values[i](反向关系)
  2. 对每个查询 [C, D],从C开始DFS搜索到D的路径
  3. 路径上所有权重相乘得到结果

图解过程

示例: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

  DFSa开始:
    访问a (当前乘积=1.0)
      → 访问b (当前乘积=1.0*2.0=2.0)
        → 访问c (当前乘积=2.0*3.0=6.0) ✓ 到达目标!
  返回6.0

Step 3: 查询 b / a

  DFSb开始:
    访问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. 提前判断:查询前先检查起点和终点是否存在于图中
  2. 特殊情况快速返回:如果起点==终点,直接返回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) — 相同

为什么是最优解

  1. 时间复杂度已达理论最优 — 必须至少遍历一次查询路径才能计算结果
  2. 空间复杂度合理 — O(V+E)用于存储图,无法避免
  3. 实现简洁 — 代码清晰,不易出错
  4. 边界处理完善 — 覆盖了所有特殊情况
  5. 对于此题数据规模(最多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))的理论复杂度
  • 增加了提前判断,实际运行更快
  • 代码简洁,边界情况处理完善
  • 对于此题数据规模已足够高效,无需复杂优化

面试建议:

  1. 先花30秒分析问题:"这是一个带权图的路径搜索问题"
  2. 说明建图思路:"每个除法关系对应两条有向边(正向和反向)"
  3. 实现🏆优化DFS:"用DFS搜索路径,路径权重相乘"
  4. 强调边界处理:"提前判断变量是否存在,特殊处理自除"
  5. 手动测试边界用例:变量不存在、自除、链式推导

🎤 面试现场

模拟面试中的完整对话流程,帮你练习"边想边说"。

面试官:请你解决一下这道除法求值问题。

:(审题30秒)好的,这道题给定一些除法关系,要根据这些关系计算新的除法结果。

让我分析一下...这本质上是一个带权图的路径搜索问题:

  • 每个变量是图的一个节点
  • 如果知道 A / B = x,就建两条边:A→B权重x,B→A权重1/x
  • 计算 C / D,就是在图中找C到D的路径,路径上所有权重相乘

我的思路是:

  1. 用邻接表构建带权有向图
  2. 对每个查询,用DFS从起点搜索到终点
  3. 路径上的权重相乘得到结果
  4. 找不到路径或变量不存在返回-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)
  • 适用条件:关系可传递,需要路径推导
  • 识别关键词:"汇率换算"、"单位转换"、"关系推导"、"依赖链计算"
  • 核心要素:
    1. 带权有向图(邻接表存储)
    2. DFS/BFS路径搜索
    3. 路径权重累积(相乘或相加)
  • 模板代码:
# 带权图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  # 无路径

易错点 ⚠️

  1. 忘记建反向边

    • 错误:只建 A → B,导致无法计算 B / A
    • 正确:同时建 A → BB → A,权重互为倒数
  2. visited未传递或未回溯

    • 错误:每次DFS用全局visited,导致后续查询受影响
    • 正确:每个查询用独立的 set(),或在回溯时remove
  3. 权重计算错误

    • 错误:路径权重相加而非相乘
    • 正确:除法的传递是乘法,路径权重必须相乘

🏗️ 工程实战(选读)

这个算法思想在真实项目中的应用,让你知道"学了有什么用"。

  • 场景1:货币汇率系统

    • 金融系统中多币种汇率转换
    • 用带权图存储汇率,支持间接换算
  • 场景2:单位转换库

    • 物理量单位转换(米→厘米→英寸→英尺)
    • 用图表示转换关系,自动推导间接转换
  • 场景3:知识图谱推理

    • 关系数据库中实体关系推导
    • A是B的父亲,B是C的父亲 → A是C的祖父
  • 场景4:API依赖分析

    • 微服务间调用链路分析
    • 服务A调用B,B调用C → A间接依赖C

🏋️ 举一反三

完成本课后,试试这些同类题目来巩固知识:

题目难度相关知识点提示
LeetCode 1162. 地图分析MediumBFS多源最短路径从所有陆地同时BFS扩散
LeetCode 785. 判断二分图MediumDFS/BFS染色用两种颜色标记,冲突则非二分图
LeetCode 765. 情侣牵手Hard并查集/图环计数找错位情侣形成的环
LeetCode 133. 克隆图MediumDFS/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 学习资料都在这里,后续复习和拓展会更省时间。