Facebook Pixel

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 and b / c = 3.0
  • 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:

  1. If you can derive the answer from the given equations (even indirectly through chaining), return that value
  2. If either variable in the query doesn't appear in any equation, return -1.0
  3. 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 from a to b with weight 2.0 and an edge from b to a with weight 0.5.

Is it a tree?

  • No: The graph may contain cycles. For instance, we could have a / b = 2.0, b / c = 3.0, and c / 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. 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] represents x / root(x)
  2. 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
  3. 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 of b by making pb the parent of pa
    • Calculate the weight: Since a / b = v, and we know a / pa = w[a] and b / pb = w[b], we need pa / pb = (a / w[a]) / (b / w[b]) = (a / b) * (w[b] / w[a]) = v * w[b] / w[a]
  4. 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(), calculate c / d = (c / root) / (d / root) = w[c] / w[d]

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 Evaluator

Example 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] (meaning a/b = 2.0 and b/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 of a: p[a] = b
    • Calculate weight: w[a] = w[b] * 2.0 / w[a] = 1 * 2.0 / 1 = 2.0
  • State after:
    p = {a: b, b: b, c: c}
    w = {a: 2.0, b: 1, c: 1}
    This means 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 of b: p[b] = c
    • Calculate weight: w[b] = w[c] * 3.0 / w[b] = 1 * 3.0 / 1 = 3.0
  • 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 path
    • origin = b
    • Recursively find(b): returns c (and updates b's parent to c directly)
    • Update w[a] = w[a] * w[origin] = 2.0 * 3.0 = 6.0
    • Set p[a] = c
  • Call find(c): returns c
  • 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 in p, 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 of O(Ξ±(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 most V entries: O(V)
  • The weight dictionary w stores at most V entries: O(V)
  • The recursion stack for find operation can go up to O(V) in the worst case before path compression, but after path compression, the depth is reduced to O(Ξ±(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] and b / 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More