LeetCode 399 除法求值


文章目录

摘要

这道题其实挺有意思的,它不是让你去做真正的除法,而是让你去"推理"不同变量之间的关系。

比如说:

  • 我知道 a / b = 2.0
  • 又知道 b / c = 3.0
    那我就能推出 a / c = 6.0

这道题的本质,其实是一个 图(Graph)问题

每个变量是一个节点,a / b = 2.0 代表 a -> b 的一条有向边,权重是 2.0;反过来 b -> a 也是一条边,权重是 1/2.0。

当我们要回答类似 a / c 这种问题时,本质就是在问:

"从 a 出发到 c 的路径上,所有权重连乘是多少?"

听起来抽象?别急,我们慢慢拆开看。

描述

题目输入包括三部分:

  1. 等式 equations :如 [["a","b"],["b","c"]]
  2. 对应的值 values :如 [2.0, 3.0],表示 a / b = 2.0b / c = 3.0
  3. 查询 queries :如 [["a","c"],["b","a"],["a","e"]]

要求我们输出每个查询的结果,比如上面例子中:

  • a / c = 6.0
  • b / a = 0.5
  • a / e = -1.0(因为 e 未定义)

题解答案

核心思路其实有两种常见方案:

  1. 图 + DFS(深度优先搜索)

    • 建立一个加权有向图;
    • 对每个查询执行一次 DFS;
    • 在路径中不断累乘权重;
    • 如果能从起点走到终点,就返回结果,否则返回 -1.0
  2. 并查集(Union-Find with Weight)

    • 把每个变量放进并查集;
    • 记录它与父节点之间的权重;
    • 查询时通过比较"根节点"是否相同来判断是否连通;
    • 并根据权重关系算出最终比值。

在 Swift 里,用 DFS 实现会更直观,也方便演示。下面我们就用 DFS 的方法写一个可运行的版本。

题解代码分析

下面是完整的 Swift 实现版本:

swift 复制代码
import Foundation

class Solution {
    private var graph = [String: [(String, Double)]]()

    init() {}

    // 主函数
    func calcEquation(_ equations: [[String]], _ values: [Double], _ queries: [[String]]) -> [Double] {
        buildGraph(equations, values)
        
        var results: [Double] = []
        for query in queries {
            let (start, end) = (query[0], query[1])
            var visited = Set<String>()
            let result = dfs(start, end, 1.0, &visited)
            results.append(result)
        }
        return results
    }

    // 建图
    private func buildGraph(_ equations: [[String]], _ values: [Double]) {
        for (i, eq) in equations.enumerated() {
            let (a, b) = (eq[0], eq[1])
            let val = values[i]
            
            graph[a, default: []].append((b, val))
            graph[b, default: []].append((a, 1.0 / val))
        }
    }

    // DFS 搜索路径
    private func dfs(_ start: String, _ end: String, _ value: Double, _ visited: inout Set<String>) -> Double {
        guard let neighbors = graph[start] else {
            return -1.0
        }
        if start == end { return value }
        visited.insert(start)
        
        for (next, weight) in neighbors {
            if !visited.contains(next) {
                let result = dfs(next, end, value * weight, &visited)
                if result != -1.0 {
                    return result
                }
            }
        }
        return -1.0
    }
}

代码拆解讲解

1. 建图阶段

我们把每个等式看成两条有向边:

  • a -> b 权重为 val
  • b -> a 权重为 1 / val

例如:

txt 复制代码
a / b = 2.0
b / c = 3.0

构建出的图是:

txt 复制代码
a -> b (2.0)
b -> a (0.5)
b -> c (3.0)
c -> b (0.333)
2. DFS 搜索阶段

DFS 的任务就是找一条从 startend 的路径,并在沿途不断乘上边权。

举个例子:

  • 要算 a / c
  • a 出发 → 找到邻居 b(权重 2.0);
  • 继续从 b 出发 → 找到邻居 c(权重 3.0);
  • 最终结果是 2.0 * 3.0 = 6.0

如果某个节点没有通往目标的路径,就返回 -1.0

3. visited 集合

用来防止死循环,比如 a / bb / a 互相指向时,DFS 可能无限递归。

示例测试及结果

我们用题目中的例子来跑一下看看。

swift 复制代码
let equations = [["a","b"],["b","c"]]
let values = [2.0, 3.0]
let queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]

let sol = Solution()
let result = sol.calcEquation(equations, values, queries)
print(result)

输出结果:

txt 复制代码
[6.0, 0.5, -1.0, 1.0, -1.0]

非常符合预期。

你可以再试一个:

swift 复制代码
let eq2 = [["a","b"],["b","c"],["bc","cd"]]
let val2 = [1.5, 2.5, 5.0]
let q2 = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]]

let sol2 = Solution()
print(sol2.calcEquation(eq2, val2, q2))

输出:

txt 复制代码
[3.75, 0.4, 5.0, 0.2]

完美通过

时间复杂度

  • 建图阶段: O(N),其中 N 是等式数量;
  • 每次查询: 在最坏情况下 DFS 会遍历所有节点,复杂度 O(V + E),但因为题目规模小(最多 20),可视为常数;
  • 总体复杂度: O(N + Q × (V + E)),对这道题来说性能非常充足。

空间复杂度

  • 图存储结构使用 O(V + E);
  • DFS 递归栈深度 O(V);
  • 所以总体空间复杂度是 O(V + E)

总结

这道题虽然看起来是"除法",但核心完全是图的遍历。

可以把它理解成一个"变量之间的依赖网络",我们只需要在这个网络里找到一条从起点到终点的路径,并把沿途的"权重"连乘起来。

总结一下两种解法的优缺点:

方法 思路 优点 缺点 适用场景
DFS/BFS 图遍历 简单直观 查询多时会重复计算 小规模题或快速实现
并查集 动态连通性 查询快 实现复杂 查询频繁或动态更新场景

实际工程中的应用类比:

  • 货币兑换系统:每种货币相当于一个节点,汇率就是边权;
  • 单位换算系统 :比如 km/mcm/mm 的换算;
  • 分布式依赖图:不同微服务之间的调用链条计算。

如果你能透彻理解这道题,基本上已经掌握了「图建模 + 路径搜索 + 权重传递」的通用套路,这在算法与工程中都极其常见。

相关推荐
仰泳的熊猫3 小时前
LeetCode:98. 验证二叉搜索树
数据结构·c++·算法·leetcode
JAVA学习通4 小时前
零基础OSS组件(Java)
java·linux·leetcode
Python智慧行囊4 小时前
图像处理(三)--开运算与闭运算,梯度运算,礼帽与黑帽
人工智能·算法·计算机视觉
前端小L4 小时前
动态规划的“细节魔鬼”:子序列 vs 子数组 —— 最长重复子数组
算法·动态规划
草莓熊Lotso4 小时前
《算法闯关指南:优选算法--二分查找》--19.x的平方根,20.搜索插入位置
java·开发语言·c++·算法
sali-tec4 小时前
C# 基于halcon的视觉工作流-章46-不匀面划痕
人工智能·算法·计算机视觉·c#
yuniko-n5 小时前
【力扣 SQL 50】连接
数据库·后端·sql·算法·leetcode
胖咕噜的稞达鸭5 小时前
算法入门:专题二---滑动窗口(长度最小的子数组)更新中
c语言·数据结构·c++·算法·推荐算法
海洲探索-Hydrovo10 小时前
TTP Aether X 天通透传模块丨国产自主可控大数据双向通讯定位模组
网络·人工智能·科技·算法·信息与通信