399. Evaluate Division - Explanation

Problem Link

Description

You are given an array of variable pairs equations and an array of real numbers values, where equations[i] = [Ai, Bi] and values[i] represent the equation Ai / Bi = values[i]. Each Ai or Bi is a string that represents a single variable.

You are also given some queries, where queries[j] = [Cj, Dj] represents the jth query where you must find the answer for Cj / Dj = ?.

Return the answers to all queries. If a single answer cannot be determined, return -1.0.

Note: The input is always valid. You may assume that evaluating the queries will not result in division by zero and that there is no contradiction.

Note: The variables that do not occur in the list of equations are undefined, so the answer cannot be determined for them.

Example 1:

Input: equations = [["a","b"],["b","c"],["ab","bc"]], values = [4.0,1.0,3.25], queries = [["a","c"],["b","a"],["c","c"],["ab","a"],["d","d"]]

Output: [4.00000,0.25000,1.00000,-1.00000,-1.00000]

Example 2:

Input: equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"]]

Output: [0.50000,2.00000]

Constraints:

  • 1 <= equations.length, queries.length <= 20
  • equations[i].length == queries[i].length == 2
  • 1 <= Ai.length, Bi.length, Cj.length, Dj.length <= 5
  • values.length == equations.length
  • 0.0 < values[i] <= 20.0
  • Ai, Bi, Cj, Dj consist of lower case English letters and digits.


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Graph Representation - Building adjacency lists from equations where variables are nodes and ratios are edge weights
  • BFS/DFS Graph Traversal - Finding paths between nodes while accumulating products of edge weights
  • Union-Find (Disjoint Set Union) - Advanced technique for tracking connected components with weighted relationships
  • Hash Maps - Storing graph edges and tracking visited nodes during traversal
  • Floyd-Warshall Algorithm - All-pairs shortest path concept adapted for computing transitive ratios

Intuition

We can think of each equation a / b = value as a directed edge from a to b with weight value, and an edge from b to a with weight 1/value. This transforms the problem into a graph traversal: to evaluate x / y, we need to find a path from x to y and multiply the edge weights along that path.

BFS works well here because we explore all neighbors at the current distance before moving further. Starting from the source variable, we track the accumulated product as we traverse. When we reach the target, that accumulated product gives us the answer.

Algorithm

  1. Build an adjacency list where each variable maps to a list of (neighbor, weight) pairs. For each equation a / b = value, add edges a -> b with weight value and b -> a with weight 1/value.
  2. For each query (src, target):
    • If either variable is not in the graph, return -1.
    • Initialize a queue with (src, 1.0) and a visited set.
    • While the queue is not empty:
      • Dequeue a node and its accumulated weight.
      • If this node equals target, return the accumulated weight.
      • For each unvisited neighbor, enqueue it with the updated weight (current weight multiplied by edge weight).
    • If target is never reached, return -1.
  3. Return the results for all queries.
class Solution:
    def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
        adj = collections.defaultdict(list)  # Map a -> list of [b, a/b]

        for i, eq in enumerate(equations):
            a, b = eq
            adj[a].append((b, values[i]))
            adj[b].append((a, 1 / values[i]))

        def bfs(src, target):
            if src not in adj or target not in adj:
                return -1
            q, visit = deque([(src, 1)]), set()
            visit.add(src)

            while q:
                node, w = q.popleft()
                if node == target:
                    return w
                for nei, weight in adj[node]:
                    if nei not in visit:
                        q.append((nei, w * weight))
                        visit.add(nei)
            return -1

        return [bfs(q[0], q[1]) for q in queries]

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(n+m)O(n + m)

Where nn is the number of unique strings and mm is the number of queries.


Intuition

Like BFS, DFS also explores paths through the graph, but it goes deep before backtracking. We recursively explore from the source, multiplying edge weights as we go. The first complete path we find from source to target gives us the answer.

DFS is often simpler to implement recursively. At each step, we check if we have reached the target. If not, we try all unvisited neighbors, and if any recursive call succeeds (returns a non-negative result), we multiply by the current edge weight and return.

Algorithm

  1. Build the same adjacency list as in BFS.
  2. For each query (src, target):
    • If either variable is not in the graph, return -1.
    • If src == target, return 1.0 (any variable divided by itself is 1).
    • Mark src as visited and explore all neighbors recursively.
    • For each unvisited neighbor, recursively search for target. If found, multiply the result by the edge weight and return.
    • If no path is found, return -1.
  3. Return the results for all queries.
class Solution:
    def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
        adj = collections.defaultdict(list)  # Map a -> list of [b, a/b]

        for i, eq in enumerate(equations):
            a, b = eq
            adj[a].append((b, values[i]))
            adj[b].append((a, 1 / values[i]))

        def dfs(src, target, visited):
            if src not in adj or target not in adj:
                return -1
            if src == target:
                return 1

            visited.add(src)

            for nei, weight in adj[src]:
                if nei not in visited:
                    result = dfs(nei, target, visited)
                    if result != -1:
                        return weight * result

            return -1

        return [dfs(q[0], q[1], set()) for q in queries]

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(n+m)O(n + m)

Where nn is the number of unique strings and mm is the number of queries.


3. Disjoint Set Union

Intuition

Union-Find can track connected components, but here we need to track ratios between variables. The key insight is to store each variable's weight relative to its root. When we union two variables x and y with ratio x/y = value, we connect their roots and adjust weights so that the ratio relationship is preserved.

To query x/y, we find both roots. If they differ, no path exists. If they match, the answer is weight[x] / weight[y], since both weights are relative to the same root.

Algorithm

  1. Initialize parent and weight maps. Each variable starts as its own parent with weight 1.0.
  2. The find operation uses path compression: while finding the root, update the weight to be relative to the root by multiplying along the path.
  3. The union operation connects two variables: find both roots, and if different, set one root's parent to the other and adjust the weight to maintain the given ratio.
  4. Process all equations by calling union for each pair.
  5. For each query (x, y):
    • If either variable is unknown or they have different roots, return -1.
    • Otherwise, return weight[x] / weight[y].
class UnionFind:
    def __init__(self):
        self.parent = {}
        self.weight = {}

    def add(self, x):
        if x not in self.parent:
            self.parent[x] = x
            self.weight[x] = 1.0

    def find(self, x):
        if x != self.parent[x]:
            orig_parent = self.parent[x]
            self.parent[x] = self.find(self.parent[x])
            self.weight[x] *= self.weight[orig_parent]
        return self.parent[x]

    def union(self, x, y, value):
        self.add(x)
        self.add(y)
        root_x = self.find(x)
        root_y = self.find(y)

        if root_x != root_y:
            self.parent[root_x] = root_y
            self.weight[root_x] = value * self.weight[y] / self.weight[x]

    def get_ratio(self, x, y):
        if x not in self.parent or y not in self.parent or self.find(x) != self.find(y):
            return -1.0
        return self.weight[x] / self.weight[y]

class Solution:
    def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
        uf = UnionFind()

        for (a, b), value in zip(equations, values):
            uf.union(a, b, value)

        return [uf.get_ratio(a, b) for a, b in queries]

Time & Space Complexity

  • Time complexity: O((m+n)logn)O((m + n)\log n)
  • Space complexity: O(n+m)O(n + m)

Where nn is the number of unique strings and mm is the number of queries.


4. Floyd Warshall

Intuition

Floyd-Warshall computes shortest paths between all pairs of nodes. Here, instead of distances, we compute the product of ratios. If we know a/b and b/c, then a/c = (a/b) * (b/c). We precompute all such transitive ratios.

This approach trades query time for preprocessing time. After running Floyd-Warshall, each query becomes a simple lookup.

Algorithm

  1. Build a graph where graph[a][b] = value for equation a/b = value, and graph[b][a] = 1/value.
  2. Run Floyd-Warshall: for each intermediate node k, and for each pair of nodes (i, j) connected through k, compute graph[i][j] = graph[i][k] * graph[k][j] if this path does not already exist.
  3. For each query (a, b):
    • If a is in the graph and b is reachable from a, return graph[a][b].
    • Otherwise, return -1.
class Solution:
    def calcEquation(self, equations, values, queries):
        graph = collections.defaultdict(dict)

        for (a, b), value in zip(equations, values):
            graph[a][b] = value
            graph[b][a] = 1.0 / value

        for k in graph:
            for i in graph[k]:
                for j in graph[k]:
                    if j not in graph[i]:
                        graph[i][j] = graph[i][k] * graph[k][j]

        result = []
        for a, b in queries:
            if a in graph and b in graph[a]:
                result.append(graph[a][b])
            else:
                result.append(-1.0)
        return result

Time & Space Complexity

  • Time complexity: O(m+n3)O(m + n ^ 3)
  • Space complexity: O(n2+m)O(n ^ 2 + m)

Where nn is the number of unique strings and mm is the number of queries.


Common Pitfalls

Forgetting to Add Reciprocal Edges

When building the graph, each equation a / b = value must create two edges: a -> b with weight value AND b -> a with weight 1/value. Forgetting the reciprocal edge means you can only traverse the relationship in one direction, causing valid queries to return -1 when the path exists in reverse.

Not Handling Unknown Variables

A query might contain variables that never appeared in any equation. Before starting BFS/DFS, you must check if both the source and target variables exist in the graph. Attempting to traverse from a non-existent node will cause null pointer errors or infinite loops depending on your implementation.

Incorrect Weight Multiplication Order in Union-Find

In the weighted Union-Find approach, the weight calculation during union is subtle: when connecting rootX to rootY, the new weight for rootX must be value * weight[y] / weight[x]. Getting this formula wrong (such as dividing instead of multiplying, or using the wrong variables) will produce incorrect ratios for subsequent queries.