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] 。每个 AiBi 是一个表示单个变量的字符串。

另有一些以数组 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 <= 20
  • equations[i].length == 2
  • 1 <= Ai.length, Bi.length <= 5
  • values.length == equations.length
  • 0.0 < values[i] <= 20.0
  • 1 <= queries.length <= 20
  • queries[i].length == 2
  • 1 <= Cj.length, Dj.length <= 5
  • Ai, Bi, Cj, Dj 由小写英文字母与数字组成

题目链接LeetCode 399. 除法求值

解题思路

这道题本质上是一个图论问题。我们可以将每个变量看作图中的节点,每个等式看作图中的边,边权表示除法的结果。

核心思路

  1. 图的构建:对于每个等式 a / b = value,我们在图中添加:

    • a → b,权重为 value
    • b → a,权重为 1/value
  2. 路径查找:对于查询 c / d,我们需要在图中找到从节点 c 到节点 d 的路径,路径上所有权重的乘积就是答案。

  3. 特殊情况处理

    • 如果 cd 不在图中,返回 -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 代码清晰,调试方便

代码亮点

  1. 🎯 图模型转换:巧妙地将除法关系转换为图的边权关系
  2. 💡 双向建图:对每个等式建立双向边,提高查询效率
  3. 🔍 路径查找优化:提供DFS、BFS、并查集三种不同的路径查找策略
  4. 🎨 特殊情况处理:完善处理节点不存在、自除、无路径等边界情况

常见错误分析

  1. 🚫 忘记建立反向边:只建立 a→b 的边,忘记建立 b→a 的反向边
  2. 🚫 权重计算错误:反向边的权重应该是原权重的倒数
  3. 🚫 访问标记错误:在DFS中忘记回溯时移除访问标记
  4. 🚫 边界条件遗漏:没有处理节点不存在或相同节点的情况

解法对比

解法 时间复杂度 空间复杂度 优点 缺点
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²) 一次计算所有路径 预处理时间长

相关题目


📖 系列导航

🔥 算法专题合集 - 查看完整合集

📢 关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新第399题。


💬 互动交流

感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。

如果这篇文章对你有帮助,请:

  • 👍 点个赞,让更多人看到这篇文章
  • 📁 收藏文章,方便后续查阅复习
  • 🔔 关注作者,获取更多高质量算法题解
  • 💭 评论区留言,分享你的解题思路或提出疑问

你的支持是我持续分享的动力!

💡 一起进步:算法学习路上不孤单,欢迎一起交流学习!