Problem Overview
LeetCode 399 Evaluate Division gives you a list of equations like ["a","b"] with corresponding values like 2.0, meaning a/b=2.0. You are then given queries like ["a","c"] and must return the computed value — or -1.0 if no path exists between those two variables.
The core challenge is chaining ratios: if a/b=2.0 and b/c=3.0, then a/c=6.0 by multiplying along the chain. Queries can ask about any pair, and if either variable never appeared in the equations, the answer must be -1.0. The problem guarantees no contradictions in the input.
This is a classic graph reachability problem with weighted edges. The variables form nodes, the equations form directed weighted edges, and each query is a path-finding problem where path value equals the product of edge weights along the route.
- 1 ≤ equations.length ≤ 20; values[i] > 0.0
- Variables are strings; same variable always has same value across equations
- Return -1.0 for queries where either variable is unknown or no path exists
- Answer for self-division a/a = 1.0 if a is a known variable
- No contradictions guaranteed; result precision within 1e-5 is accepted
Modeling as a Weighted Graph
The insight that transforms this problem is recognizing each variable as a graph node and each equation as two directed weighted edges. For equation a/b=k, add edge a→b with weight k and edge b→a with weight 1/k. The reverse edge lets you traverse in both directions — computing b/a=0.5 from a/b=2.0.
A query [src, dst] then becomes: find any path from src to dst in the graph and multiply all edge weights along that path. If a/b=2 and b/c=3, the path a→b→c has edge weights 2 and 3, so a/c = 2*3 = 6. If no path exists, return -1.0.
This graph model cleanly handles all cases: self-division returns 1.0 (a node with a trivial path to itself), unknown variables return -1.0 (not in the graph), and multi-hop ratios are just weighted path products.
Graph Insight
Once you see this as a graph problem, it reduces to path-finding with weight multiplication. Each equation becomes two directed edges (forward and reverse), and each query is just: does a weighted path exist from src to dst? If yes, return the product of weights along it.
Building the Graph
Use an adjacency list where each node maps to a list of (neighbor, weight) pairs. In Python, defaultdict(list) is ideal. In Java, use a HashMap<String, List<double[]>> where each double[] stores [neighborIndex, weight] or use a nested map.
Iterate through each equation [a, b] with value k. Add a → (b, k) and b → (a, 1.0/k) to the adjacency list. This bidirectional construction is what makes queries traversable in any direction. If the same variable appears multiple times across equations, it accumulates multiple neighbors.
After building the graph, collect the set of known variables. Any query variable not in this set immediately returns -1.0, avoiding unnecessary DFS calls into nonexistent nodes.
- 1Initialize graph as defaultdict(list) or HashMap<String, List>
- 2For each equation [a, b] with value k: add graph[a].append((b, k))
- 3Also add graph[b].append((a, 1.0 / k)) for the reverse edge
- 4Collect all known variables into a set for O(1) lookup
- 5For each query [src, dst]: check membership first, then run DFS/BFS
DFS Path Search
For each query [src, dst], perform a DFS from src accumulating the product of edge weights. Start with an accumulated value of 1.0. At each node, multiply the current accumulator by the edge weight before recursing to the neighbor.
Maintain a visited set to avoid cycles. If the current node equals dst, return the accumulated product — that is the answer for this query. If DFS exhausts all paths without reaching dst, return -1.0.
Handle edge cases before DFS: if src or dst is not in the graph, return -1.0 immediately. If src == dst and src is a known variable, return 1.0 without any traversal.
BFS Works Too
BFS works equally well here — the key is multiplying edge weights along the path rather than summing them. BFS uses a queue of (node, accumulated_product) tuples. Both DFS and BFS give correct results; DFS is often simpler to implement recursively in interviews.
Union-Find with Weights Alternative
Weighted Union-Find is an elegant alternative where each node tracks its ratio relative to its root. The find(x) operation returns (root, ratio_to_root) — the root of x's component and the multiplier needed to reach the root from x.
When unioning two variables a and b with ratio k (a/b=k), first find both roots. If they share a root, do nothing. Otherwise, link the roots and set the weight such that the chain of ratios remains consistent. This requires careful weight adjustment during the union step.
To evaluate query a/c: call find(a) → (rootA, ratioA) and find(c) → (rootC, ratioC). If rootA != rootC, return -1.0 (different components). Otherwise return ratioA / ratioC — the ratio of both values relative to their shared root cancels out correctly.
- 1Initialize parent[x] = x and weight[x] = 1.0 for all variables
- 2find(x): if parent[x] == x return (x, 1.0); else recurse and update weight[x]
- 3During path compression: weight[x] *= weight[parent[x]] before updating parent
- 4union(a, b, k): find both roots; if different, link rootA to rootB with weight ratioA / (k * ratioB)
- 5query(a, c): find both; if same root return ratioA / ratioC; else return -1.0
Code Walkthrough Python and Java
The DFS version is simpler for interviews. In Python: build the graph with defaultdict(list), then for each query call a recursive dfs(node, dst, visited, acc) that returns acc when node==dst or -1.0 if stuck. Time complexity is O(Q * (V+E)) for Q queries over a graph with V nodes and E edges.
In Java, the structure mirrors Python: HashMap<String, List<double[]>> for the graph, a recursive dfs method returning double, and a HashSet<String> for visited tracking per query. Reset the visited set between queries. Space complexity is O(V+E) for the graph plus O(V) for the recursion stack.
Both approaches handle all edge cases cleanly by checking graph membership before any DFS. The overall space is O(V+E) where V is the number of unique variables and E is 2 * equations.length (bidirectional edges).
Edge Case: Self-Division and Unknowns
Handle self-division a/a=1.0 and unknown variables separately before DFS to avoid edge cases. If src == dst and src is known, return 1.0 immediately. If either src or dst is not in the graph, return -1.0. These checks prevent incorrect DFS behavior on trivial or invalid queries.