Article / 文章
LeetCode第399题:除法求值
给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。 另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, D
博客摘要:本文深入解析LeetCode第399题“除法求值”,这是一道中等难度的图论与并查集题目。文章详细分析了三种解法:图的DFS遍历、图的BFS遍历和带权并查集。通过构建有向图建立变量间的除法关系,并使用路径查找算法求解未知的除法结果。适合想要深入理解图论算法和并查集优化的读者,帮助掌握复杂关系建模和路径查找的解题思路。
题目描述
给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。
另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。
返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。
注意: 输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结果。
注意: 未在等式列表中出现的变量是未定义的,因此无法确定它们的答案。
示例 1:
输入:equations = [["a","b"],["b","c"]], values = [2.0,3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]
输出:[6.00000,0.50000,-1.00000,1.00000,-1.00000]
解释:
条件: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 ]
示例 2:
输入:equations = [["a","b"],["b","c"],["bc","cd"]], values = [1.5,2.5,5.0], queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]]
输出:[3.75000,0.40000,5.00000,0.20000]
示例 3:
输入:equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"],["a","c"],["x","y"]]
输出:[0.50000,2.00000,-1.00000,-1.00000]
提示:
1 <= equations.length <= 20equations[i].length == 21 <= Ai.length, Bi.length <= 5values.length == equations.length0.0 < values[i] <= 20.01 <= queries.length <= 20queries[i].length == 21 <= Cj.length, Dj.length <= 5Ai, Bi, Cj, Dj由小写英文字母与数字组成
题目链接:LeetCode 399. 除法求值
解题思路
这道题本质上是一个图论问题。我们可以将每个变量看作图中的节点,每个等式看作图中的边,边权表示除法的结果。
核心思路
-
图的构建:对于每个等式
a / b = value,我们在图中添加:- 边
a → b,权重为value - 边
b → a,权重为1/value
- 边
-
路径查找:对于查询
c / d,我们需要在图中找到从节点c到节点d的路径,路径上所有权重的乘积就是答案。 -
特殊情况处理:
- 如果
c或d不在图中,返回-1.0 - 如果
c == d且节点存在,返回1.0 - 如果无法找到路径,返回
-1.0
- 如果
算法原理
方法一:DFS(深度优先搜索)
构建图后,对于每个查询,使用DFS从起点搜索到终点,记录路径上权重的乘积。
方法二:BFS(广度优先搜索)
使用BFS搜索路径,相比DFS,BFS能找到最短路径(虽然这题中路径长度不影响结果)。
方法三:带权并查集
使用并查集的思想,每个节点维护到根节点的权重比值,通过路径压缩优化查询效率。
复杂度分析
DFS/BFS解法
- 时间复杂度:O((N + M) × Q)
- N为节点数,M为边数,Q为查询数
- 每次查询最坏需要遍历整个图
- 空间复杂度:O(N + M)
- 存储图的邻接表和递归栈空间
并查集解法
- 时间复杂度:O(M × α(N) + Q × α(N))
- 构建并查集:O(M × α(N))
- 查询:O(Q × α(N))
- 空间复杂度:O(N)
- 只需要存储父节点和权重数组
图解思路
给定:equations = [["a","b"],["b","c"]], values = [2.0,3.0]
构建图:
a ---(2.0)---> b ---(3.0)---> c
<--(0.5)--- <--(0.33)---
查询 a/c:
路径:a → b → c
权重乘积:2.0 × 3.0 = 6.0
查询 b/a:
路径:b → a
权重:0.5
图的邻接表表示
| 节点 | 邻接节点及权重 |
|---|---|
| a | [(b, 2.0)] |
| b | [(a, 0.5), (c, 3.0)] |
| c | [(b, 0.333)] |
代码实现
C# 实现
public class Solution {
public double[] CalcEquation(IList<IList<string>> equations, double[] values, IList<IList<string>> queries) {
// 方法1:DFS解法
return SolveDFS(equations, values, queries);
}
// 方法1:DFS解法
private double[] SolveDFS(IList<IList<string>> equations, double[] values, IList<IList<string>> queries) {
// 构建图
var graph = new Dictionary<string, List<(string, double)>>();
for (int i = 0; i < equations.Count; i++) {
string a = equations[i][0];
string b = equations[i][1];
double value = values[i];
if (!graph.ContainsKey(a)) graph[a] = new List<(string, double)>();
if (!graph.ContainsKey(b)) graph[b] = new List<(string, double)>();
graph[a].Add((b, value)); // a / b = value
graph[b].Add((a, 1.0 / value)); // b / a = 1/value
}
// 处理查询
double[] results = new double[queries.Count];
for (int i = 0; i < queries.Count; i++) {
string start = queries[i][0];
string end = queries[i][1];
if (!graph.ContainsKey(start) || !graph.ContainsKey(end)) {
results[i] = -1.0;
} else if (start == end) {
results[i] = 1.0;
} else {
var visited = new HashSet<string>();
results[i] = DFS(graph, start, end, visited, 1.0);
}
}
return results;
}
private double DFS(Dictionary<string, List<(string, double)>> graph, string start, string end, HashSet<string> visited, double currentValue) {
if (start == end) return currentValue;
visited.Add(start);
foreach (var (neighbor, weight) in graph[start]) {
if (!visited.Contains(neighbor)) {
double result = DFS(graph, neighbor, end, visited, currentValue * weight);
if (result != -1.0) {
return result;
}
}
}
visited.Remove(start);
return -1.0;
}
// 方法2:BFS解法
public double[] CalcEquationBFS(IList<IList<string>> equations, double[] values, IList<IList<string>> queries) {
// 构建图(同DFS)
var graph = new Dictionary<string, List<(string, double)>>();
for (int i = 0; i < equations.Count; i++) {
string a = equations[i][0];
string b = equations[i][1];
double value = values[i];
if (!graph.ContainsKey(a)) graph[a] = new List<(string, double)>();
if (!graph.ContainsKey(b)) graph[b] = new List<(string, double)>();
graph[a].Add((b, value));
graph[b].Add((a, 1.0 / value));
}
double[] results = new double[queries.Count];
for (int i = 0; i < queries.Count; i++) {
results[i] = BFS(graph, queries[i][0], queries[i][1]);
}
return results;
}
private double BFS(Dictionary<string, List<(string, double)>> graph, string start, string end) {
if (!graph.ContainsKey(start) || !graph.ContainsKey(end)) return -1.0;
if (start == end) return 1.0;
var queue = new Queue<(string node, double value)>();
var visited = new HashSet<string>();
queue.Enqueue((start, 1.0));
visited.Add(start);
while (queue.Count > 0) {
var (currentNode, currentValue) = queue.Dequeue();
foreach (var (neighbor, weight) in graph[currentNode]) {
if (neighbor == end) {
return currentValue * weight;
}
if (!visited.Contains(neighbor)) {
visited.Add(neighbor);
queue.Enqueue((neighbor, currentValue * weight));
}
}
}
return -1.0;
}
}
Python 实现
from collections import defaultdict, deque
class Solution:
def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
# 方法1:DFS解法
return self.solve_dfs(equations, values, queries)
def solve_dfs(self, equations, values, queries):
# 构建图
graph = defaultdict(list)
for i, (a, b) in enumerate(equations):
value = values[i]
graph[a].append((b, value)) # a / b = value
graph[b].append((a, 1.0 / value)) # b / a = 1/value
def dfs(start, end, visited, current_value):
if start == end:
return current_value
visited.add(start)
for neighbor, weight in graph[start]:
if neighbor not in visited:
result = dfs(neighbor, end, visited, current_value * weight)
if result != -1.0:
return result
visited.remove(start)
return -1.0
# 处理查询
results = []
for start, end in queries:
if start not in graph or end not in graph:
results.append(-1.0)
elif start == end:
results.append(1.0)
else:
visited = set()
result = dfs(start, end, visited, 1.0)
results.append(result)
return results
def solve_bfs(self, equations, values, queries):
# 构建图
graph = defaultdict(list)
for i, (a, b) in enumerate(equations):
value = values[i]
graph[a].append((b, value))
graph[b].append((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:
current_node, current_value = queue.popleft()
for neighbor, weight in graph[current_node]:
if neighbor == end:
return current_value * weight
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, current_value * weight))
return -1.0
return [bfs(start, end) for start, end in queries]
def solve_union_find(self, equations, values, queries):
# 方法3:带权并查集
def find(x):
if x != parent[x]:
origin_parent = parent[x]
parent[x] = find(parent[x])
weight[x] *= weight[origin_parent]
return parent[x]
def union(x, y, value):
root_x, root_y = find(x), find(y)
if root_x != root_y:
parent[root_x] = root_y
weight[root_x] = weight[y] * value / weight[x]
# 初始化
parent = {}
weight = {}
for i, (a, b) in enumerate(equations):
if a not in parent:
parent[a] = a
weight[a] = 1.0
if b not in parent:
parent[b] = b
weight[b] = 1.0
union(a, b, values[i])
# 处理查询
results = []
for a, b in queries:
if a not in parent or b not in parent:
results.append(-1.0)
elif find(a) != find(b):
results.append(-1.0)
else:
results.append(weight[a] / weight[b])
return results
C++ 实现
class Solution {
public:
vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
return solveDFS(equations, values, queries);
}
private:
// 方法1:DFS解法
vector<double> solveDFS(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
// 构建图
unordered_map<string, vector<pair<string, double>>> graph;
for (int i = 0; i < equations.size(); i++) {
string a = equations[i][0];
string b = equations[i][1];
double value = values[i];
graph[a].emplace_back(b, value);
graph[b].emplace_back(a, 1.0 / value);
}
vector<double> results;
for (auto& query : queries) {
string start = query[0];
string end = query[1];
if (graph.find(start) == graph.end() || graph.find(end) == graph.end()) {
results.push_back(-1.0);
} else if (start == end) {
results.push_back(1.0);
} else {
unordered_set<string> visited;
double result = dfs(graph, start, end, visited, 1.0);
results.push_back(result);
}
}
return results;
}
double dfs(unordered_map<string, vector<pair<string, double>>>& graph,
const string& start, const string& end,
unordered_set<string>& visited, double currentValue) {
if (start == end) return currentValue;
visited.insert(start);
for (auto& [neighbor, weight] : graph[start]) {
if (visited.find(neighbor) == visited.end()) {
double result = dfs(graph, neighbor, end, visited, currentValue * weight);
if (result != -1.0) {
return result;
}
}
}
visited.erase(start);
return -1.0;
}
public:
// 方法2:BFS解法
vector<double> calcEquationBFS(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
unordered_map<string, vector<pair<string, double>>> graph;
// 构建图
for (int i = 0; i < equations.size(); i++) {
string a = equations[i][0];
string b = equations[i][1];
double value = values[i];
graph[a].emplace_back(b, value);
graph[b].emplace_back(a, 1.0 / value);
}
vector<double> results;
for (auto& query : queries) {
results.push_back(bfs(graph, query[0], query[1]));
}
return results;
}
private:
double bfs(unordered_map<string, vector<pair<string, double>>>& graph,
const string& start, const string& end) {
if (graph.find(start) == graph.end() || graph.find(end) == graph.end()) {
return -1.0;
}
if (start == end) return 1.0;
queue<pair<string, double>> q;
unordered_set<string> visited;
q.push({start, 1.0});
visited.insert(start);
while (!q.empty()) {
auto [currentNode, currentValue] = q.front();
q.pop();
for (auto& [neighbor, weight] : graph[currentNode]) {
if (neighbor == end) {
return currentValue * weight;
}
if (visited.find(neighbor) == visited.end()) {
visited.insert(neighbor);
q.push({neighbor, currentValue * weight});
}
}
}
return -1.0;
}
};
执行结果
C# 实现
- 执行用时:89 ms(DFS)/ 85 ms(BFS)
- 内存消耗:27.4 MB(DFS)/ 27.6 MB(BFS)
Python 实现
- 执行用时:36 ms(DFS)/ 32 ms(BFS)/ 28 ms(并查集)
- 内存消耗:16.8 MB(DFS)/ 16.9 MB(BFS)/ 16.6 MB(并查集)
C++ 实现
- 执行用时:0 ms(DFS)/ 0 ms(BFS)
- 内存消耗:7.8 MB(DFS)/ 8.1 MB(BFS)
性能对比
| 解法 | 语言 | 执行用时 | 内存消耗 | 特点 |
|---|---|---|---|---|
| DFS | C++ | 0 ms | 7.8 MB | 实现简洁,性能最优 |
| BFS | C++ | 0 ms | 8.1 MB | 找最短路径,内存稍高 |
| 并查集 | Python | 28 ms | 16.6 MB | 查询效率高,适合多次查询 |
| DFS | C# | 89 ms | 27.4 MB | 代码清晰,调试方便 |
代码亮点
- 🎯 图模型转换:巧妙地将除法关系转换为图的边权关系
- 💡 双向建图:对每个等式建立双向边,提高查询效率
- 🔍 路径查找优化:提供DFS、BFS、并查集三种不同的路径查找策略
- 🎨 特殊情况处理:完善处理节点不存在、自除、无路径等边界情况
常见错误分析
- 🚫 忘记建立反向边:只建立 a→b 的边,忘记建立 b→a 的反向边
- 🚫 权重计算错误:反向边的权重应该是原权重的倒数
- 🚫 访问标记错误:在DFS中忘记回溯时移除访问标记
- 🚫 边界条件遗漏:没有处理节点不存在或相同节点的情况
解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| DFS | O((N+M)×Q) | O(N+M) | 实现简单,内存友好 | 可能深度过大 |
| BFS | O((N+M)×Q) | O(N+M) | 找到最短路径 | 需要队列额外空间 |
| 并查集 | O(M×α(N)+Q×α(N)) | O(N) | 查询效率高 | 实现复杂,理解难度大 |
| Floyd算法 | O(N³) | O(N²) | 一次计算所有路径 | 预处理时间长 |
相关题目
- LeetCode 200. 岛屿数量 - 中等
- LeetCode 547. 省份数量 - 中等
- LeetCode 684. 冗余连接 - 中等
- LeetCode 785. 判断二分图 - 中等
📖 系列导航
🔥 算法专题合集 - 查看完整合集
📢 关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新第399题。
💬 互动交流
感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。
如果这篇文章对你有帮助,请:
- 👍 点个赞,让更多人看到这篇文章
- 📁 收藏文章,方便后续查阅复习
- 🔔 关注作者,获取更多高质量算法题解
- 💭 评论区留言,分享你的解题思路或提出疑问
你的支持是我持续分享的动力!
💡 一起进步:算法学习路上不孤单,欢迎一起交流学习!