title: Evaluate Division
slug: evaluate-division
difficulty: medium
leetcode_id: 399
leetcode_url: https://leetcode.com/problems/evaluate-division/
categories:
  - graphs
  - hash-tables
patterns:
  - slug: bfs
    is_optimal: false
  - slug: dfs
    is_optimal: false
  - slug: union-find
    is_optimal: true

function_signature: "def calc_equation(equations: list[list[str]], values: list[float], queries: list[list[str]]) -> list[float]:"

test_cases:
  visible:
    - input: { equations: [["a", "b"], ["b", "c"]], values: [2.0, 3.0], queries: [["a", "c"], ["b", "a"], ["a", "e"], ["a", "a"], ["x", "x"]] }
      expected: [6.0, 0.5, -1.0, 1.0, -1.0]
    - input: { equations: [["a", "b"], ["b", "c"], ["bc", "cd"]], values: [1.5, 2.5, 5.0], queries: [["a", "c"], ["c", "b"], ["bc", "cd"], ["cd", "bc"]] }
      expected: [3.75, 0.4, 5.0, 0.2]
    - input: { equations: [["a", "b"]], values: [0.5], queries: [["a", "b"], ["b", "a"], ["a", "c"], ["x", "y"]] }
      expected: [0.5, 2.0, -1.0, -1.0]
  hidden:
    - input: { equations: [["a", "b"]], values: [2.0], queries: [["a", "a"], ["b", "b"]] }
      expected: [1.0, 1.0]
    - input: { equations: [], values: [], queries: [["a", "b"]] }
      expected: [-1.0]
    - input: { equations: [["a", "b"], ["c", "d"]], values: [2.0, 3.0], queries: [["a", "d"], ["c", "b"]] }
      expected: [-1.0, -1.0]
    - input: { equations: [["a", "b"], ["b", "c"], ["c", "d"]], values: [2.0, 3.0, 4.0], queries: [["a", "d"], ["d", "a"]] }
      expected: [24.0, 0.041666666666666664]
    - input: { equations: [["x", "y"]], values: [1.0], queries: [["x", "y"], ["y", "x"]] }
      expected: [1.0, 1.0]

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

  You are also given some `queries`, where `queries[j] = [C_j, D_j]` represents the j<sup>th</sup> query where you must find the answer for `C_j / D_j = ?`.

  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.

constraints: |
  - `1 <= equations.length <= 20`
  - `equations[i].length == 2`
  - `1 <= A_i.length, B_i.length <= 5`
  - `values.length == equations.length`
  - `0.0 < values[i] <= 20.0`
  - `1 <= queries.length <= 20`
  - `queries[i].length == 2`
  - `1 <= C_j.length, D_j.length <= 5`
  - `A_i, B_i, C_j, D_j` consist of lower case English letters and digits

examples:
  - input: 'equations = [["a","b"],["b","c"]], values = [2.0,3.0], queries = [["a","c"],["b","a"],["a","e"],["a","a"],["x","x"]]'
    output: "[6.00000,0.50000,-1.00000,1.00000,-1.00000]"
    explanation: "Given: a / b = 2.0, b / c = 3.0. Queries are: a / c = 6.0 (via a/b * b/c), b / a = 0.5 (reciprocal), a / e = -1.0 (e undefined), a / a = 1.0 (same variable), x / x = -1.0 (x undefined)."
  - input: 'equations = [["a","b"],["b","c"],["bc","cd"]], values = [1.5,2.5,5.0], queries = [["a","c"],["c","b"],["bc","cd"],["cd","bc"]]'
    output: "[3.75000,0.40000,5.00000,0.20000]"
    explanation: "a / c = 1.5 * 2.5 = 3.75, c / b = 1 / 2.5 = 0.4, bc / cd = 5.0 (given), cd / bc = 1 / 5.0 = 0.2."
  - input: 'equations = [["a","b"]], values = [0.5], queries = [["a","b"],["b","a"],["a","c"],["x","y"]]'
    output: "[0.50000,2.00000,-1.00000,-1.00000]"
    explanation: "a / b = 0.5 (given), b / a = 2.0 (reciprocal), a / c = -1.0 (c undefined), x / y = -1.0 (both undefined)."

explanation:
  intuition: |
    The key insight is to **model this problem as a graph**. Think of each variable as a node, and each equation as a weighted edge connecting two nodes.

    If `a / b = 2.0`, we can draw an edge from `a` to `b` with weight `2.0`. We also draw the reverse edge from `b` to `a` with weight `1/2.0 = 0.5` (the reciprocal).

    Now, answering a query like `a / c = ?` becomes a **path-finding problem**: can we find a path from node `a` to node `c`? If we can, the answer is the **product of all edge weights along the path**.

    Think of it like currency exchange: if 1 USD = 2 EUR and 1 EUR = 3 GBP, then 1 USD = 6 GBP. We're "chaining" the ratios together by multiplication.

    This graph-based thinking transforms a seemingly algebraic problem into a classic graph traversal — we can use BFS or DFS to find paths and accumulate products along the way.

  approach: |
    We solve this using **Graph Construction + BFS/DFS**:

    **Step 1: Build the graph**

    - Create an adjacency list (dictionary of dictionaries) to store the graph
    - For each equation `[A, B]` with value `v`:
      - Add edge `A → B` with weight `v`
      - Add edge `B → A` with weight `1/v` (the reciprocal)

    &nbsp;

    **Step 2: Process each query**

    - For query `[C, D]`:
      - If either `C` or `D` is not in the graph, return `-1.0`
      - If `C == D`, return `1.0` (any variable divided by itself is 1)
      - Otherwise, use BFS/DFS to find a path from `C` to `D`

    &nbsp;

    **Step 3: BFS to find the path and compute the result**

    - Start BFS from node `C` with initial product `1.0`
    - Use a queue storing `(current_node, accumulated_product)`
    - Track visited nodes to avoid cycles
    - For each neighbor, multiply the current product by the edge weight
    - If we reach node `D`, return the accumulated product
    - If BFS completes without finding `D`, return `-1.0`

    &nbsp;

    **Step 4: Collect and return all results**

    - Apply the BFS query function to each query
    - Return the list of results

  common_pitfalls:
    - title: Forgetting the Reciprocal Edge
      description: |
        Each equation gives us two pieces of information: if `a / b = 2`, then `b / a = 0.5`. You must add **both edges** to the graph.

        Without the reverse edge, you can only traverse in one direction, missing valid paths. For example, with just `a → b`, you couldn't answer the query `b / a`.
      wrong_approach: "Only adding edge A → B"
      correct_approach: "Adding both A → B (weight v) and B → A (weight 1/v)"

    - title: Not Handling Undefined Variables
      description: |
        Variables that don't appear in any equation are undefined. The query `x / y` where neither `x` nor `y` exists should return `-1.0`.

        Even `x / x` returns `-1.0` if `x` is not in the graph — we can't assume undefined variables equal themselves because they're not defined at all.
      wrong_approach: "Assuming any variable divided by itself equals 1"
      correct_approach: "Check if variables exist in graph before assuming x/x = 1"

    - title: Missing Cycle Detection
      description: |
        Without tracking visited nodes during BFS/DFS, you could get stuck in infinite loops. For example, with edges `a ↔ b ↔ c`, a naive traversal could bounce back and forth forever.

        Always maintain a visited set and skip already-visited nodes.
      wrong_approach: "BFS/DFS without visited tracking"
      correct_approach: "Use a visited set to avoid revisiting nodes"

    - title: Incorrect Path Product Accumulation
      description: |
        When traversing the path, you must **multiply** edge weights together, not add them. Division chains work multiplicatively: `a/b * b/c = a/c`.

        Each BFS state needs to carry its accumulated product, not just the node.
      wrong_approach: "Adding edge weights along the path"
      correct_approach: "Multiplying edge weights along the path"

  key_takeaways:
    - "**Graph modeling**: Many ratio/relationship problems can be modeled as weighted graphs where finding answers means finding paths"
    - "**Bidirectional edges**: Division relationships are symmetric — `a/b = v` implies `b/a = 1/v`. Always add both edges"
    - "**BFS for path finding**: When you need to find any path between two nodes, BFS (or DFS) is the standard approach"
    - "**Related problems**: This pattern applies to currency exchange, unit conversion, and any transitive relationship problems"

  time_complexity: "O(Q × (V + E)) where Q is the number of queries, V is the number of unique variables, and E is the number of equations. Each BFS traverses at most all nodes and edges."
  space_complexity: "O(V + E) for storing the graph. The BFS queue and visited set use O(V) additional space per query."

solutions:
  - approach_name: BFS Graph Traversal
    is_optimal: true
    code: |
      from collections import defaultdict, deque

      def calc_equation(
          equations: list[list[str]],
          values: list[float],
          queries: list[list[str]]
      ) -> list[float]:
          # Build the weighted graph
          graph = defaultdict(dict)
          for (a, b), value in zip(equations, values):
              graph[a][b] = value      # a / b = value
              graph[b][a] = 1 / value  # b / a = 1/value

          def bfs(start: str, end: str) -> float:
              # Check if variables exist in graph
              if start not in graph or end not in graph:
                  return -1.0
              # Same variable divides to 1
              if start == end:
                  return 1.0

              # BFS: queue stores (node, accumulated_product)
              queue = deque([(start, 1.0)])
              visited = {start}

              while queue:
                  node, product = queue.popleft()

                  # Check all neighbors
                  for neighbor, weight in graph[node].items():
                      if neighbor == end:
                          # Found the target - return accumulated product
                          return product * weight

                      if neighbor not in visited:
                          visited.add(neighbor)
                          queue.append((neighbor, product * weight))

              # No path found
              return -1.0

          # Process all queries
          return [bfs(c, d) for c, d in queries]
    explanation: |
      **Time Complexity:** O(Q × (V + E)) — For each query, BFS may visit all vertices and edges.

      **Space Complexity:** O(V + E) — Graph storage dominates; BFS uses O(V) per query.

      We build a bidirectional weighted graph where each equation creates two edges. For each query, BFS finds a path while accumulating the product of edge weights. This handles all cases: direct edges, multi-hop paths, undefined variables, and self-division.

  - approach_name: DFS Graph Traversal
    is_optimal: true
    code: |
      from collections import defaultdict

      def calc_equation(
          equations: list[list[str]],
          values: list[float],
          queries: list[list[str]]
      ) -> list[float]:
          # Build the weighted graph
          graph = defaultdict(dict)
          for (a, b), value in zip(equations, values):
              graph[a][b] = value
              graph[b][a] = 1 / value

          def dfs(start: str, end: str, visited: set) -> float:
              # Variable not in graph
              if start not in graph:
                  return -1.0
              # Found the target
              if start == end:
                  return 1.0

              visited.add(start)

              # Explore all neighbors
              for neighbor, weight in graph[start].items():
                  if neighbor not in visited:
                      result = dfs(neighbor, end, visited)
                      # If path found, multiply weights
                      if result != -1.0:
                          return weight * result

              # No path found from this node
              return -1.0

          results = []
          for c, d in queries:
              if c not in graph or d not in graph:
                  results.append(-1.0)
              else:
                  results.append(dfs(c, d, set()))

          return results
    explanation: |
      **Time Complexity:** O(Q × (V + E)) — Same as BFS; each query may explore all nodes/edges.

      **Space Complexity:** O(V + E) — Graph storage plus O(V) recursion stack depth.

      DFS achieves the same result as BFS with a recursive approach. We explore paths depth-first, multiplying edge weights as we backtrack. Both approaches are optimal for this problem size.

  - approach_name: Union-Find with Weights
    is_optimal: true
    code: |
      class UnionFind:
          def __init__(self):
              # parent[x] = (root, weight) where x / root = weight
              self.parent = {}

          def find(self, x: str) -> tuple[str, float]:
              if x not in self.parent:
                  self.parent[x] = (x, 1.0)
                  return (x, 1.0)

              if self.parent[x][0] == x:
                  return self.parent[x]

              # Path compression with weight update
              root, weight = self.find(self.parent[x][0])
              self.parent[x] = (root, self.parent[x][1] * weight)
              return self.parent[x]

          def union(self, x: str, y: str, value: float) -> None:
              # x / y = value
              root_x, weight_x = self.find(x)  # x / root_x = weight_x
              root_y, weight_y = self.find(y)  # y / root_y = weight_y

              if root_x != root_y:
                  # Connect root_x to root_y
                  # root_x / root_y = (x / root_x)^-1 * (x / y) * (y / root_y)
                  #                 = weight_y * value / weight_x
                  self.parent[root_x] = (root_y, weight_y * value / weight_x)

          def query(self, x: str, y: str) -> float:
              if x not in self.parent or y not in self.parent:
                  return -1.0

              root_x, weight_x = self.find(x)
              root_y, weight_y = self.find(y)

              if root_x != root_y:
                  return -1.0  # Different components

              # x / y = (x / root) / (y / root) = weight_x / weight_y
              return weight_x / weight_y

      def calc_equation(
          equations: list[list[str]],
          values: list[float],
          queries: list[list[str]]
      ) -> list[float]:
          uf = UnionFind()

          # Build union-find structure
          for (a, b), value in zip(equations, values):
              uf.union(a, b, value)

          # Answer queries
          return [uf.query(c, d) for c, d in queries]
    explanation: |
      **Time Complexity:** O((E + Q) × α(V)) — Near O(1) per operation with path compression, where α is the inverse Ackermann function.

      **Space Complexity:** O(V) — Storage for parent pointers and weights.

      Union-Find tracks connected components with weighted edges. Each node stores its ratio to its root. When querying `x / y`, we find both roots — if they match, the answer is `weight_x / weight_y`. This approach excels when there are many queries on the same graph.
