Facebook Pixel

2307. Check for Contradictions in Equations πŸ”’

Problem Description

You are given a 2D array of strings equations and an array of real numbers values. Each equations[i] = [Ai, Bi] paired with values[i] represents the mathematical relationship Ai / Bi = values[i].

Your task is to determine if there exists a contradiction in these equations. Return true if a contradiction exists, otherwise return false.

For example, if you have equations stating A / B = 2 and B / C = 3, you can derive that A / C = 6. If another equation states A / C = 5, this would be a contradiction.

When comparing two numbers for equality, consider them equal if their absolute difference is less than 10^-5. This accounts for floating-point precision issues.

The problem essentially asks you to check if all the given division relationships are mathematically consistent with each other. If you can find any derived relationship that conflicts with a given equation, a contradiction exists.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: This problem can be modeled as a graph where each variable (string) is a node, and each equation creates an edge between two nodes with a weight representing their ratio.

Is it a tree?

  • No: The graph is not necessarily a tree. Multiple equations can create cycles (e.g., A/B = 2, B/C = 3, A/C = 6), and we need to check if these cycles are consistent.

Is the problem related to directed acyclic graphs?

  • No: The graph can have cycles, and we need to verify the consistency of these cycles rather than working with a DAG structure.

Is the problem related to shortest paths?

  • No: We're not finding shortest paths between nodes. Instead, we're checking if the multiplicative relationships along paths are consistent.

Does the problem involve connectivity?

  • Yes: We need to determine which variables are connected through equations and check if variables in the same connected component have consistent ratio relationships.

Disjoint Set Union

  • Yes: DSU (Union-Find) is the appropriate choice here. We can use it to:
    • Track which variables belong to the same connected component
    • Maintain the relative weights (ratios) between variables
    • Detect contradictions when merging components or when two variables are already in the same component but their ratio doesn't match the given equation

Conclusion: The flowchart leads us to use Disjoint Set Union (DSU) with weights. While this is typically associated with DFS patterns for graph problems, the weighted union-find approach efficiently handles the connectivity and ratio consistency checking required for this problem. The DFS pattern is implicitly present in the path compression operation of the union-find structure.

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

Intuition

The key insight is recognizing that division equations create a network of relationships between variables. If we know A / B = 2 and B / C = 3, we can derive that A / C = 6 through the transitive property of division. A contradiction occurs when we have two different paths between the same variables that yield different ratios.

Think of each variable as a node in a graph. When we have an equation A / B = value, we're essentially saying these two nodes are connected with a specific ratio relationship. As we process more equations, we're either:

  1. Connecting previously unconnected components (learning new relationships)
  2. Adding redundant information that should be consistent with what we already know

The challenge is efficiently tracking these ratio relationships. If we pick any node as a reference point with value 1, we can express all other connected nodes as multiples of this reference. For example, if we set B = 1 and A / B = 2, then A = 2. If we also know B / C = 3, then C = 1/3.

This is where weighted union-find shines. Each node stores its ratio relative to its root. When we union two components, we adjust the weights to maintain consistent ratios. When we encounter an equation between nodes already in the same component, we can quickly check if their current ratio matches the given equation.

The path compression in union-find naturally handles the chain of divisions. When finding the root of a node, we update its weight to be the direct ratio to the root, effectively "flattening" the calculation of A / B Γ— B / C Γ— C / D = A / D.

A contradiction is detected when two nodes that are already connected have a ratio that differs from the given equation by more than the tolerance 10^-5.

Learn more about Depth-First Search, Union Find and Graph patterns.

Solution Approach

The solution uses a weighted union-find data structure to track the relationships between variables and detect contradictions.

Step 1: Map strings to integers First, we convert all string variables to integer indices for easier manipulation. We iterate through all equations and assign a unique integer ID to each distinct variable using a dictionary d.

d = defaultdict(int)
n = 0
for e in equations:
    for s in e:
        if s not in d:
            d[s] = n
            n += 1

Step 2: Initialize union-find structures We create two arrays:

  • p: parent array where p[i] stores the parent of node i
  • w: weight array where w[i] stores the ratio of node i to its parent

Initially, each node is its own parent with weight 1.0.

p = list(range(n))
w = [1.0] * n

Step 3: Find operation with path compression The find function locates the root of a node while updating weights along the path. During path compression, we multiply weights to get the direct ratio to the root:

def find(x: int) -> int:
    if p[x] != x:
        root = find(p[x])
        w[x] *= w[p[x]]  # Update weight to be ratio to root
        p[x] = root       # Path compression
    return p[x]

Step 4: Process equations For each equation A / B = v:

  1. Find the roots of A and B
  2. If they're in different components:
    • Union them by making one root point to the other
    • Calculate the new weight: w[pb] = v * w[a] / w[b]
    • This ensures A / B = (w[a] / w[root]) / (w[b] / w[root]) = w[a] / w[b] = v
  3. If they're already in the same component:
    • Check if the existing ratio matches the given value
    • If |v * w[a] - w[b]| >= eps, we have a contradiction
for (a, b), v in zip(equations, values):
    a, b = d[a], d[b]
    pa, pb = find(a), find(b)
    if pa != pb:
        p[pb] = pa
        w[pb] = v * w[a] / w[b]
    elif abs(v * w[a] - w[b]) >= eps:
        return True

The time complexity is O(n Γ— Ξ±(n)) where Ξ± is the inverse Ackermann function (nearly constant), and space complexity is O(n) for storing the parent and weight arrays.

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 small example to illustrate how the weighted union-find solution detects contradictions.

Example Input:

  • equations = [["A", "B"], ["B", "C"], ["A", "C"]]
  • values = [2.0, 3.0, 5.0]

This represents: A/B = 2, B/C = 3, A/C = 5

Step 1: Map strings to integers

  • "A" β†’ 0
  • "B" β†’ 1
  • "C" β†’ 2

Step 2: Initialize union-find

  • Parent array p = [0, 1, 2] (each node is its own parent)
  • Weight array w = [1.0, 1.0, 1.0] (initial weights are 1.0)

Step 3: Process first equation A/B = 2

  • Find roots: root(0) = 0, root(1) = 1
  • Different components, so union them
  • Make node 1's parent = 0
  • Calculate weight: w[1] = 2.0 Γ— 1.0 / 1.0 = 2.0
  • Result: p = [0, 0, 2], w = [1.0, 2.0, 1.0]
  • This means A = 1.0 (relative to root 0), B = 2.0 (so A/B = 1.0/2.0 = 0.5... wait, let me recalculate)

Actually, let me reconsider the weight interpretation. The weight w[i] represents the value of node i divided by its parent's value. So if B's parent is A and w[B] = v, then B/A = v, which means A/B = 1/v.

Let me restart with the correct interpretation:

Step 3: Process first equation A/B = 2

  • Find roots: root(0) = 0, root(1) = 1
  • Different components, union them by making 1's parent = 0
  • We need A/B = 2, which means B = A/2
  • Calculate weight: w[1] = w[0] / (2 Γ— w[1]) = 1.0 / (2 Γ— 1.0) = 0.5
  • Result: p = [0, 0, 2], w = [1.0, 0.5, 1.0]
  • Now B/A = 0.5, so A/B = 2 βœ“

Step 4: Process second equation B/C = 3

  • Find roots: root(1) = 0 (with path compression, w[1] remains 0.5), root(2) = 2
  • Different components, union them by making 2's parent = 0
  • We need B/C = 3, where B has value 0.5 relative to root 0
  • Calculate weight: w[2] = w[1] / (3 Γ— w[2]) = 0.5 / (3 Γ— 1.0) = 1/6
  • Result: p = [0, 0, 0], w = [1.0, 0.5, 1/6]
  • Now C = 1/6 relative to root, B = 0.5, so B/C = 0.5/(1/6) = 3 βœ“

Step 5: Process third equation A/C = 5

  • Find roots: root(0) = 0, root(2) = 0
  • Same component! Check consistency
  • Current ratio: A/C = w[0]/w[2] = 1.0/(1/6) = 6
  • Given value: 5
  • Check: |6 - 5| = 1 β‰₯ 10^-5
  • Contradiction detected! Return true

The algorithm correctly identifies that A/C cannot equal 5 when A/B = 2 and B/C = 3 (which implies A/C = 6).

Solution Implementation

1class Solution:
2    def checkContradictions(
3        self, equations: List[List[str]], values: List[float]
4    ) -> bool:
5        # Find the root of a node with path compression and weight update
6        def find_root(node: int) -> int:
7            if parent[node] != node:
8                # Recursively find the root
9                root = find_root(parent[node])
10                # Path compression: multiply weights along the path
11                weight[node] *= weight[parent[node]]
12                # Update parent to root for path compression
13                parent[node] = root
14            return parent[node]
15      
16        # Map variable names to unique integer IDs
17        variable_to_id = defaultdict(int)
18        next_id = 0
19      
20        # Assign unique ID to each variable in equations
21        for equation in equations:
22            for variable in equation:
23                if variable not in variable_to_id:
24                    variable_to_id[variable] = next_id
25                    next_id += 1
26      
27        # Initialize Union-Find structure
28        # parent[i] stores the parent of node i
29        parent = list(range(next_id))
30        # weight[i] stores the ratio from node i to its parent
31        weight = [1.0] * next_id
32      
33        # Tolerance for floating-point comparison
34        epsilon = 1e-5
35      
36        # Process each equation
37        for (var_a, var_b), value in zip(equations, values):
38            # Get IDs for both variables
39            id_a, id_b = variable_to_id[var_a], variable_to_id[var_b]
40          
41            # Find roots of both variables
42            root_a, root_b = find_root(id_a), find_root(id_b)
43          
44            if root_a != root_b:
45                # Variables are in different components, merge them
46                # Set root_a as parent of root_b
47                parent[root_b] = root_a
48                # Calculate weight for root_b to maintain the equation
49                # var_a / var_b = value, so root_b's weight = value * weight_a / weight_b
50                weight[root_b] = value * weight[id_a] / weight[id_b]
51            else:
52                # Variables are already in the same component
53                # Check if the new equation contradicts existing relationships
54                # Expected ratio: var_a / var_b should equal value
55                # Actual ratio: weight[id_a] / weight[id_b]
56                if abs(value * weight[id_a] - weight[id_b]) >= epsilon:
57                    return True  # Contradiction found
58      
59        return False  # No contradictions found
60
1class Solution {
2    // Parent array for Union-Find structure
3    private int[] parent;
4    // Weight array to store the ratio from current node to its parent
5    private double[] weight;
6
7    public boolean checkContradictions(List<List<String>> equations, double[] values) {
8        // Map to store variable name to index mapping
9        Map<String, Integer> variableToIndex = new HashMap<>();
10        int variableCount = 0;
11      
12        // Assign unique index to each variable
13        for (List<String> equation : equations) {
14            for (String variable : equation) {
15                if (!variableToIndex.containsKey(variable)) {
16                    variableToIndex.put(variable, variableCount++);
17                }
18            }
19        }
20      
21        // Initialize Union-Find structure
22        parent = new int[variableCount];
23        weight = new double[variableCount];
24        for (int i = 0; i < variableCount; ++i) {
25            parent[i] = i;  // Each node is its own parent initially
26            weight[i] = 1.0;  // Initial weight is 1.0
27        }
28      
29        // Epsilon for floating point comparison
30        final double EPSILON = 1e-5;
31      
32        // Process each equation
33        for (int i = 0; i < equations.size(); ++i) {
34            // Get indices of the two variables in current equation
35            int indexA = variableToIndex.get(equations.get(i).get(0));
36            int indexB = variableToIndex.get(equations.get(i).get(1));
37          
38            // Find root parents of both variables
39            int parentA = find(indexA);
40            int parentB = find(indexB);
41          
42            // Current equation value: variableA / variableB = value
43            double currentValue = values[i];
44          
45            if (parentA != parentB) {
46                // Variables are in different sets, merge them
47                parent[parentB] = parentA;
48                // Update weight to maintain the ratio relationship
49                // weight[parentB] represents parentB's value relative to parentA
50                weight[parentB] = currentValue * weight[indexA] / weight[indexB];
51            } else {
52                // Variables are already in same set, check for contradiction
53                // Calculate what the ratio should be based on existing relationships
54                double expectedValue = weight[indexB] / weight[indexA];
55              
56                // If the calculated ratio differs from given value, contradiction found
57                if (Math.abs(currentValue - expectedValue) >= EPSILON) {
58                    return true;
59                }
60            }
61        }
62      
63        // No contradictions found
64        return false;
65    }
66
67    /**
68     * Find operation with path compression for Union-Find
69     * Also updates weights during path compression
70     * @param x the node to find root for
71     * @return the root parent of node x
72     */
73    private int find(int x) {
74        if (parent[x] != x) {
75            int root = find(parent[x]);
76            // Path compression: update weight when compressing path
77            weight[x] *= weight[parent[x]];
78            parent[x] = root;
79        }
80        return parent[x];
81    }
82}
83
1class Solution {
2public:
3    bool checkContradictions(vector<vector<string>>& equations, vector<double>& values) {
4        // Map each variable name to a unique integer ID
5        unordered_map<string, int> variableToId;
6        int variableCount = 0;
7      
8        // Assign unique IDs to all variables appearing in equations
9        for (auto& equation : equations) {
10            for (auto& variable : equation) {
11                if (!variableToId.count(variable)) {
12                    variableToId[variable] = variableCount++;
13                }
14            }
15        }
16      
17        // Initialize Union-Find structure with path compression and weighted edges
18        vector<int> parent(variableCount);
19        iota(parent.begin(), parent.end(), 0);  // Each node is initially its own parent
20        vector<double> weight(variableCount, 1.0);  // Weight from node to its parent
21      
22        // Find operation with path compression and weight update
23        // Returns the root of the set containing x
24        function<int(int)> find = [&](int x) -> int {
25            if (parent[x] != x) {
26                int root = find(parent[x]);
27                weight[x] *= weight[parent[x]];  // Update weight during path compression
28                parent[x] = root;  // Path compression
29            }
30            return parent[x];
31        };
32      
33        // Process each equation and check for contradictions
34        for (int i = 0; i < equations.size(); ++i) {
35            // Get IDs for the two variables in the current equation
36            int varA = variableToId[equations[i][0]];
37            int varB = variableToId[equations[i][1]];
38            double ratio = values[i];  // varA / varB = ratio
39          
40            // Find roots of both variables
41            int rootA = find(varA);
42            int rootB = find(varB);
43          
44            if (rootA != rootB) {
45                // Variables are in different sets, merge them
46                parent[rootB] = rootA;
47                // Calculate weight for rootB: varA / varB = ratio
48                // weight[varA] * x = weight[varB] * ratio, where x is the new weight[rootB]
49                weight[rootB] = ratio * weight[varA] / weight[varB];
50            } else {
51                // Variables are already in the same set, check for contradiction
52                // If varA / varB should equal ratio, then weight[varA] / weight[varB] should equal ratio
53                if (fabs(ratio * weight[varA] - weight[varB]) >= 1e-5) {
54                    return true;  // Contradiction found
55                }
56            }
57        }
58      
59        return false;  // No contradictions found
60    }
61};
62
1/**
2 * Checks if there are contradictions in the given equations and their values.
3 * Uses Union-Find (Disjoint Set Union) with weighted edges to detect contradictions.
4 * 
5 * @param equations - Array of variable pairs representing equations (e.g., [["a", "b"], ["b", "c"]])
6 * @param values - Array of values where values[i] represents equations[i][0] / equations[i][1]
7 * @returns true if contradictions exist, false otherwise
8 */
9function checkContradictions(equations: string[][], values: number[]): boolean {
10    // Map to store variable names to their unique indices
11    const variableToIndex: Record<string, number> = {};
12    let variableCount = 0;
13
14    // Assign a unique index to each variable
15    for (const equation of equations) {
16        for (const variable of equation) {
17            if (!(variable in variableToIndex)) {
18                variableToIndex[variable] = variableCount;
19                variableCount++;
20            }
21        }
22    }
23
24    // Parent array for Union-Find structure
25    const parent: number[] = Array.from({ length: variableCount }, (_, index) => index);
26  
27    // Weight array to store the ratio of each node to its parent
28    // weight[i] represents the value of variable[i] / variable[parent[i]]
29    const weight: number[] = Array.from({ length: variableCount }, () => 1.0);
30
31    /**
32     * Find operation with path compression and weight update.
33     * Finds the root of the set containing element x and updates weights along the path.
34     * 
35     * @param x - The element to find the root for
36     * @returns The root of the set containing x
37     */
38    const find = (x: number): number => {
39        if (parent[x] !== x) {
40            const root = find(parent[x]);
41            // Update weight during path compression
42            // weight[x] = weight[x] * weight[parent[x]]
43            weight[x] *= weight[parent[x]];
44            parent[x] = root;
45        }
46        return parent[x];
47    };
48
49    // Process each equation to build the Union-Find structure
50    for (let i = 0; i < equations.length; i++) {
51        // Get indices for the two variables in the current equation
52        const indexA = variableToIndex[equations[i][0]];
53        const indexB = variableToIndex[equations[i][1]];
54        const quotient = values[i]; // a / b = quotient
55
56        // Find roots of both variables
57        const rootA = find(indexA);
58        const rootB = find(indexB);
59
60        if (rootA !== rootB) {
61            // Variables are in different sets, merge them
62            parent[rootB] = rootA;
63            // Calculate the weight of rootB relative to rootA
64            // Since a / b = quotient, and we have weights relative to roots,
65            // we need: weight[rootB] = (quotient * weight[a]) / weight[b]
66            weight[rootB] = (quotient * weight[indexA]) / weight[indexB];
67        } else {
68            // Variables are already in the same set, check for contradiction
69            // Both a and b have the same root, so we can calculate their ratio
70            // Expected: a / b = quotient
71            // Actual: weight[a] / weight[b] (both relative to the same root)
72            if (Math.abs(quotient * weight[indexA] - weight[indexB]) >= 1e-5) {
73                return true; // Contradiction found
74            }
75        }
76    }
77
78    return false; // No contradictions found
79}
80

Time and Space Complexity

Time Complexity: O(n * Ξ±(n)) where n is the total number of unique variables in all equations, and Ξ±(n) is the inverse Ackermann function.

  • Building the dictionary d requires iterating through all equations and variables: O(n) where n is the total number of unique variables
  • Initializing parent array p and weight array w: O(n)
  • Processing each equation involves:
    • Two find() operations with path compression
    • Either a union operation or a contradiction check
    • Each find() operation has amortized time complexity of O(Ξ±(n)) due to path compression
    • There are m equations to process where m = len(equations)
  • Overall: O(n + m * Ξ±(n)) which simplifies to O(n * Ξ±(n)) since m ≀ nΒ² in the worst case

Space Complexity: O(n)

  • Dictionary d stores mappings for n unique variables: O(n)
  • Parent array p of size n: O(n)
  • Weight array w of size n: O(n)
  • Recursion stack for find() function in worst case (before path compression): O(n)
  • Total space complexity: O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Weight Calculation During Union Operation

One of the most common mistakes is incorrectly calculating the weight when merging two components. When setting root_b as a child of root_a, many implementations mistakenly use:

Incorrect:

weight[root_b] = value * weight[id_b] / weight[id_a]  # Wrong formula!

Why it's wrong: The relationship we need to maintain is var_a / var_b = value. After union, both variables should have the correct ratio to their common root. The formula should ensure that (weight[id_a] / weight[root]) / (weight[id_b] / weight[root]) = value.

Correct:

weight[root_b] = value * weight[id_a] / weight[id_b]

2. Forgetting Path Compression Weight Update

Another critical pitfall is implementing path compression without updating weights along the compressed path:

Incorrect:

def find_root(node: int) -> int:
    if parent[node] != node:
        parent[node] = find_root(parent[node])  # Only compress path
        # Forgot to update weight[node]!
    return parent[node]

Why it's wrong: When we compress the path, we're changing a node's parent from its immediate parent to the root. The weight must be updated to reflect the ratio to the new parent (root).

Correct:

def find_root(node: int) -> int:
    if parent[node] != node:
        root = find_root(parent[node])
        weight[node] *= weight[parent[node]]  # Update weight BEFORE updating parent
        parent[node] = root
    return parent[node]

3. Incorrect Contradiction Check

When checking for contradictions in already-connected components, a common error is comparing the wrong values:

Incorrect:

if abs(value - weight[id_a] / weight[id_b]) >= epsilon:  # Direct division
    return True

Why it's wrong: After path compression, weight[id_a] and weight[id_b] represent ratios to the root, not to each other. Direct division can cause division by zero or incorrect results.

Correct:

if abs(value * weight[id_b] - weight[id_a]) >= epsilon:
    return True

This rearranged formula avoids division and correctly checks if weight[id_a] / weight[id_b] β‰ˆ value.

4. Not Handling Self-Division Cases

If the input contains equations like A / A = value where value β‰  1.0, this represents a contradiction that should be detected immediately:

Enhanced Solution:

for (var_a, var_b), value in zip(equations, values):
    if var_a == var_b:
        if abs(value - 1.0) >= epsilon:
            return True  # Self-division must equal 1
        continue  # Skip processing if valid
  
    # ... rest of the logic

5. Floating-Point Precision Issues

Using exact equality (==) for floating-point comparison instead of epsilon-based comparison:

Incorrect:

if value * weight[id_b] != weight[id_a]:  # Exact comparison
    return True

Correct:

if abs(value * weight[id_b] - weight[id_a]) >= epsilon:
    return True

Always use tolerance-based comparison for floating-point numbers to avoid false positives due to rounding errors.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What does the following code do?

1def f(arr1, arr2):
2  i, j = 0, 0
3  new_arr = []
4  while i < len(arr1) and j < len(arr2):
5      if arr1[i] < arr2[j]:
6          new_arr.append(arr1[i])
7          i += 1
8      else:
9          new_arr.append(arr2[j])
10          j += 1
11  new_arr.extend(arr1[i:])
12  new_arr.extend(arr2[j:])
13  return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2  int i = 0, j = 0;
3  List<Integer> newArr = new ArrayList<>();
4
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.add(arr1[i]);
8          i++;
9      } else {
10          newArr.add(arr2[j]);
11          j++;
12      }
13  }
14
15  while (i < arr1.length) {
16      newArr.add(arr1[i]);
17      i++;
18  }
19
20  while (j < arr2.length) {
21      newArr.add(arr2[j]);
22      j++;
23  }
24
25  return newArr;
26}
27
1function f(arr1, arr2) {
2  let i = 0, j = 0;
3  let newArr = [];
4  
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.push(arr1[i]);
8          i++;
9      } else {
10          newArr.push(arr2[j]);
11          j++;
12      }
13  }
14  
15  while (i < arr1.length) {
16      newArr.push(arr1[i]);
17      i++;
18  }
19  
20  while (j < arr2.length) {
21      newArr.push(arr2[j]);
22      j++;
23  }
24  
25  return newArr;
26}
27

Recommended Readings

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

Load More