LeetCode 399. 除法求值:TS实现+图文解析

0 阅读7分钟

在LeetCode中等难度题目中,399. 除法求值是一道非常经典的「图论应用」题目。它看似是数学除法问题,实则可以转化为「带权重的图遍历」,用 BFS 或 DFS 就能轻松解决。今天就带大家从题目解析、思路拆解,到 TS 代码实现、易错点避坑,一步步吃透这道题。

一、题目解读(清晰易懂版)

题目核心很简单:给你一组除法等式(比如 a/b = 2.0、b/c = 3.0),再给你一组查询(比如 a/c、b/a),要求你计算每个查询的结果,无法确定的结果返回 -1.0。

关键约束(必看,避坑关键)

  • 输入永远有效,不会出现除数为 0 的情况,也不会有矛盾的等式(比如不会同时出现 a/b=2 和 a/b=3)。

  • 如果查询中出现了「没在等式里出现过的变量」(比如查询 a/e,而 e 从未出现),直接返回 -1.0。

  • 如果两个变量是同一个(比如查询 a/a),结果固定为 1.0。

示例帮你理解

输入:

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/c = (a/b) * (b/c) = 2.0 * 3.0 = 6.0

  • b/a = 1 / (a/b) = 1/2.0 = 0.5

  • a/e 中 e 未出现,返回 -1.0

  • a/a 固定为 1.0

  • x 未出现,返回 -1.0

二、核心思路:把除法等式转化为「带权重的图」

这道题的关键突破点,是将「变量」和「除法关系」转化为图的「节点」和「带权重的边」—— 这是图论在算法题中的典型应用,新手一定要掌握这个转化思维。

转化逻辑(重中之重)

对于每个等式 Ai / Bi = values[i],我们可以转化为两条有向边:

  1. 从 Ai 指向 Bi,权重为 values[i](表示 Ai ÷ Bi = 权重);

  2. 从 Bi 指向 Ai,权重为 1/values[i](表示 Bi ÷ Ai = 1/权重)。

举个例子:a/b = 2.0 → 边 a→b 权重 2.0,边 b→a 权重 0.5。

查询怎么解?

查询 [Cj, Dj] 本质是求「从节点 Cj 到节点 Dj 的路径上,所有边的权重乘积」—— 因为路径上的权重乘积,就是 Cj ÷ Dj 的结果。

比如查询 a/c:路径是 a→b→c,权重乘积是 2.0 * 3.0 = 6.0,正好是 a/c 的结果。

遍历方式选择

我们用 BFS(广度优先搜索)来遍历图,原因很简单:BFS 适合「找两个节点之间的路径」,并且能在遍历过程中累积权重乘积,找到目标节点就直接返回结果,效率很高。

三、TS 代码实现(带详细注释,可直接复制运行)

下面是修复后的完整代码(原代码有一个小bug,已修复,后面会讲易错点),每一行都加了注释,新手也能看懂:

function calcEquation(equations: string[][], values: number[], queries: string[][]): number[] {
  // 1. 构建图:邻接表存储,格式为 { 节点: { 邻居: 权重 } }
  // 比如 graph[a][b] = 2.0 表示 a ÷ b = 2.0
  const graph: Record<string, Record<string, number>> = {};

  // 初始化图并添加边(处理每一个等式)
  for (let i = 0; i < equations.length; i++) {
    const [a, b] = equations[i]; // 取出等式的两个变量
    const value = values[i];      // 取出等式对应的比值

    // 如果节点还没在图中,初始化一个空对象(避免报错)
    if (!graph[a]) graph[a] = {};
    if (!graph[b]) graph[b] = {};

    // 添加两条有向边:a→b 和 b→a
    graph[a][b] = value;
    graph[b][a] = 1 / value; // 题目保证无除数为0,无需判断value===0
  }

  // 2. 定义 BFS 函数:计算 start ÷ end 的结果
  const bfs = (start: string, end: string): number => {
    // 边界情况1:起始节点或目标节点不存在(未在等式中出现),返回 -1.0
    if (!graph[start] || !graph[end]) return -1.0;
    // 边界情况2:起始节点和目标节点是同一个,返回 1.0
    if (start === end) return 1.0;

    // 队列:存储 [当前节点, 到当前节点的权重乘积]
    // 初始值:从start出发,乘积为1.0(start ÷ start = 1.0)
    const queue: [string, number][] = [[start, 1.0]];
    // 已访问集合:避免重复访问节点,防止死循环
    const visited = new Set<string>([start]);

    // BFS 核心循环:遍历队列中的所有节点
    while (queue.length > 0) {
      // 取出队列头部的节点和当前乘积(shift() 取出第一个元素)
      const [current, product] = queue.shift()!;
      // 取出当前节点的所有邻居(即与current有除法关系的变量)
      const nexts = graph[current];

      // 遍历每个邻居
      for (let next in nexts) {
        // 跳过已访问的节点,避免重复遍历
        if (visited.has(next)) {
          continue;
        }

        // 计算到当前邻居的权重乘积(累积之前的乘积 * 当前边的权重)
        const newProduct = product * graph[current][next];

        // 找到目标节点,直接返回乘积(就是 start ÷ end 的结果)
        if (next === end) return newProduct;

        // 标记邻居为已访问,加入队列,继续遍历
        visited.add(next);
        queue.push([next, newProduct]);
      }
    }

    // 遍历完所有路径都没找到目标节点,返回 -1.0
    return -1.0;
  };

  // 3. 处理所有查询:对每个查询调用BFS,收集结果
  return queries.map(([c, d]) => bfs(c, d));
}

// 测试用例(可直接运行验证)
const equations = [["a","b"],["b","c"]];
const values = [2.0, 3.0];
const queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]];
console.log(calcEquation(equations, values, queries)); // 输出: [6.0, 0.5, -1.0, 1.0, -1.0]

四、易错点避坑(新手常犯,必看!)

很多同学写这道题时,代码看似正确,但运行结果却不对,大多是踩了以下几个坑,尤其是第一个:

坑1:BFS 中「跳过已访问节点」的逻辑写反(最常见)

原代码中曾有这样一行:

if (!visited.has(next)) { continue; }

这是典型的逻辑错误!本意是「跳过已访问的节点」,结果写成了「跳过未访问的节点」,导致所有有效路径都被跳过,代码永远返回 -1.0。

正确写法是:

if (visited.has(next)) { continue; }

坑2:多余的「除数为0」判断

题目明确说明「除法运算中不会出现除数为 0 的情况」,所以不需要写 values[i] === 0 ? 0 : 1 / values[i],直接写 1 / values[i] 即可,简化代码且避免冗余。

坑3:忘记处理「变量不存在」的情况

如果查询中的变量(比如 e、x)没有在 equations 中出现,graph 中就没有这个节点,此时必须返回 -1.0,这也是题目明确要求的。

坑4:忽略「同一节点」的情况

当查询的两个变量是同一个(比如 a/a),结果固定为 1.0,这是一个容易忽略的边界 case,需要单独处理。

五、代码运行原理(图文辅助理解)

1. 图的构建过程

对于 equations = [["a","b"],["b","c"]],values = [2.0, 3.0],构建的图如下:

  • graph[a] = { b: 2.0 }

  • graph[b] = { a: 0.5, c: 3.0 }

  • graph[c] = { b: 1/3.0 }

2. BFS 遍历过程(以查询 a/c 为例)

  1. 初始化队列:[[a, 1.0]],已访问:{a}

  2. 取出 [a, 1.0],遍历 a 的邻居 b(未访问),计算 newProduct = 1.0 * 2.0 = 2.0;b 不是目标 c,加入队列,已访问变为 {a, b}。

  3. 取出 [b, 2.0],遍历 b 的邻居 a(已访问,跳过)、c(未访问),计算 newProduct = 2.0 * 3.0 = 6.0;c 是目标节点,直接返回 6.0。

六、总结与拓展

核心收获

这道题的本质是「图的权重路径查询」,核心思维是「将数学关系转化为图结构」—— 这种转化能力在很多算法题中都很有用(比如后续会遇到的「单词接龙」「网络延迟时间」等)。

用 BFS 遍历的优势是:找到目标节点就立即返回,不需要遍历所有节点,效率较高;如果换成 DFS,逻辑类似,只是遍历顺序不同。

拓展思考

如果题目中的等式很多(比如 1000 个),查询也很多(比如 1000 个),BFS 每次查询都要重新遍历,效率会偏低。此时可以用「并查集(Union-Find)」优化,将每个变量的「父节点」和「权重(相对于父节点的比值)」存储起来,查询时直接通过并查集找到两个变量的公共父节点,快速计算比值。

最后

LeetCode 中等题的核心不是「难」,而是「找对思路」。这道题看似是数学题,实则是图论的基础应用,只要掌握「除法等式转图」的思维,再注意避开几个易错点,就能轻松 AC。