除法求值

标签: 深度优先搜索 广度优先搜索 并查集 数组 最短路

难度: Medium

给你一个变量对数组 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 ]
注意:x 是未定义的 => -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 由小写英文字母与数字组成

Submission

运行时间: 20 ms

内存: 16.1 MB

class Solution:
    def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
        graph = {}  
        for (s, e), v in zip(equations, values):
            if s not in graph:
                graph[s] = {}   # 存储邻节点的哈希表
            graph[s][e] = v     # 生成一条s指向e,权重为v的路径,表示 s / e = v
            if e not in graph:
                graph[e] = {}
            graph[e][s] = 1 / v # 生成一条反向路径,权重为1 / v,表示 e / s = 1 /v
            graph[s][s] = 1.0   # 生成一个指向自己、权重为1的路径,表示自己除自己等于1
            graph[e][e] = 1.0

        queue = []
        n = len(queries)
        ans = [-1.0]*n

        for i, (x, y) in enumerate(queries): 
            if x not in graph or y not in graph:
                continue
            
            queue = [[x, 1.0]]
            visited = [x]
            while queue:
                node, mul = queue.pop(0)
                for adj, weight in graph[node].items(): 
                    if adj == y: 
                        ans[i] = mul*weight
                        break
                    if adj not in visited:
                        visited.append(adj)
                        queue.append([adj, mul*weight])
        
        return ans

Explain

这个题解基于图的广度优先搜索(BFS)算法。首先构建一个图,其中节点表示变量,边表示变量之间的除法关系。对于每个等式 a/b=v,添加一条从 a 到 b 的有向边,权重为 v,同时添加一条从 b 到 a 的反向边,权重为 1/v。对于每个查询 x/y,从 x 开始进行 BFS 搜索,同时记录路径上的权重乘积,直到找到 y 或搜索完整个连通分量。最后将搜索结果存入答案数组。

时间复杂度: O(M*N)

空间复杂度: O(N)

class Solution:
    def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
        graph = {}  
        for (s, e), v in zip(equations, values):
            if s not in graph:
                graph[s] = {}   # 存储邻节点的哈希表
            graph[s][e] = v     # 生成一条s指向e,权重为v的路径,表示 s / e = v
            if e not in graph:
                graph[e] = {}
            graph[e][s] = 1 / v # 生成一条反向路径,权重为1 / v,表示 e / s = 1 /v
            graph[s][s] = 1.0   # 生成一个指向自己、权重为1的路径,表示自己除自己等于1
            graph[e][e] = 1.0

        queue = []
        n = len(queries)
        ans = [-1.0]*n

        for i, (x, y) in enumerate(queries): 
            if x not in graph or y not in graph:
                continue
            
            queue = [[x, 1.0]]   # BFS 队列,存储 [节点, 路径权重乘积]
            visited = [x]       # 标记已访问过的节点,避免重复访问
            while queue:
                node, mul = queue.pop(0)
                for adj, weight in graph[node].items(): 
                    if adj == y:       # 找到目标节点,记录结果
                        ans[i] = mul*weight
                        break
                    if adj not in visited:  # 将未访问过的邻接节点加入队列
                        visited.append(adj)
                        queue.append([adj, mul*weight])
        
        return ans

Explore

在构建图的过程中,每添加一条有向边表示一个变量除以另一个变量的结果,例如 a/b = v。为了处理可能的查询,如 b/a,我们需要添加一条反向边,并且设置其权重为原权重的倒数,即 1/v。这使得无论查询是正向还是反向,我们都可以直接通过图中的路径找到答案。这种做法确保了图的完整性和对称性,使得任意两个变量之间的除法关系都可以通过图直接得到。

确保BFS过程中路径权重乘积的计算精度,主要依赖于浮点数的处理方式和编程语言本身的浮点数精度。在实际应用中,可以通过限制浮点数的位数,例如使用 Python 的 decimal 模块来更精确地控制浮点数的精度。此外,对于最终结果的验证,可以通过限制输出的小数位数来减少因精度问题带来的影响。尽管完全避免浮点数误差很难,但通过这些方法可以在大多数情况下保证结果的准确性。

BFS(广度优先搜索)相比于DFS(深度优先搜索),在此类问题中通常更适合寻找最短路径或者在图中层次较浅的地方快速找到解。由于 BFS 是逐层探索,它可以较快地找到从起点到终点的最短路径(即最少的乘法操作)。这意味着在计算变量除法时,BFS 可以更快地找到结果,而不需要遍历不必要的深层路径,这在某些情况下可以提高效率并减少计算量。

如果查询中的两个变量位于不同的连通分量,说明这两个变量之间没有任何直接或间接的乘除关系可以被追踪。在这种情况下,BFS 搜索将无法从一个变量到达另一个变量。在算法实现中,这种情况被检测到后会直接返回 -1.0,表示查询的结果是未知或不存在。这样的设计确保了算法可以正确处理图中不连通的部分。