Facebook Pixel

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 variable xi equals variable yi
  • "xi!=yi" - indicates that variable xi does not equal variable yi

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 returns true because you can assign the same value to all three variables
  • If you have equations ["a==b", "b!=a"], this returns false 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:

  1. First pass: Process all equality equations ("==") by unioning the variables that should be equal
  2. 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.

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

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:

  1. 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."

  2. 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.

  3. Check inequalities for contradictions: After all groups are formed, for each a != b, we check if a and b 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 of a's group to the root of b'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 Evaluator

Example 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:

  1. 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
  2. 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

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:

  1. 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

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 of find() with path compression is O(α(m)), where α is the inverse Ackermann function and m 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 to O(n).

Space Complexity: O(1)

  • The parent array p has a fixed size of 26 (for 26 lowercase letters), which is O(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 also O(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 return False 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.
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

How many ways can you arrange the three letters A, B and C?


Recommended Readings

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

Load More