990. Satisfiability of Equality Equations
Problem Description
You are given an array of string equations where each equation represents a relationship between two variables. Each equation is exactly 4 characters long and follows one of two formats:
"xi==yi"
- indicates that variablexi
equals variableyi
"xi!=yi"
- indicates that variablexi
does not equal variableyi
Here, xi
and yi
are single lowercase letters representing variable names.
Your task is to determine if it's possible to assign integer values to all variables such that all the given equations are satisfied simultaneously. Return true
if such an assignment exists, or false
if the equations are contradictory.
For example:
- If you have equations
["a==b", "b==c", "a==c"]
, this returnstrue
because you can assign the same value to all three variables - If you have equations
["a==b", "b!=a"]
, this returnsfalse
because no assignment can satisfy both equations (a cannot both equal and not equal b)
The solution uses a Union-Find (Disjoint Set Union) data structure to handle this problem efficiently:
- First pass: Process all equality equations (
"=="
) by unioning the variables that should be equal - Second pass: Check all inequality equations (
"!="
) to see if any two variables that should be different are actually in the same group (have the same root)
The code maps each lowercase letter to an index (0-25) and uses path compression in the find
function for optimization. If any inequality equation involves two variables that belong to the same group (have been unified through equality equations), the equations are impossible to satisfy.
Intuition
The key insight is recognizing that equality relationships are transitive - if a == b
and b == c
, then a == c
must also be true. This creates groups of variables that must all have the same value.
Think of it like forming clusters of friends: if person A is friends with person B, and person B is friends with person C, then they all belong to the same friend group. Similarly, variables connected through equality form groups that must share the same value.
The challenge comes when we need to check inequality constraints. If two variables are declared as not equal (!=
), they must belong to different groups. But how do we efficiently track which variables belong to which group, especially when groups can merge through equality relationships?
This naturally leads us to Union-Find, a data structure designed exactly for this purpose - tracking connected components and merging them efficiently. Here's the thought process:
-
Process equalities first: When we see
a == b
, we union these two variables into the same group. This is like saying "these two must have the same value, so put them in the same set." -
Why process all equalities before inequalities? We need to fully establish all the groups before checking if any inequality constraint is violated. If we checked inequalities while still processing equalities, we might incorrectly think two variables are in different groups when a later equality would put them in the same group.
-
Check inequalities for contradictions: After all groups are formed, for each
a != b
, we check ifa
andb
ended up in the same group. If they did, it's impossible to satisfy the equations (we can't assign the same value to make them equal while also needing them to be different).
The beauty of this approach is that Union-Find handles the transitivity automatically - when we union a
with b
and then b
with c
, the data structure ensures a
and c
are recognized as being in the same group without us explicitly connecting them.
Learn more about Union Find and Graph patterns.
Solution Approach
The solution implements a Union-Find (Disjoint Set Union) data structure with path compression to efficiently group variables and check for contradictions.
Data Structure Initialization:
p = list(range(26))
We create a parent array p
where each index represents a lowercase letter (0 for 'a', 1 for 'b', ..., 25 for 'z'). Initially, each variable is its own parent, meaning each variable forms its own group.
Find Function with Path Compression:
def find(x):
if p[x] != x:
p[x] = find(p[x])
return p[x]
This recursive function finds the root/representative of a variable's group. Path compression optimization is used - when finding the root, we update the parent pointers along the path to point directly to the root, flattening the tree structure for faster future lookups.
Processing Equality Equations:
for e in equations:
a, b = ord(e[0]) - ord('a'), ord(e[-1]) - ord('a')
if e[1] == '=':
p[find(a)] = find(b)
- Convert characters to indices:
ord(e[0]) - ord('a')
converts the first character to an index (0-25) - For equality equations (identified by
e[1] == '='
), we union the two variables by making one root point to the other p[find(a)] = find(b)
connects the root ofa
's group to the root ofb
's group
Checking Inequality Constraints:
for e in equations:
a, b = ord(e[0]) - ord('a'), ord(e[-1]) - ord('a')
if e[1] == '!' and find(a) == find(b):
return False
return True
- In the second pass, we check all inequality equations (identified by
e[1] == '!'
) - If
find(a) == find(b)
, it means both variables belong to the same group (must have the same value) - This creates a contradiction since the inequality states they must be different
- If no contradictions are found after checking all inequalities, return
True
Time Complexity: O(n × α(26))
where n
is the number of equations and α
is the inverse Ackermann function (practically constant)
Space Complexity: O(1)
since we only use a fixed-size array of 26 elements regardless of input size
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the example with equations: ["a==b", "b==c", "a!=c"]
Initial Setup:
- Parent array
p = [0, 1, 2, ..., 25]
where index 0 represents 'a', 1 represents 'b', 2 represents 'c' - Initially:
p[0] = 0
(a is its own parent),p[1] = 1
(b is its own parent),p[2] = 2
(c is its own parent)
First Pass - Process Equality Equations:
-
Process "a==b":
- Convert:
a = 0
,b = 1
- Since
e[1] == '='
, we union them find(0)
returns 0 (a's root is itself)find(1)
returns 1 (b's root is itself)- Execute:
p[0] = 1
(make a's root point to b's root) - Now a and b are in the same group
- Convert:
-
Process "b==c":
- Convert:
b = 1
,c = 2
- Since
e[1] == '='
, we union them find(1)
returns 1 (b's root is itself)find(2)
returns 2 (c's root is itself)- Execute:
p[1] = 2
(make b's root point to c's root) - Now b and c are in the same group
- Convert:
Current state: p = [1, 2, 2, 3, 4, ...]
- a points to b (p[0] = 1)
- b points to c (p[1] = 2)
- c points to itself (p[2] = 2)
- All three variables are now in the same group with c as the root
Second Pass - Check Inequality Equations:
- Check "a!=c":
- Convert:
a = 0
,c = 2
- Since
e[1] == '!'
, we check if they're in the same group find(0)
:- p[0] = 1, so recursively call find(1)
- p[1] = 2, so recursively call find(2)
- p[2] = 2, return 2
- Path compression updates: p[0] = 2, p[1] = 2
find(2)
returns 2- Since
find(0) == find(2)
(both return 2), they're in the same group - This violates the inequality constraint!
- Return False
- Convert:
The equations are contradictory because through transitivity (a==b and b==c implies a==c), but we also have a!=c, which is impossible to satisfy.
Solution Implementation
1from typing import List
2
3class Solution:
4 def equationsPossible(self, equations: List[str]) -> bool:
5 """
6 Determines if all given equations can be satisfied simultaneously.
7 Uses Union-Find (Disjoint Set Union) to group equal variables together.
8
9 Args:
10 equations: List of equations in format "a==b" or "a!=b"
11
12 Returns:
13 True if all equations can be satisfied, False otherwise
14 """
15
16 def find(x: int) -> int:
17 """
18 Find the root parent of element x with path compression.
19
20 Args:
21 x: The element to find the root for
22
23 Returns:
24 The root parent of element x
25 """
26 if parent[x] != x:
27 # Path compression: directly connect x to its root
28 parent[x] = find(parent[x])
29 return parent[x]
30
31 # Initialize parent array where each element is its own parent
32 # Index represents character (0='a', 1='b', ..., 25='z')
33 parent = list(range(26))
34
35 # First pass: Process all equality equations to union equal variables
36 for equation in equations:
37 # Extract the two variables from the equation
38 var1_index = ord(equation[0]) - ord('a')
39 var2_index = ord(equation[-1]) - ord('a')
40
41 # If it's an equality equation, union the two variables
42 if equation[1] == '=':
43 root1 = find(var1_index)
44 root2 = find(var2_index)
45 parent[root1] = root2
46
47 # Second pass: Check all inequality equations for contradictions
48 for equation in equations:
49 # Extract the two variables from the equation
50 var1_index = ord(equation[0]) - ord('a')
51 var2_index = ord(equation[-1]) - ord('a')
52
53 # If it's an inequality equation, check if variables are in same group
54 if equation[1] == '!':
55 if find(var1_index) == find(var2_index):
56 # Contradiction: variables should be different but are in same group
57 return False
58
59 # All equations can be satisfied
60 return True
61
1class Solution {
2 // Parent array for Union-Find data structure
3 // Each index represents a letter (0-25 for a-z)
4 private int[] parent;
5
6 /**
7 * Determines if all given equations can be satisfied simultaneously.
8 * Uses Union-Find to group variables that must be equal.
9 *
10 * @param equations Array of equation strings in format "a==b" or "a!=b"
11 * @return true if all equations can be satisfied, false otherwise
12 */
13 public boolean equationsPossible(String[] equations) {
14 // Initialize parent array where each variable is its own parent initially
15 parent = new int[26];
16 for (int i = 0; i < 26; i++) {
17 parent[i] = i;
18 }
19
20 // First pass: Process all equality equations to build union sets
21 for (String equation : equations) {
22 // Extract variables from positions 0 and 3 in the equation string
23 int variable1 = equation.charAt(0) - 'a';
24 int variable2 = equation.charAt(3) - 'a';
25
26 // If this is an equality equation, union the two variables
27 if (equation.charAt(1) == '=') {
28 int root1 = find(variable1);
29 int root2 = find(variable2);
30 parent[root1] = root2; // Union operation
31 }
32 }
33
34 // Second pass: Check all inequality equations for contradictions
35 for (String equation : equations) {
36 // Extract variables from positions 0 and 3 in the equation string
37 int variable1 = equation.charAt(0) - 'a';
38 int variable2 = equation.charAt(3) - 'a';
39
40 // If this is an inequality equation, check if variables are in same set
41 if (equation.charAt(1) == '!') {
42 // If both variables have the same root, they're equal
43 // This contradicts the inequality equation
44 if (find(variable1) == find(variable2)) {
45 return false;
46 }
47 }
48 }
49
50 // All equations are consistent
51 return true;
52 }
53
54 /**
55 * Find operation with path compression for Union-Find.
56 * Finds the root parent of the given element.
57 *
58 * @param x The element to find the root for
59 * @return The root parent of element x
60 */
61 private int find(int x) {
62 // Path compression: make all nodes point directly to root
63 if (parent[x] != x) {
64 parent[x] = find(parent[x]);
65 }
66 return parent[x];
67 }
68}
69
1class Solution {
2public:
3 vector<int> parent;
4
5 bool equationsPossible(vector<string>& equations) {
6 // Initialize parent array for 26 lowercase letters
7 parent.resize(26);
8 for (int i = 0; i < 26; ++i) {
9 parent[i] = i; // Each letter is initially its own parent
10 }
11
12 // First pass: Process all equality equations to build union-find structure
13 for (auto& equation : equations) {
14 int variableA = equation[0] - 'a'; // Convert first variable to index (0-25)
15 int variableB = equation[3] - 'a'; // Convert second variable to index (0-25)
16
17 if (equation[1] == '=') {
18 // Union operation: connect the two variables
19 parent[find(variableA)] = find(variableB);
20 }
21 }
22
23 // Second pass: Check all inequality equations for contradictions
24 for (auto& equation : equations) {
25 int variableA = equation[0] - 'a'; // Convert first variable to index (0-25)
26 int variableB = equation[3] - 'a'; // Convert second variable to index (0-25)
27
28 if (equation[1] == '!') {
29 // If two variables that should not be equal have the same parent, return false
30 if (find(variableA) == find(variableB)) {
31 return false;
32 }
33 }
34 }
35
36 return true; // All equations are satisfiable
37 }
38
39 // Find operation with path compression for Union-Find
40 int find(int x) {
41 if (parent[x] != x) {
42 parent[x] = find(parent[x]); // Path compression: directly connect to root
43 }
44 return parent[x];
45 }
46};
47
1// Parent array for Union-Find data structure, tracking parent of each letter (a-z)
2let parent: number[];
3
4/**
5 * Initialize the Union-Find structure with each element as its own parent
6 */
7function initializeUnionFind(): void {
8 parent = Array.from({ length: 26 }, (_, index) => index);
9}
10
11/**
12 * Find the root parent of a given index with path compression
13 * @param index - The index to find the root parent for
14 * @returns The root parent index
15 */
16function find(index: number): number {
17 // If element is its own parent, it's the root
18 if (parent[index] === index) {
19 return index;
20 }
21 // Path compression: directly connect to root parent
22 parent[index] = find(parent[index]);
23 return parent[index];
24}
25
26/**
27 * Union two elements by connecting their root parents
28 * @param index1 - First element index
29 * @param index2 - Second element index
30 */
31function union(index1: number, index2: number): void {
32 // Connect root of index1 to root of index2
33 parent[find(index1)] = find(index2);
34}
35
36/**
37 * Check if all equations can be satisfied simultaneously
38 * @param equations - Array of equations in format "a==b" or "a!=b"
39 * @returns true if all equations can be satisfied, false otherwise
40 */
41function equationsPossible(equations: string[]): boolean {
42 // Initialize Union-Find structure
43 initializeUnionFind();
44
45 // First pass: process all equality equations to build connected components
46 for (const equation of equations) {
47 const firstVariable = equation[0];
48 const operator = equation[1];
49 const secondVariable = equation[3];
50
51 // Union variables that are equal
52 if (operator === '=') {
53 const firstIndex = firstVariable.charCodeAt(0) - 'a'.charCodeAt(0);
54 const secondIndex = secondVariable.charCodeAt(0) - 'a'.charCodeAt(0);
55 union(firstIndex, secondIndex);
56 }
57 }
58
59 // Second pass: check all inequality equations for contradictions
60 for (const equation of equations) {
61 const firstVariable = equation[0];
62 const operator = equation[1];
63 const secondVariable = equation[3];
64
65 // Check if inequality is violated (variables in same component but should be different)
66 if (operator === '!') {
67 const firstIndex = firstVariable.charCodeAt(0) - 'a'.charCodeAt(0);
68 const secondIndex = secondVariable.charCodeAt(0) - 'a'.charCodeAt(0);
69
70 // If both variables have same root parent, they're equal (contradiction)
71 if (find(firstIndex) === find(secondIndex)) {
72 return false;
73 }
74 }
75 }
76
77 // All equations can be satisfied
78 return true;
79}
80
Time and Space Complexity
Time Complexity: O(n * α(26))
≈ O(n)
where n
is the number of equations.
- The code iterates through the equations list twice (two separate for loops), each taking
O(n)
time. - Within each iteration, the
find()
function is called, which uses path compression. The amortized time complexity offind()
with path compression isO(α(m))
, whereα
is the inverse Ackermann function andm
is the number of elements in the disjoint set. - Since we're dealing with at most 26 lowercase letters,
m = 26
, makingα(26)
effectively a small constant (practically ≤ 4). - Therefore, the overall time complexity is
O(n * α(26))
, which simplifies toO(n)
.
Space Complexity: O(1)
- The parent array
p
has a fixed size of 26 (for 26 lowercase letters), which isO(26) = O(1)
. - The recursion stack for the
find()
function can go at most 26 levels deep in the worst case (before path compression), which is alsoO(26) = O(1)
. - No other data structures scale with the input size.
- Therefore, the overall space complexity is
O(1)
constant space.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Processing Equations in Wrong Order
Pitfall: A common mistake is processing inequality equations (!=
) before or mixed with equality equations (==
). This leads to incorrect results because the union-find structure isn't fully built when checking contradictions.
Example of incorrect approach:
# WRONG: Processing equations as they come
for equation in equations:
var1 = ord(equation[0]) - ord('a')
var2 = ord(equation[-1]) - ord('a')
if equation[1] == '=':
parent[find(var1)] = find(var2)
else: # equation[1] == '!'
if find(var1) == find(var2):
return False
Why it fails: Consider equations ["a!=b", "b==c", "a==c"]
. If we process them in order:
- First:
a!=b
- passes (a and b are in different groups) - Second:
b==c
- union b and c - Third:
a==c
- union a and c (now a, b, c are all in same group) - Result: Returns
True
but should returnFalse
since a!=b contradicts a==c and b==c
Solution: Always process in two separate passes:
# CORRECT: Two-pass approach
# First pass: Process ALL equality equations
for equation in equations:
if equation[1] == '=':
var1 = ord(equation[0]) - ord('a')
var2 = ord(equation[-1]) - ord('a')
parent[find(var1)] = find(var2)
# Second pass: Check ALL inequality equations
for equation in equations:
if equation[1] == '!':
var1 = ord(equation[0]) - ord('a')
var2 = ord(equation[-1]) - ord('a')
if find(var1) == find(var2):
return False
2. Incorrect Union Operation
Pitfall: Directly setting parent without finding roots first.
Incorrect:
# WRONG: Direct parent assignment if equation[1] == '=': parent[var1] = var2 # Should union roots, not variables directly
Why it fails: This doesn't properly merge the entire groups. If var1 already has children in its group, they won't be connected to var2's group.
Solution: Always union the roots:
# CORRECT: Union by root if equation[1] == '=': parent[find(var1)] = find(var2)
3. Missing Path Compression
Pitfall: Implementing find without path compression can lead to performance degradation.
Inefficient:
# WITHOUT path compression - can be slow
def find(x):
while parent[x] != x:
x = parent[x]
return x
Solution: Use path compression for optimal performance:
# WITH path compression - optimized
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # Compress path
return parent[x]
4. Incorrect Character-to-Index Mapping
Pitfall: Using wrong ASCII conversion or forgetting the offset.
Incorrect examples:
# WRONG: Direct ord() without offset
var1 = ord(equation[0]) # Results in 97 for 'a', out of bounds!
# WRONG: Using wrong index
var1 = equation[0] # This is a string, not an integer index
Solution: Always subtract ord('a')
to get 0-based index:
# CORRECT: Proper conversion
var1 = ord(equation[0]) - ord('a') # 'a' -> 0, 'b' -> 1, etc.
How many ways can you arrange the three letters A, B and C?
Recommended Readings
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!