LeetCode #399 Medium

Evaluate Division

Evaluate Division is LeetCode 399 (Medium). You get equations[i] = [Aᵢ, Bᵢ] and values[i], which mean Aᵢ / Bᵢ = values[i]. For every query [Cⱼ, Dⱼ], return the value of Cⱼ / Dⱼ, or -1.0 if it cannot be worked out.

  • Each variable is a short string of lowercase letters and digits.
  • A variable that appears in no equation is undefined, so any query that uses it has no answer.
  • The equations never contradict each other and every value is positive, so no division by zero can occur.

There are at most 20 equations and 20 queries, so running a fresh search for each query is cheap.

Constraints
  • 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 consist of lower case English letters and digits.
arraydepth-first-searchunion-findgraphshortest-path
Open on LeetCode ↗
02

Intuition

Equations chain together: if a / b and b / c are known, then a / c = (a / b) × (b / c). So treat each variable as a node and each equation as two weighted arrows, a → b with weight w and b → a with weight 1 / w.

The ratio of any two variables is then the product of the weights along a path between them. The equations are consistent, so every path between the same two variables gives the same product. That means any search will do, with no need for a shortest path. If no path exists, the two variables sit in separate groups of equations and nothing links their values.

How to spot this pattern

Any problem where facts compose by multiplying, such as exchange rates, unit conversions or gear ratios, turns into this weighted graph: walk a path and multiply the edge weights. If queries vastly outnumber facts, weighted union-find answers each one without a search.

03

Approach

Try it first

Before reading on: given a / b = 2 and b / c = 3, work out c / a by hand. Which equations did you use, and in which direction did you read each one? Then decide what x / x should return when x appears in no equation.

1

Store every equation in both directions

For a / b = w, set graph[a][b] = w and graph[b][a] = 1 / w. A query may read an equation backwards, as in b / a, and the reverse arrow is what lets the search travel that way.

2

Answer -1.0 for an undefined variable

If either variable of the query is missing from graph, return -1.0 at once. This includes x / x for an unknown x: the problem treats it as undefined, so the shortcut "anything divided by itself is 1" must not be applied.

3

Search from the numerator, carrying a ratio

Start a stack with (src, 1.0) and a seen set holding src. Pop a pair (node, ratio); if node is the target, return ratio. The ratio stored with a node is always src / node, so when the target comes off the stack it holds the answer.

4

Push each unseen neighbour with ratio × weight

For each arrow node → nxt with weight w, skip nxt if it is already seen. Otherwise mark it and push (nxt, ratio × w), because (src / node) × (node / nxt) = src / nxt. Marking on push keeps every variable on the stack at most once.

5

Return -1.0 when the stack runs out

An empty stack means every variable linked to src has been visited without meeting the target. The two variables are in separate groups of equations, so their ratio is unknown.

04

Evaluate Division solution in Python | C++ | Java

▶1from collections import defaultdict
▶2 
▶3class Solution:
▶4 def calcEquation(self, equations, values, queries):
▶5 graph = defaultdict(dict)
▶6 for (a, b), value in zip(equations, values):
▶7 graph[a][b] = value
▶8 graph[b][a] = 1.0 / value
▶9 
▶10 def ratio(src, dst):
▶11 if src not in graph or dst not in graph:
▶12 return -1.0
▶13 stack = [(src, 1.0)]
▶14 seen = {src}
▶15 while stack:
▶16 node, product = stack.pop()
▶17 if node == dst:
▶18 return product
▶19 for nxt, weight in graph[node].items():
▶20 if nxt not in seen:
▶21 seen.add(nxt)
▶22 stack.append((nxt, product * weight))
▶23 return -1.0
▶24 
▶25 return [ratio(c, d) for c, d in queries]
×2×0.5abcqueryratio so farstack (top on the right)emptyanswersa / cb / aa / ea / ax / xa / b = 2 → add both arrows
equationa / b = 2
reverseb / a = 0.5
The equation says a = 2 × b. So stepping from a to b multiplies a ratio by 2, and stepping back from b to a multiplies by 1/2 = 0.5. Both arrows are stored, so a query can travel either way.
×2×0.5×3×0.333abcqueryratio so farstack (top on the right)emptyanswersa / cb / aa / ea / ax / xb / c = 3 → add both arrows
equationb / c = 3
reversec / b = 0.333
The equation says b = 3 × c. So stepping from b to c multiplies a ratio by 3, and stepping back from c to b multiplies by 1/3 = 0.333. Both arrows are stored, so a query can travel either way.
×2×0.5×3×0.333afrombctoquerya÷cratio so far1stack (top on the right)a · 1answersa / cb / aa / ea / ax / xstart at a with ratio 1
querya / c
stack[(a, 1)]
The search starts at a. The ratio a / a is 1, so that is the value carried at the start. Each arrow walked later multiplies it, turning it into a divided by the variable reached.
×2×0.5×3×0.333afrombctoquerya÷cratio so far1stack (top on the right)emptyanswersa / cb / aa / ea / ax / xtake a (ratio 1) from the stack
nodea
ratio1a / a
Pop a. Its ratio 1 means a / a = 1. It is not the target, so its arrows are tried next.
×2×0.5×3×0.333afrombctoquerya÷cratio so far1×2=2stack (top on the right)b · 2answersa / cb / aa / ea / ax / xwalk to b → ratio 2
nextb
ratio1 × 2 = 2a / b
Walking a → b multiplies by 2: (a / a) × (a / b) = a / b = 2. b is marked seen and pushed with that ratio.
×2×0.5×3×0.333afrombctoquerya÷cratio so far2stack (top on the right)emptyanswersa / cb / aa / ea / ax / xtake b (ratio 2) from the stack
nodeb
ratio2a / b
Pop b. Its ratio 2 means a / b = 2. It is not the target, so its arrows are tried next.
×2×0.5×3×0.333afrombctoquerya÷cratio so far2stack (top on the right)emptyanswersa / cb / aa / ea / ax / xa already seen → skip
nextaseen
The arrow b → a leads to a variable that is already on the stack or already done. Every equation adds an arrow back, so without this check the search would bounce between b and a forever.
×2×0.5×3×0.333afrombctoquerya÷cratio so far2×3=6stack (top on the right)c · 6answersa / cb / aa / ea / ax / xwalk to c → ratio 6
nextc
ratio2 × 3 = 6a / c
Walking b → c multiplies by 3: (a / b) × (b / c) = a / c = 6. c is marked seen and pushed with that ratio.
×2×0.5×3×0.333afrombctoquerya÷cratio so far6stack (top on the right)emptyanswers6a / cb / aa / ea / ax / xreached c → a / c = 6
nodec
answer6
The popped variable is the target. The ratio carried with it is the product of the arrow labels along the path from a, which equals a / c = 6.
×2×0.5×3×0.333atobfromcqueryb÷aratio so far1stack (top on the right)b · 1answers6a / cb / aa / ea / ax / xstart at b with ratio 1
queryb / a
stack[(b, 1)]
The search starts at b. The ratio b / b is 1, so that is the value carried at the start. Each arrow walked later multiplies it, turning it into b divided by the variable reached.
×2×0.5×3×0.333atobfromcqueryb÷aratio so far1stack (top on the right)emptyanswers6a / cb / aa / ea / ax / xtake b (ratio 1) from the stack
nodeb
ratio1b / b
Pop b. Its ratio 1 means b / b = 1. It is not the target, so its arrows are tried next.
×2×0.5×3×0.333atobfromcqueryb÷aratio so far1×0.5=0.5stack (top on the right)a · 0.5answers6a / cb / aa / ea / ax / xwalk to a → ratio 0.5
nexta
ratio1 × 0.5 = 0.5b / a
Walking b → a multiplies by 0.5: (b / b) × (b / a) = b / a = 0.5. a is marked seen and pushed with that ratio.
×2×0.5×3×0.333atobfromcqueryb÷aratio so far1×3=3stack (top on the right)a · 0.5c · 3answers6a / cb / aa / ea / ax / xwalk to c → ratio 3
nextc
ratio1 × 3 = 3b / c
Walking b → c multiplies by 3: (b / b) × (b / c) = b / c = 3. c is marked seen and pushed with that ratio.
×2×0.5×3×0.333atobfromcqueryb÷aratio so far3stack (top on the right)a · 0.5answers6a / cb / aa / ea / ax / xtake c (ratio 3) from the stack
nodec
ratio3b / c
Pop c. Its ratio 3 means b / c = 3. It is not the target, so its arrows are tried next.
×2×0.5×3×0.333atobfromcqueryb÷aratio so far3stack (top on the right)a · 0.5answers6a / cb / aa / ea / ax / xb already seen → skip
nextbseen
The arrow c → b leads to a variable that is already on the stack or already done. Every equation adds an arrow back, so without this check the search would bounce between c and b forever.
×2×0.5×3×0.333atobfromcqueryb÷aratio so far0.5stack (top on the right)emptyanswers6a / c0.5b / aa / ea / ax / xreached a → b / a = 0.5
nodea
answer0.5
The popped variable is the target. The ratio carried with it is the product of the arrow labels along the path from b, which equals b / a = 0.5.
×2×0.5×3×0.333afrombcquerya÷eratio so farstack (top on the right)emptyanswers6a / c0.5b / a−1a / ea / ax / xe is in no equation → −1
querya / e
answer−1.0
e never appears in an equation, so nothing is known about its value. The answer is −1.0 even when the query is e / e: the rule that x / x = 1 is not applied to a variable the equations never define.
×2×0.5×3×0.333afrom, tobcquerya÷aratio so far1stack (top on the right)a · 1answers6a / c0.5b / a−1a / ea / ax / xstart at a with ratio 1
querya / a
stack[(a, 1)]
The search starts at a. The ratio a / a is 1, so that is the value carried at the start. Each arrow walked later multiplies it, turning it into a divided by the variable reached.
×2×0.5×3×0.333afrom, tobcquerya÷aratio so far1stack (top on the right)emptyanswers6a / c0.5b / a−1a / e1a / ax / xreached a → a / a = 1
nodea
answer1
The start is already the target, so the answer is the starting ratio, 1. This only happens for a variable that appears in some equation.
×2×0.5×3×0.333abcqueryx÷xratio so farstack (top on the right)emptyanswers6a / c0.5b / a−1a / e1a / a−1x / xx is in no equation → −1
queryx / x
answer−1.0
x never appears in an equation, so nothing is known about its value. The answer is −1.0 even when the query is x / x: the rule that x / x = 1 is not applied to a variable the equations never define.
×2×0.5×3×0.333abcqueryratio so farstack (top on the right)emptyanswers6a / c0.5b / a−1a / e1a / a−1x / xall 5 queries answered
result[6, 0.5, -1.0, 1, -1.0]
Each query ran its own search with its own seen set, so the answers are independent. The list is returned in query order.
05

Weighted union-find

parent[x] points toward the root of x's group and weight[x] stores x / parent[x]. find flattens the path and multiplies the weights on the way, so afterwards weight[x] is x / root. Joining a / b = v hangs the root of a under the root of b with weight v × weight[b] / weight[a], which is exactly root(a) / root(b). A query then divides the two ratios to the shared root.

▶1class Solution:
▶2 def calcEquation(self, equations, values, queries):
▶3 parent, weight = {}, {}
▶4 
▶5 def find(x):
▶6 if parent[x] != x:
▶7 root = find(parent[x])
▶8 weight[x] *= weight[parent[x]]
▶9 parent[x] = root
▶10 return parent[x]
▶11 
▶12 for (a, b), value in zip(equations, values):
▶13 for v in (a, b):
▶14 if v not in parent:
▶15 parent[v], weight[v] = v, 1.0
▶16 ra, rb = find(a), find(b)
▶17 if ra != rb:
▶18 parent[ra] = rb
▶19 weight[ra] = value * weight[b] / weight[a]
▶20 
▶21 answers = []
▶22 for c, d in queries:
▶23 if c not in parent or d not in parent or find(c) != find(d):
▶24 answers.append(-1.0)
▶25 else:
▶26 answers.append(weight[c] / weight[d])
▶27 return answers
06

Common pitfalls

Storing each equation one way only

✗ Wrong
graph[a][b] = value
✓ Right
graph[a][b] = value
graph[b][a] = 1.0 / value

With equations = [["a", "b"]], values = [2.0] and the query ["b", "a"], the search starts at b, finds no arrow out of it and returns -1.0. The right answer is 0.5.

Searching without a seen set

✗ Wrong
for nxt, weight in graph[node].items():
    stack.append((nxt, product * weight))
✓ Right
for nxt, weight in graph[node].items():
    if nxt not in seen:
        seen.add(nxt)
        stack.append((nxt, product * weight))

Every equation adds an arrow back, so each pair of variables forms a loop. For equations = [["a", "b"], ["c", "d"]] and the query ["a", "d"], the stack keeps refilling with a and b and never empties, so the search never ends.

Sharing one seen set across queries

✗ Wrong
seen = set()
def ratio(src, dst):
    seen.add(src)
✓ Right
def ratio(src, dst):
    seen = {src}

In the first example, the query a / c marks a, b and c. The next query b / a then finds every neighbour already seen and returns -1.0 instead of 0.5. Each search needs a fresh set.

07

Edge cases

Query a / a where a is in an equation

The first pop is a itself, which is the target, so the answer is the starting ratio 1.0.

08

Complexity

Time
O(Q · (V + E))
Space
O(V + E)
V is the number of distinct variables, E the number of equations and Q the number of queries. Each query visits each variable and arrow at most once. The graph stores 2E arrows.
09

Evaluate Division FAQ

Why multiply along the path instead of adding?

Each arrow is a ratio, and ratios chain by multiplying: (a / b) × (b / c) = a / c, because the b cancels. Adding weights would suit a path whose cost is a sum, such as distance. To reuse a sum-based algorithm, take logarithms of the weights; a sum of logs is the log of the product.

Can BFS replace DFS here?

Yes. Replace the stack with a queue and nothing else changes. Because every path between two variables gives the same product, the order in which variables are explored does not affect the answer, only which path happens to be found first.