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 jth 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)   **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`   **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`   **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.