399. Evaluate Division
Problem Description
This problem asks you to evaluate division expressions based on given equation relationships between variables.
You're given a set of equations where each equation tells you the result of dividing one variable by another. For example, if you have a / b = 2.0
, this means variable a
divided by variable b
equals 2.0
.
Input:
equations
: An array of variable pairs like[["a", "b"], ["b", "c"]]
values
: An array of corresponding division results like[2.0, 3.0]
- Together these mean:
a / b = 2.0
andb / c = 3.0
- Together these mean:
queries
: An array of variable pairs you need to evaluate, like[["a", "c"], ["b", "a"]]
Task:
For each query pair [C, D]
, you need to find the value of C / D
using the given equations.
Key Rules:
- If you can derive the answer from the given equations (even indirectly through chaining), return that value
- If either variable in the query doesn't appear in any equation, return
-1.0
- If both variables exist but there's no path connecting them through the equations, return
-1.0
Example:
If you know a / b = 2.0
and b / c = 3.0
, you can derive:
a / c = (a / b) * (b / c) = 2.0 * 3.0 = 6.0
b / a = 1 / (a / b) = 1 / 2.0 = 0.5
a / a = 1.0
(any variable divided by itself equals 1)
The solution uses a Union-Find (Disjoint Set Union) data structure with path compression and weight tracking to efficiently find these relationships and calculate the division results.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem can be modeled as a graph where variables are nodes and equations represent weighted edges between them. For example, if
a / b = 2.0
, we have an edge froma
tob
with weight2.0
and an edge fromb
toa
with weight0.5
.
Is it a tree?
- No: The graph may contain cycles. For instance, we could have
a / b = 2.0
,b / c = 3.0
, andc / a = 0.167
, forming a cycle.
Is the problem related to directed acyclic graphs?
- No: As mentioned, the graph can have cycles, so it's not a DAG.
Is the problem related to shortest paths?
- No: We're not finding shortest paths. We need to find if there's a path between two variables and calculate the product of weights along that path.
Does the problem involve connectivity?
- Yes: The core challenge is determining if two variables are connected through the equation relationships. If they're in the same connected component, we can calculate the division result.
Disjoint Set Union
- Yes: This is the approach used in the solution. DSU (Union-Find) efficiently handles connectivity queries and maintains the multiplication factors (weights) between variables.
Conclusion: While the flowchart leads us to DSU, it's worth noting that this problem can also be solved using DFS. With DFS, we would traverse from the source variable to the target variable, multiplying the edge weights along the path. The DSU approach is more efficient for multiple queries as it preprocesses the relationships, making each query O(Ξ±(n)) time complexity after the initial setup.
Intuition
The key insight is recognizing that division relationships form a network where we need to find paths and calculate products along those paths.
Think about it this way: if a / b = 2
and b / c = 3
, then a / c = 6
because a = 2b
and b = 3c
, so a = 2 * 3c = 6c
. This is essentially path multiplication - we multiply the weights along the path from a
to c
.
The challenge is efficiently handling multiple queries. We could use DFS for each query, but that would involve repeated graph traversals. Instead, we can preprocess the relationships to make queries faster.
The Union-Find (DSU) approach is clever here. It groups variables that are connected through equations into the same set. But regular Union-Find only tells us if elements are connected - we need more. We need to know the multiplication factor between any variable and its root.
Here's the brilliant part: we maintain a weight w[x]
for each variable x
that represents x / root(x)
. When we query c / d
, if they're in the same connected component (same root), then:
c / d = (c / root) / (d / root) = w[c] / w[d]
During the union operation, when connecting two components with roots pa
and pb
, we need to maintain consistency. If we're processing a / b = v
, and a
has root pa
with weight w[a]
(meaning a / pa = w[a]
), and similarly for b
, then:
a / b = v
(pa * w[a]) / (pb * w[b]) = v
pa / pb = v * w[b] / w[a]
This gives us the weight for connecting pa
to pb
.
The path compression optimization in find()
is crucial - when we compress the path, we update weights by multiplying them along the chain, ensuring each variable directly knows its ratio to the root.
Solution Approach
The solution implements a weighted Union-Find (Disjoint Set Union) data structure with path compression to efficiently handle division queries.
Data Structures:
p
: A dictionary storing the parent of each variable (for Union-Find)w
: A dictionary storing the weight of each variable, representing the division ratio to its parent
Implementation Walkthrough:
-
Find Function with Path Compression:
def find(x): if p[x] != x: origin = p[x] p[x] = find(p[x]) w[x] *= w[origin] return p[x]
- Recursively finds the root of variable
x
- During path compression, updates the weight by multiplying along the path
- After compression,
w[x]
representsx / root(x)
- Recursively finds the root of variable
-
Initialization:
w = defaultdict(lambda: 1) p = defaultdict() for a, b in equations: p[a], p[b] = a, b
- Initialize weights to 1 (each variable equals itself)
- Set each variable as its own parent initially
-
Union Operation:
for i, v in enumerate(values): a, b = equations[i] pa, pb = find(a), find(b) if pa == pb: continue p[pa] = pb w[pa] = w[b] * v / w[a]
- For each equation
a / b = v
, find roots of both variables - If already in same component, skip (avoid redundant unions)
- Connect component of
a
to component ofb
by makingpb
the parent ofpa
- Calculate the weight: Since
a / b = v
, and we knowa / pa = w[a]
andb / pb = w[b]
, we needpa / pb = (a / w[a]) / (b / w[b]) = (a / b) * (w[b] / w[a]) = v * w[b] / w[a]
- For each equation
-
Query Processing:
return [ -1 if c not in p or d not in p or find(c) != find(d) else w[c] / w[d] for c, d in queries ]
- For each query
c / d
:- Return
-1
if either variable doesn't exist or they're in different components - Otherwise, since both have the same root after
find()
, calculatec / d = (c / root) / (d / root) = w[c] / w[d]
- Return
- For each query
Time Complexity:
- Building the union-find: O(N * Ξ±(N)) where N is the number of equations and Ξ± is the inverse Ackermann function
- Processing queries: O(M * Ξ±(N)) where M is the number of queries
- Overall: O((M + N) * Ξ±(N)) β O(M + N) for practical purposes
Space Complexity: O(N) for storing the parent and weight dictionaries
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how the weighted Union-Find solution works.
Given:
- equations:
[["a","b"], ["b","c"]]
- values:
[2.0, 3.0]
(meaninga/b = 2.0
andb/c = 3.0
) - queries:
[["a","c"], ["b","a"], ["x","y"]]
Step 1: Initialization
p = {a: a, b: b, c: c} # Each variable is its own parent w = {a: 1, b: 1, c: 1} # Initial weights are 1
Step 2: Process First Equation (a/b = 2.0)
- Find roots:
find(a) = a
,find(b) = b
- Different roots, so union them:
- Make
b
the parent ofa
:p[a] = b
- Calculate weight:
w[a] = w[b] * 2.0 / w[a] = 1 * 2.0 / 1 = 2.0
- Make
- State after:
This meansp = {a: b, b: b, c: c} w = {a: 2.0, b: 1, c: 1}
a/b = 2.0
(a is 2 times its parent b)
Step 3: Process Second Equation (b/c = 3.0)
- Find roots:
find(b) = b
,find(c) = c
- Different roots, so union them:
- Make
c
the parent ofb
:p[b] = c
- Calculate weight:
w[b] = w[c] * 3.0 / w[b] = 1 * 3.0 / 1 = 3.0
- Make
- State after:
p = {a: b, b: c, c: c} w = {a: 2.0, b: 3.0, c: 1}
Step 4: Process Queries
Query 1: a/c
- Call
find(a)
:p[a] = b β a
, so compress pathorigin = b
- Recursively
find(b)
: returnsc
(and updatesb
's parent toc
directly) - Update
w[a] = w[a] * w[origin] = 2.0 * 3.0 = 6.0
- Set
p[a] = c
- Call
find(c)
: returnsc
- Both have root
c
, so calculate:a/c = w[a]/w[c] = 6.0/1 = 6.0
Query 2: b/a
find(b) = c
,find(a) = c
(already compressed)- Both have root
c
, so calculate:b/a = w[b]/w[a] = 3.0/6.0 = 0.5
Query 3: x/y
x
not inp
, return-1.0
Final Answer: [6.0, 0.5, -1.0]
The key insight is that after path compression, each variable stores its division ratio directly to the root, making queries O(1) after the find operation.
Solution Implementation
1class Solution:
2 def calcEquation(
3 self, equations: List[List[str]], values: List[float], queries: List[List[str]]
4 ) -> List[float]:
5 """
6 Evaluate division equations using weighted Union-Find.
7
8 Given equations A/B = k, we represent them as a graph where:
9 - Each variable is a node
10 - The weight from node to its parent represents the division ratio
11 - Path compression is used to optimize find operations
12 """
13
14 def find(node: str) -> str:
15 """
16 Find the root of the node with path compression.
17 Also updates the weight to be the cumulative product from node to root.
18 """
19 if parent[node] != node:
20 original_parent = parent[node]
21 # Recursively find the root and compress the path
22 parent[node] = find(parent[node])
23 # Update weight: multiply by the weight of the original parent
24 # This gives us the cumulative weight from node to root
25 weight[node] *= weight[original_parent]
26 return parent[node]
27
28 # Initialize weight dictionary (default value 1.0 for identity)
29 weight = defaultdict(lambda: 1.0)
30 # Initialize parent dictionary for Union-Find
31 parent = defaultdict(str)
32
33 # Initialize each variable as its own parent (self-loop)
34 for numerator, denominator in equations:
35 parent[numerator] = numerator
36 parent[denominator] = denominator
37
38 # Process each equation and build the Union-Find structure
39 for i, value in enumerate(values):
40 numerator, denominator = equations[i]
41
42 # Find roots of both variables
43 root_numerator = find(numerator)
44 root_denominator = find(denominator)
45
46 # If they already have the same root, skip (already connected)
47 if root_numerator == root_denominator:
48 continue
49
50 # Union: make root_numerator point to root_denominator
51 parent[root_numerator] = root_denominator
52
53 # Calculate the weight for the new connection
54 # We need: numerator / denominator = value
55 # Since weight[x] represents x / root(x), we have:
56 # numerator / root_numerator = weight[numerator]
57 # denominator / root_denominator = weight[denominator]
58 # After union: root_numerator / root_denominator = weight[root_numerator]
59 # From numerator / denominator = value, we can derive:
60 # weight[root_numerator] = weight[denominator] * value / weight[numerator]
61 weight[root_numerator] = weight[denominator] * value / weight[numerator]
62
63 # Process queries and calculate results
64 results = []
65 for dividend, divisor in queries:
66 # Check if both variables exist and belong to the same connected component
67 if (dividend not in parent or
68 divisor not in parent or
69 find(dividend) != find(divisor)):
70 results.append(-1.0)
71 else:
72 # Both variables are in the same component
73 # dividend / divisor = (dividend / root) / (divisor / root)
74 # = weight[dividend] / weight[divisor]
75 results.append(weight[dividend] / weight[divisor])
76
77 return results
78
1class Solution {
2 // Parent map for Union-Find structure
3 private Map<String, String> parent;
4 // Weight map storing the division result from node to its root
5 private Map<String, Double> weight;
6
7 public double[] calcEquation(
8 List<List<String>> equations, double[] values, List<List<String>> queries) {
9
10 int numEquations = equations.size();
11 parent = new HashMap<>();
12 weight = new HashMap<>();
13
14 // Initialize each variable as its own parent with weight 1.0
15 for (List<String> equation : equations) {
16 String numerator = equation.get(0);
17 String denominator = equation.get(1);
18 parent.put(numerator, numerator);
19 parent.put(denominator, denominator);
20 weight.put(numerator, 1.0);
21 weight.put(denominator, 1.0);
22 }
23
24 // Build the Union-Find structure with weighted edges
25 for (int i = 0; i < numEquations; ++i) {
26 List<String> equation = equations.get(i);
27 String numerator = equation.get(0);
28 String denominator = equation.get(1);
29
30 // Find the root of both variables
31 String rootNumerator = find(numerator);
32 String rootDenominator = find(denominator);
33
34 // Skip if they already belong to the same set
35 if (Objects.equals(rootNumerator, rootDenominator)) {
36 continue;
37 }
38
39 // Union: connect rootNumerator to rootDenominator
40 parent.put(rootNumerator, rootDenominator);
41 // Update weight: rootNumerator / rootDenominator = (denominator / root) * value / (numerator / root)
42 weight.put(rootNumerator, weight.get(denominator) * values[i] / weight.get(numerator));
43 }
44
45 // Process queries
46 int numQueries = queries.size();
47 double[] results = new double[numQueries];
48
49 for (int i = 0; i < numQueries; ++i) {
50 String dividend = queries.get(i).get(0);
51 String divisor = queries.get(i).get(1);
52
53 // Check if both variables exist and belong to the same connected component
54 if (!parent.containsKey(dividend) ||
55 !parent.containsKey(divisor) ||
56 !Objects.equals(find(dividend), find(divisor))) {
57 results[i] = -1.0;
58 } else {
59 // Calculate dividend / divisor using their weights to the common root
60 results[i] = weight.get(dividend) / weight.get(divisor);
61 }
62 }
63
64 return results;
65 }
66
67 /**
68 * Find operation with path compression.
69 * Returns the root of the set containing x.
70 * Also updates the weight from x to its root during path compression.
71 */
72 private String find(String x) {
73 if (!Objects.equals(parent.get(x), x)) {
74 String originalParent = parent.get(x);
75 // Recursively find the root and compress the path
76 parent.put(x, find(parent.get(x)));
77 // Update weight: x/root = x/originalParent * originalParent/root
78 weight.put(x, weight.get(x) * weight.get(originalParent));
79 }
80 return parent.get(x);
81 }
82}
83
1class Solution {
2public:
3 // Parent mapping for union-find structure
4 unordered_map<string, string> parent;
5 // Weight/ratio from current node to its parent
6 unordered_map<string, double> weight;
7
8 vector<double> calcEquation(vector<vector<string>>& equations, vector<double>& values, vector<vector<string>>& queries) {
9 int numEquations = equations.size();
10
11 // Initialize union-find structure
12 // Each variable is initially its own parent with weight 1.0
13 for (const auto& equation : equations) {
14 parent[equation[0]] = equation[0];
15 parent[equation[1]] = equation[1];
16 weight[equation[0]] = 1.0;
17 weight[equation[1]] = 1.0;
18 }
19
20 // Build union-find structure with weighted edges
21 for (int i = 0; i < numEquations; ++i) {
22 vector<string> equation = equations[i];
23 string numerator = equation[0];
24 string denominator = equation[1];
25
26 // Find root parents of both variables
27 string parentNumerator = find(numerator);
28 string parentDenominator = find(denominator);
29
30 // Skip if already in the same group
31 if (parentNumerator == parentDenominator) {
32 continue;
33 }
34
35 // Union: make parentNumerator point to parentDenominator
36 parent[parentNumerator] = parentDenominator;
37 // Update weight to maintain the ratio relationship
38 // values[i] = numerator / denominator
39 // weight[parentNumerator] = (denominator's path product) * values[i] / (numerator's path product)
40 weight[parentNumerator] = weight[denominator] * values[i] / weight[numerator];
41 }
42
43 // Process queries
44 int numQueries = queries.size();
45 vector<double> results(numQueries);
46
47 for (int i = 0; i < numQueries; ++i) {
48 string dividend = queries[i][0];
49 string divisor = queries[i][1];
50
51 // Check if both variables exist and belong to the same connected component
52 if (parent.find(dividend) == parent.end() ||
53 parent.find(divisor) == parent.end() ||
54 find(dividend) != find(divisor)) {
55 results[i] = -1.0;
56 } else {
57 // Calculate the ratio: dividend / divisor
58 results[i] = weight[dividend] / weight[divisor];
59 }
60 }
61
62 return results;
63 }
64
65 // Find root parent with path compression and weight update
66 string find(string variable) {
67 if (parent[variable] != variable) {
68 string originalParent = parent[variable];
69 // Recursively find root and compress path
70 parent[variable] = find(parent[variable]);
71 // Update weight: multiply weights along the path
72 weight[variable] *= weight[originalParent];
73 }
74 return parent[variable];
75 }
76};
77
1/**
2 * Evaluates division equations and calculates query results using graph traversal
3 * @param equations - Array of variable pairs representing division equations
4 * @param values - Array of division results corresponding to equations
5 * @param queries - Array of variable pairs to evaluate
6 * @returns Array of results for each query, -1 if no path exists
7 */
8function calcEquation(equations: string[][], values: number[], queries: string[][]): number[] {
9 // Build adjacency list graph where edges represent division relationships
10 // Key: variable name, Value: array of [connected variable, division value]
11 const graph: Record<string, [string, number][]> = {};
12
13 // Initialize result array with -1 (default for impossible queries)
14 const results: number[] = Array.from({ length: queries.length }, () => -1);
15
16 // Build bidirectional graph from equations
17 for (let i = 0; i < equations.length; i++) {
18 const [dividend, divisor] = equations[i];
19
20 // Add forward edge: dividend / divisor = values[i]
21 (graph[dividend] ??= []).push([divisor, values[i]]);
22
23 // Add reverse edge: divisor / dividend = 1 / values[i]
24 (graph[divisor] ??= []).push([dividend, 1 / values[i]]);
25 }
26
27 // Process each query using BFS
28 for (let i = 0; i < queries.length; i++) {
29 const [startVariable, targetVariable] = queries[i];
30
31 // Skip if either variable doesn't exist in the graph
32 if (!graph[startVariable] || !graph[targetVariable]) {
33 continue;
34 }
35
36 // Handle self-division case
37 if (startVariable === targetVariable) {
38 results[i] = 1;
39 continue;
40 }
41
42 // Track visited nodes to avoid cycles
43 const visited: Set<string> = new Set<string>();
44
45 // BFS queue: [current variable, accumulated product from start]
46 const queue: [string, number][] = [[startVariable, 1]];
47
48 // Perform BFS to find path from startVariable to targetVariable
49 for (const [currentVariable, currentProduct] of queue) {
50 // Skip if already visited
51 if (visited.has(currentVariable)) {
52 continue;
53 }
54 visited.add(currentVariable);
55
56 // Explore all neighbors
57 for (const [nextVariable, edgeWeight] of graph[currentVariable]) {
58 // Skip visited neighbors
59 if (visited.has(nextVariable)) {
60 continue;
61 }
62
63 // Check if we reached the target
64 if (nextVariable === targetVariable) {
65 results[i] = currentProduct * edgeWeight;
66 break;
67 }
68
69 // Add neighbor to queue with updated product
70 queue.push([nextVariable, currentProduct * edgeWeight]);
71 }
72
73 // Early termination if answer found
74 if (results[i] !== -1) {
75 break;
76 }
77 }
78 }
79
80 return results;
81}
82
Time and Space Complexity
Time Complexity: O((E + Q) * Ξ±(V))
where E
is the number of equations, Q
is the number of queries, V
is the number of unique variables, and Ξ±
is the inverse Ackermann function.
- Initializing parent pointers for all variables in equations:
O(E)
- Processing each equation involves two
find
operations and possibly one union operation:O(E * Ξ±(V))
- For each query, we perform at most two
find
operations:O(Q * Ξ±(V))
- The
find
operation with path compression has an amortized time complexity ofO(Ξ±(V))
, whereΞ±
is the inverse Ackermann function, which is effectively constant for all practical values
Space Complexity: O(V)
where V
is the number of unique variables.
- The parent dictionary
p
stores at mostV
entries:O(V)
- The weight dictionary
w
stores at mostV
entries:O(V)
- The recursion stack for
find
operation can go up toO(V)
in the worst case before path compression, but after path compression, the depth is reduced toO(Ξ±(V))
- The output list stores
Q
results:O(Q)
Overall space complexity is O(V + Q)
, but since we typically consider auxiliary space excluding the output, the auxiliary space complexity is O(V)
.
Common Pitfalls
1. Not Handling the Union Weight Calculation Correctly
The Pitfall: The most common mistake is incorrectly calculating the weight when performing the union operation. Many developers mistakenly use:
# WRONG! w[pa] = v * w[a] / w[b]
Why it's wrong:
The weight relationship needs to maintain the invariant that w[x]
represents x / root(x)
. When connecting two components, we need to ensure that the division relationship a / b = v
is preserved through the root nodes.
The Correct Approach:
w[pa] = w[b] * v / w[a]
This ensures that:
a / b = v
(given)a / pa = w[a]
andb / pb = w[b]
(after find operations)pa / pb = (a / w[a]) / (b / w[b]) = v * w[b] / w[a]
2. Forgetting to Compress Paths During Find Operation
The Pitfall: Implementing find without updating weights during path compression:
# WRONG - doesn't update weights!
def find(x):
if p[x] != x:
p[x] = find(p[x]) # Missing weight update
return p[x]
Why it's wrong: Path compression changes the parent relationships, so weights must be updated to maintain the correct division ratios. Without updating weights, the accumulated product from node to root becomes incorrect.
The Correct Approach:
def find(x):
if p[x] != x:
origin = p[x]
p[x] = find(p[x])
w[x] *= w[origin] # Update weight to maintain correct ratio
return p[x]
3. Not Checking if Variables Are Already Connected
The Pitfall: Processing redundant unions without checking if nodes are already in the same component:
# WRONG - might create inconsistencies
for i, v in enumerate(values):
a, b = equations[i]
p[find(a)] = find(b)
# ... weight calculation
Why it's wrong: If two variables are already connected, adding another edge between them could create inconsistencies or override existing correct relationships. While it might work in some cases, it's inefficient and can lead to subtle bugs.
The Correct Approach:
pa, pb = find(a), find(b) if pa == pb: continue # Skip if already connected p[pa] = pb w[pa] = w[b] * v / w[a]
4. Incorrect Query Result Calculation
The Pitfall: Calculating the query result without ensuring both variables have been compressed to their roots:
# WRONG - might use stale weights if c in p and d in p and p[c] == p[d]: return w[c] / w[d]
Why it's wrong:
The weights are only guaranteed to be correct after calling find()
, which performs path compression and updates the weights. Without calling find()
, you might be using outdated parent relationships and weights.
The Correct Approach:
if c not in p or d not in p or find(c) != find(d): return -1.0 else: return w[c] / w[d] # Weights are now updated after find()
5. Not Initializing Variables Properly
The Pitfall: Only initializing variables when they appear as numerators:
# WRONG - might miss some variables for eq in equations: p[eq[0]] = eq[0] # Only initializing first variable
Why it's wrong: Both variables in each equation need to be initialized as their own parent. Missing initialization can cause KeyError or incorrect parent relationships.
The Correct Approach:
for a, b in equations: p[a], p[b] = a, b # Initialize both variables
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Want a Structured Path to Master System Design Too? Donβt Miss This!