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.
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:
- Connecting previously unconnected components (learning new relationships)
- 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 wherep[i]
stores the parent of nodei
w
: weight array wherew[i]
stores the ratio of nodei
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
:
- Find the roots of A and B
- 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
- 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 EvaluatorExample 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)
wheren
is the total number of unique variables - Initializing parent array
p
and weight arrayw
: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 ofO(Ξ±(n))
due to path compression - There are
m
equations to process wherem = len(equations)
- Two
- Overall:
O(n + m * Ξ±(n))
which simplifies toO(n * Ξ±(n))
sincem β€ nΒ²
in the worst case
Space Complexity: O(n)
- Dictionary
d
stores mappings forn
unique variables:O(n)
- Parent array
p
of sizen
:O(n)
- Weight array
w
of sizen
: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.
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
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
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
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Donβt Miss This!