当前:LC399 · 除法求值 · 首次出现于 Day 35 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC399图算法图 DFS · 除法求值图搜索前线

除法求值

equations=[["a","b"],["b","c"]],values=[2,3],queries a/c。

题目是什么

equations=[["a","b"],["b","c"]],values=[2,3],queries a/c。

解决什么问题

当前节点的合法邻居是谁,访问标记何时写入?

核心结论

建加权有向图,a->b 权 2 则 b->a 权 1/2;DFS/BFS 求路径积。

01交互算法精讲

先说结论:这道题到底解决什么

怎样从“equations=[["a","b"],["b","c"]],values=[2,3],queries a/c。”推导出 图 DFS · 除法求值,并证明每次状态变化都不会漏掉答案?

中心结论:建加权有向图,a->b 权 2 则 b->a 权 1/2;DFS/BFS 求路径积。

读完必须能回答
  1. 1.暴力方案在哪里重复计算,为什么仍然是正确基线?
  2. 2.当前节点的合法邻居是谁,访问标记何时写入?
  3. 3.不变量“路径积=边权连乘。”为什么能保证算法安全前进?
02交互算法精讲

完整题目与题意拆解

给出方程式 A / B = k, 其中 A 和 B 均为代表字符串的变量, k 是一个浮点型数字。根据已知方程式求解问题,并返回计算结果。如果结果不存在,则返回 -1.0。

示例 : 给定 a / b = 2.0, b / c = 3.0 问题: a / c = ?, b / a = ?, a / e = ?, a / a = ?, x / x = ? 返回 [6.0, 0.5, -1.0, 1.0, -1.0 ]

输入为: vector > equations, vector & values, vector > queries(方程式,方程式结果,问题方程式), 其中 equations.size() == values.size(),即方程式的长度与方程式结果长度相等(程式与结果一一对应),并且结果值均为正数。以上为方程式的描述。 返回vector 类型。

假设输入总是有效的。你可以假设除法运算中不会出现除数为0的情况,且不存在任何矛盾的结果。

在本站主例中,equations=[["a","b"],["b","c"]],values=[2,3],queries a/c。

算法最终需要得到或观察:a/c=2/3。

把题目翻译成状态
  • 输入:equations=[["a","b"],["b","c"]],values=[2,3],queries a/c。
  • 机器需要维护:graph、visited、累积积。
  • 最终可观察结果:a/c=2/3。
动画 1 · 题意扫描

先看清算法到底要维护什么

先建立输入、目标、输出和第一批状态,不急着进入模板。

Step 1/20%
题目与输入建立输入、目标与算法心智

克隆图:每个结点复制一份,边保持一致

正在加载算法场景...
03交互算法精讲

第一层方案:暴力做法

重复从多个位置搜索会反复访问同一状态;统一的 visited 与前线结构让每个状态只处理一次。

暴力方案的价值是确认题意并提供正确性基线。它通常会覆盖所有候选, 但没有保存已经确认的信息,因此同一状态会被重新计算。

动画 2 · 暴力重复

重复工作究竟发生在哪里

把重复读取或重复搜索的区域明确标出,再决定优化必须保存什么。

Step 1/20%
先做对:建立暴力基线枚举所有候选并完整验证

克隆图:每个结点复制一份,边保持一致

正在加载算法场景...
优化方向:建加权有向图,a->b 权 2 则 b->a 权 1/2;DFS/BFS 求路径积。
04交互算法精讲

整体地图:先做什么,再做什么

  1. 1建模把输入翻译成“图搜索前线”,明确答案需要观察什么。
  2. 2状态只维护 graph、visited、累积积。
  3. 3转移每一步按照 建加权有向图,a->b 权 2 则 b->a 权 1/2;DFS/BFS 求路径积。
  4. 4收尾读取 a/c=2/3。,并复核边界与复杂度。
05交互算法精讲

图搜索前线:核心概念

先把题目对象翻译成节点和边,再选择 BFS 或 DFS。

这道题不是为了记住一个组件名称,而是为了让状态具有可解释的语义:graph、visited、累积积。

核心不变量
  • 路径积=边权连乘。
动画 3 · 核心概念

建立“图搜索前线”心智模型

用主例建立核心状态,先预测下一步,再公开正确分支和理由。

Step 1/30%
题目与输入建立输入、目标与算法心智

克隆图:每个结点复制一份,边保持一致

正在加载算法场景...
06交互算法精讲

核心机制:状态如何一步步变化

给出一些字母变量的倍数关系,问给出任意两个字母的倍数是多少。 这一题可以用 DFS 或者并查集来解题。先来看看 DFS 的做法。先建图。每个字母或者字母组合可以看做成一个节点,给出的 `equations` 关系可以看成两个节点之间的有向边。每条有向边都有权值。那么问题可以转换成是否存在一条从起点节点到终点节点的路径,如果存在,输出这条路径上所有有向边权值的累乘结果。如果不存在这条路径,就返回 -1 。如果给的起点和终点不在给出的节点集里面,也输出 -1 。 再来看看并查集的做法。先将每两个有倍数关系的节点做并查集 `union()` 操作。例如 A/B = 2,那么把 B 作为 `parent` 节点,`parents[A] = {B,2}`,`parents[B] = {B,1}`,B 指向自己是 1 。还有一个关系是 `B/C=3`,由于 B 已经在并查集中了,所以这个时候需要把这个关系反过来,处理成 `C/B = 1/3` ,即 `parents[C] = {B,1/3}`。这样把所有有关系的字母都 `union()` 起来。如何求任意两个字母的倍数关系呢?例如 `A/C = ?` 在并查集中查找,可以找到 `parents[C] == parents[A] == B`,那么就用 `parents[A]/parents[C] = 2/(1/3) = 6`。为什么可以这样做呢?因为 `A/B = 2`,`C/B = 1/3`,那么 `A/C = (A/B)/(C/B)` 即 `parents[A]/parents[C] = 2/(1/3) = 6`。

建加权有向图,a->b 权 2 则 b->a 权 1/2;DFS/BFS 求路径积。

执行过程中持续维护:graph、visited、累积积。

正确性依赖以下不变量:路径积=边权连乘。

面试时可以压缩为:建图+DFS 求连通积 O(E+Q·(V+E))。

落到当前题,执行机制可以压缩为:建加权有向图,a->b 权 2 则 b->a 权 1/2;DFS/BFS 求路径积。每一次更新都必须保持核心不变量,而不是只让样例碰巧得到正确答案。

动画 4 · 机制构建

一次状态转移为什么成立

集中观察一次选择、计算和状态写回,让变量变化与原因同时出现。

Step 1/50%
克隆结点 1 → 11map[old]=new Node

创建 clone(1)

正在加载算法场景...
07交互算法精讲

正确性证明:为什么不会漏答案

初始化

初始化:算法开始时,全部合法候选仍在状态表示范围内;路径积=边权连乘。

保持

保持:执行“建加权有向图,a->b 权 2 则 b->a 权 1/2;DFS/BFS 求路径积。”时,只删除已经能证明不可能的候选,并把新信息写回 graph、visited、累积积。

终止

终止:没有待处理状态或达到命中条件时,当前可观察结果就是“a/c=2/3。”。

正确性抓手不是“样例跑通”,而是每一帧结束后仍能复述:路径积=边权连乘。
08交互算法精讲

完整执行过程

  1. 1题目与输入equations=[["a","b"],["b","c"]],values=[2,3],queries a/c。 因为:建加权有向图,a->b 权 2 则 b->a 权 1/2;DFS/BFS 求路径积。
  2. 2哈希 map: oldId → cloneId遇结点先查 map。 因为:避免重复克隆与死循环。
  3. 3克隆结点 1 → 11复制结点值。 因为:再递归克隆邻居并连边。
  4. 4克隆结点 2 → 12复制结点值。 因为:再递归克隆邻居并连边。
  5. 5克隆结点 3 → 13复制结点值。 因为:再递归克隆邻居并连边。
  6. 6深拷贝图完成map 中所有结点已复制。 因为:O(V+E) DFS + map。
  7. 7哈希 map: oldId → cloneId遇结点先查 map。 因为:避免重复克隆与死循环。
  8. 8收尾与复杂度a/c=2/3。 因为:时间 O(n) · 空间 O(1)。建加权有向图,a->b 权 2 则 b->a 权 1/2;DFS/BFS 求路径积。
动画 5 · 完整执行

从输入完整走到可观察结果

从主例第一帧运行到答案,时间轴始终显示当前状态、因果解释和下一步。

Step 1/80%
题目与输入建立输入、目标与算法心智

克隆图:每个结点复制一份,边保持一致

正在加载算法场景...
09交互算法精讲

把动画和 Go 代码逐行对应

代码窗口不会按容易漂移的固定数字行号硬绑动画,而是根据当前语义阶段, 在完整 Go 实现中定位选择、计算、写回或返回分支。当前变量与高亮行一起变化。

动画 6 · 代码映射

让每个动作都落到 Go 分支

重新执行关键分支,只显示当前代码附近窗口,并说明这行为什么在此刻运行。

Step 1/60%
哈希 map: oldId → cloneIdDFS/BFS clone

克隆图:每个结点复制一份,边保持一致

正在加载算法场景...
10交互算法精讲

完整 Go 提交代码与最小测试

完整 Go 解法
type stringUnionFind struct {
	parents map[string]string
	vals    map[string]float64
}

func (suf stringUnionFind) add(x string) {
	if _, ok := suf.parents[x]; ok {
		return
	}
	suf.parents[x] = x
	suf.vals[x] = 1.0
}

func (suf stringUnionFind) find(x string) string {
	p := suf.parents[x]
	if x != p {
		pp := suf.find(p)
		suf.vals[x] *= suf.vals[p]
		suf.parents[x] = pp
	}
	return suf.parents[x]
}

func (suf stringUnionFind) union(x, y string, v float64) {
	suf.add(x)
	suf.add(y)
	px, py := suf.find(x), suf.find(y)
	suf.parents[px] = py
	// x / px = vals[x]
	// x / y = v
	// 由上面 2 个式子就可以得出 px = v * vals[y] / vals[x]
	suf.vals[px] = v * suf.vals[y] / suf.vals[x]
}

func calcEquation(equations [][]string, values []float64, queries [][]string) []float64 {
	res, suf := make([]float64, len(queries)), stringUnionFind{parents: map[string]string{}, vals: map[string]float64{}}
	for i := 0; i < len(values); i++ {
		suf.union(equations[i][0], equations[i][1], values[i])
	}
	for i := 0; i < len(queries); i++ {
		x, y := queries[i][0], queries[i][1]
		if _, ok := suf.parents[x]; ok {
			if _, ok := suf.parents[y]; ok {
				if suf.find(x) == suf.find(y) {
					res[i] = suf.vals[x] / suf.vals[y]
				} else {
					res[i] = -1
				}
			} else {
				res[i] = -1
			}
		} else {
			res[i] = -1
		}
	}
	return res
}
最小测试集合
func main() {
    // 1. 主例
    //    输入:mode="clone-graph", numCourses=3
    //    期望:a/c=2/3。
    //
    // 2. 失败 / 未命中
    //    检查:除零/query 不存在返回 -1。
    //
    // 3. 边界
    //    空输入、单元素、最小合法规模,以及答案恰好落在边界的情况。
    //
    // 4. 迁移
    //    LC200 岛屿;LC133 克隆
}

Go 参考实现基于 halfrost/LeetCode-Go 的 MIT 许可代码整理,并按本站教学结构补充解释与动画映射。

11交互算法精讲

正确性与复杂度

时间复杂度 O(n)

执行过程中只保留仍可能影响答案的状态。建加权有向图,a->b 权 2 则 b->a 权 1/2;DFS/BFS 求路径积。

空间复杂度 O(1)

额外状态主要用于维护:graph、visited、累积积。

终局不变量
  • 路径积=边权连乘。
12交互算法精讲

最容易写错的地方

错误 1

除零/query 不存在返回 -1。

边界复查

必须额外检查空输入、单元素、未命中或不可达情况,以及恰好落在边界的输入。

13交互算法精讲

最后复盘:带走逻辑链

  1. 1题意equations=[["a","b"],["b","c"]],values=[2,3],queries a/c。
  2. 2重复重复从多个位置搜索会反复访问同一状态;统一的 visited 与前线结构让每个状态只处理一次。
  3. 3优化建加权有向图,a->b 权 2 则 b->a 权 1/2;DFS/BFS 求路径积。
  4. 4证明路径积=边权连乘。
  5. 5复杂度时间 O(n),空间 O(1)
面试表达:建图+DFS 求连通积 O(E+Q·(V+E))。
迁移练习
  • LC200 岛屿
  • LC133 克隆