2Brute2Best
🔥0

Satisfiability of Equality Equations

Medium·LeetCode #990Basic Anchor
LeetCode
30-Second Mental ModelUnion-Find (equivalence) — union every equality, then check the inequalities
Complexity:O(N · (26 + N))→O(N · α)
Key Insight

Keep one group per letter. Join every == pair first, then check each != against the finished groups. Two letters with the same root are linked by a chain of ==, so a != between them is a contradiction.

Core Validity Invariant

After every == is merged, two letters are forced equal exactly when they share a root, so a != between them is a contradiction.

01

Problem

You get a list of equations between single lowercase letters. Each equation is either x==y, which says x and y must be equal, or x!=y, which says they must be different.

Return true if you can give every letter a value so that all the equations hold at the same time. Return false if the equations contradict each other.

Input

equations: a list of 4-character strings. Each one is a letter, then == or !=, then a letter. For example "a==b".

Output

true if all equations can hold together, false otherwise.

Example 1
Input: equations = ["a==b", "b!=a"]
Output: false

a equals b, so b cannot be different from a.

Example 2
Input: equations = ["b==a", "a==b"]
Output: true

Both equations say the same thing, so they agree.

Example 3
Input: equations = ["a==b", "b!=c", "c==a"]
Output: false

a equals b and c equals a, so b equals c. That breaks b!=c.

Example 4
Input: equations = ["c==c", "b==d", "x!=z"]
Output: true

c equals itself, b and d are linked to each other only, and x and z are unrelated.

Constraints
  • • 1 <= equations.length <= 500
  • • equations[i].length == 4
  • • equations[i][0] is a lowercase letter
  • • equations[i][1] is either '=' or '!'
  • • equations[i][2] is '='
  • • equations[i][3] is a lowercase letter
02

First, think yourself

Before opening any solution, try to answer these on paper. Use hints only when stuck — each hint is counted.

  1. 1.

    An == links two letters together. Which letters end up linked once I follow every == I have?

  2. 2.

    Suppose the list is a!=b, then a==c, then c==b. Can I decide a!=b the moment I read it? Why not?

  3. 3.

    Which structure already keeps track of sets of linked items, and how do I ask whether two items are in the same set?

  4. 4.

    Does a!=a need its own special case, or can the same check handle it?

  5. 5.

    What do I need to know about the whole list before I can judge a single !=?

03

Brute force approach

05

Optimized approach

06

Why does this algorithm work?

07

Dry run

08

All approaches compared

ApproachIdeaTimeSpace
Brute ForceBFS over the == links from one letter, once for each !=O(N · (26 + N))O(26 + N)
OptimalUnion-Find: merge every == pair, then fail a != whose letters share a rootO(N · α)O(1)
09

Why does this belong to this pattern?

Clues in the problem statement
  • “x == y”Joins x and y into one group. Equal is transitive, so the group grows along chains of `==`.
  • “x != y”A rule to check against the finished groups, not something to union.
  • “can all the equations hold together”One contradiction is enough to return false, and no contradiction means true.
Equal is transitive: a==b and b==c make a==c+Every != must join two different groups+Groups come from the == pairs only→Union-Find (equivalence) — union every equality, then check the inequalities

This is the grouping by equivalence sub-pattern, where == links become unions. What makes this one different from its neighbours is order: the check for != must wait until every == has been merged, because a later == can join two letters that an earlier != looked at. Accounts Merge only needs the unions. Here the != pairs are a test on the finished groups, so they run second. Compare roots with find(x) == find(y), not with parent[x] == parent[y], because a letter can sit two steps below its root.

10

Don't make this mistake

Checking the != in input order, in the same loop as the ==
✗ for (auto& e : equations) { if (e[1] == '!' && dsu.find(e[0] - 'a') == dsu.find(e[3] - 'a')) return false; if (e[1] == '=') dsu.unite(e[0] - 'a', e[3] - 'a'); } return true;
✓ for (auto& e : equations) if (e[1] == '=') dsu.unite(e[0] - 'a', e[3] - 'a'); for (auto& e : equations) if (e[1] == '!' && dsu.find(e[0] - 'a') == dsu.find(e[3] - 'a')) return false; return true;

This is the pattern-confusion trap. On ["a!=b", "a==c", "c==b"] the first equation is checked while a and b are still separate, so nothing fails. Then a==c and c==b join a, c and b into one group, but the loop has already passed a!=b. It returns true, and the answer is false.

Skipping the check for a!=a
✗ if (e[1] == '!' && e[0] != e[3] && dsu.find(e[0] - 'a') == dsu.find(e[3] - 'a')) return false;
✓ if (e[1] == '!' && dsu.find(e[0] - 'a') == dsu.find(e[3] - 'a')) return false;

A letter is never different from itself, so ["a!=a"] must return false. The plain find check already does that, since find(a) == find(a). The extra e[0] != e[3] test turns it into true.

Comparing parents instead of roots
✗ if (e[1] == '!' && dsu.parent[e[0] - 'a'] == dsu.parent[e[3] - 'a']) return false;
✓ if (e[1] == '!' && dsu.find(e[0] - 'a') == dsu.find(e[3] - 'a')) return false;

parent[x] is only one step up, not the group name. On ["a==b", "c==d", "b==d", "a!=d"] the unions leave parent[a] = a and parent[d] = c, so the parent check says a and d differ and returns true. But d sits under c, and c sits under a, so find(d) = a and the answer is false.

11

Want to implement this?

Write it yourself on LeetCode, then come back and compare your code with the solutions above.

Open coding workspace
How did it go?
Beyond The LeetCode Problem

How this concept connects to System Design & C++ Systems

Bridge DSA with production engineering

Top engineering interviews test whether you understand how algorithms like Union-Find (DSU) translate into production architectures and low-level memory mechanics.