195. Number of Provinces
๐ LeetCode Problem: 547. Number of Provinces
๐ Difficulty: Medium
๐ท๏ธ Topics: Graph, DFS, BFS, Union-Find, Connected Components
Problem Statement
There are n cities. Some of them are connected, while some are not. If city a is connected directly with city b, and city b is connected directly with city c, then city a is connected indirectly with city c.
A province is a group of directly or indirectly connected cities and no other cities outside of the group.
You are given an n x n matrix isConnected where isConnected[i][j] = 1 if the iแตสฐ city and the jแตสฐ city are directly connected, and isConnected[i][j] = 0 otherwise.
Return the total number of provinces.
Example 1:
Input: isConnected = [[1,1,0],
[1,1,0],
[0,0,1]]
Output: 2
Explanation:
Cities 0 and 1 are connected: 0 โ 1
City 2 is alone: 2
Province 1: {0, 1}
Province 2: {2}
Total: 2 provinces
Example 2:
Input: isConnected = [[1,0,0],
[0,1,0],
[0,0,1]]
Output: 3
Explanation:
No cities are connected.
Province 1: {0}
Province 2: {1}
Province 3: {2}
Total: 3 provinces (all isolated)
Constraints:
- 1 <= n <= 200
- n == isConnected.length
- n == isConnected[i].length
- isConnected[i][j] is 1 or 0.
- isConnected[i][i] == 1
- isConnected[i][j] == isConnected[j][i]
๐ณ Visual Understanding - The Complete Picture
The Problem We're Solving ๐ค
Given connections between cities, find number of groups: ๐๏ธ
Example:
Cities: 0, 1, 2, 3
Connections: 0โ1, 2โ3
Groups (Provinces):
Group 1: {0, 1} (connected to each other)
Group 2: {2, 3} (connected to each other)
Answer: 2 provinces
Key question: How many separate groups of connected cities? ๐ฏ
Understanding the Matrix ๐
isConnected matrix: shows direct connections
Example:
0 1 2
0 [1, 1, 0] City 0: connected to 0 (itself), 1
1 [1, 1, 0] City 1: connected to 0, 1 (itself)
2 [0, 0, 1] City 2: connected to 2 (itself only)
Visual:
0 โ 1 2
Two provinces! โ
Another example:
0 1 2 3
0 [1, 1, 0, 0]
1 [1, 1, 1, 0]
2 [0, 1, 1, 0]
3 [0, 0, 0, 1]
Connections:
0โ1, 1โ2 (0,1,2 form one group)
3 alone (3 is isolated)
Visual:
0 โ 1 โ 2 3
Two provinces: {0,1,2} and {3} โ
๐ง The Journey - From Real World to Union-Find
Part 1: The Real World Problem - "Friend Groups" ๐ฅ
๐ญ Imagine a classroom with students:
Students: Alice, Bob, Carol, Dave, Eve
Friendships:
Alice โ Bob
Carol โ Dave
Eve is alone
Question: How many friend groups?
Groups:
Group 1: {Alice, Bob}
Group 2: {Carol, Dave}
Group 3: {Eve}
Answer: 3 friend groups! โ
This is EXACTLY our provinces problem! ๐ฏ
Just replace "students" with "cities"
and "friendships" with "connections"!
Part 2: The Naive Way - "Ask Everyone!" ๐ค
๐ญ How would you count friend groups in real life?
Approach 1: Start with someone, ask who their friends are
Start with Alice:
"Who are your friends?"
Alice: "Bob!"
"Bob, who are your friends?"
Bob: "Alice!"
Group 1 complete: {Alice, Bob} โ
Start with Carol (unvisited):
"Who are your friends?"
Carol: "Dave!"
"Dave, who are your friends?"
Dave: "Carol!"
Group 2 complete: {Carol, Dave} โ
Start with Eve (unvisited):
"Who are your friends?"
Eve: "Nobody!"
Group 3 complete: {Eve} โ
Total: 3 groups!
This is DFS/BFS approach! ๐
Part 3: The Smart Way - "Union-Find Intro" ๐ก
๐ญ But there's a SMARTER way using Union-Find!
Real world analogy: School clubs! ๐ซ
Initially, everyone is their own club:
Alice's club: {Alice}
Bob's club: {Bob}
Carol's club: {Carol}
Dave's club: {Dave}
Eve's club: {Eve}
Total clubs: 5 โ
Now, process friendships:
Friendship: Alice โ Bob
Action: MERGE their clubs!
Result:
AliceBob's club: {Alice, Bob}
Carol's club: {Carol}
Dave's club: {Dave}
Eve's club: {Eve}
Total clubs: 4 โ
Friendship: Carol โ Dave
Action: MERGE their clubs!
Result:
AliceBob's club: {Alice, Bob}
CarolDave's club: {Carol, Dave}
Eve's club: {Eve}
Total clubs: 3 โ
No more friendships!
Final answer: 3 groups! ๐ฏ
This is Union-Find! ๐
Part 4: Union-Find Core Concepts ๐
โจ Union-Find has THREE key operations:
1๏ธโฃ MAKE-SET: Create individual sets
Initially, everyone is their own set
2๏ธโฃ FIND: Find which set someone belongs to
"Who is the leader of your group?"
3๏ธโฃ UNION: Merge two sets together
"Join these two groups into one!"
Data structure: Array of parents! ๐
Example with 5 people (indices 0-4):
parent = [0, 1, 2, 3, 4]
Meaning: Each person is their own parent (own leader)
Person 0: parent[0] = 0 (leader of self)
Person 1: parent[1] = 1 (leader of self)
Person 2: parent[2] = 2 (leader of self)
Person 3: parent[3] = 3 (leader of self)
Person 4: parent[4] = 4 (leader of self)
After union(0, 1):
parent = [0, 0, 2, 3, 4]
// โ
Person 1 now points to 0 as leader!
Group: {0, 1} with leader 0
After union(2, 3):
parent = [0, 0, 2, 2, 4]
// โ
Person 3 now points to 2 as leader!
Groups: {0, 1} with leader 0
{2, 3} with leader 2
{4} with leader 4
Total groups: 3 โ
Count groups: Count how many are their own parent!
parent[0] == 0? YES โ
(leader)
parent[1] == 1? NO โ
parent[2] == 2? YES โ
(leader)
parent[3] == 3? NO โ
parent[4] == 4? YES โ
(leader)
Leaders: 3 โ Answer: 3 groups! ๐ฏ
Part 5: Union-Find Operations in Detail ๐
๐ญ Let's understand FIND and UNION deeply:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FIND: "Who's your leader?" โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
find(x):
Keep following parent pointers until you find the leader!
Leader = someone who is their own parent
Example:
parent = [0, 0, 1, 2]
find(0):
parent[0] = 0
0 == 0? YES! Leader found! โ
Return: 0
find(1):
parent[1] = 0
1 == 0? NO
Follow: go to 0
parent[0] = 0
0 == 0? YES! Leader found! โ
Return: 0
find(2):
parent[2] = 1
2 == 1? NO
Follow: go to 1
parent[1] = 0
1 == 0? NO
Follow: go to 0
parent[0] = 0
0 == 0? YES! Leader found! โ
Return: 0
find(3):
parent[3] = 2
Follow chain: 3 โ 2 โ 1 โ 0 โ
Return: 0
All point to same leader 0!
They're in the same group! ๐ฏ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ UNION: "Merge two groups!" โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
union(x, y):
1. Find leader of x's group
2. Find leader of y's group
3. Make one leader point to the other!
Example:
parent = [0, 1, 2] (3 separate groups)
union(0, 1):
leader_0 = find(0) = 0
leader_1 = find(1) = 1
Make 1 point to 0:
parent[1] = 0
Result: parent = [0, 0, 2]
Groups: {0, 1} and {2}
union(1, 2):
leader_1 = find(1) = 0 (1's leader is 0!)
leader_2 = find(2) = 2
Make 2 point to 0:
parent[2] = 0
Result: parent = [0, 0, 0]
Groups: {0, 1, 2} - ALL CONNECTED! โ
Beautiful! ๐
Part 6: Why Union-Find is Powerful ๐ช
๐ญ Comparing approaches:
DFS/BFS Approach:
โ
Easy to understand
โ
Works perfectly
โ Builds graph first
โ Visits every node/edge
Time: O(nยฒ) for this problem
Union-Find Approach:
โ
Direct: process connections as we see them
โ
No graph needed!
โ
Efficient with optimizations
โ
Answers "are these connected?" in O(1) amortized!
Time: O(nยฒ) but with TINY constant factor
Real power shows in DYNAMIC scenarios:
"Are cities X and Y connected?"
Union-Find: O(1) amortized! โก
DFS/BFS: O(n) every time! โ
For this problem, both work!
But Union-Find teaches fundamental pattern! ๐ฏ
๐ฏ Approach 1: DFS (Easiest to Understand!)
The Key Insight ๐ก
Use DFS to find connected components ๐. Treat matrix as graph: if isConnected[i][j] = 1, there's edge iโj ๐. For each unvisited city, start DFS to mark all cities in same province ๐. Count how many DFS starts = number of provinces! ๐ฏ. Time: O(nยฒ) โก, Space: O(n) ๐ฆ!
Classic connected components! โจ
Complete Implementation
class Solution {
public int findCircleNum(int[][] isConnected) {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// DFS APPROACH - FIND CONNECTED COMPONENTS ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Each DFS explores one complete province
// Count number of DFS starts = number of provinces!
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
int n = isConnected.length;
boolean[] visited = new boolean[n];
int provinces = 0;
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// TRY STARTING DFS FROM EACH CITY ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
for (int i = 0; i < n; i++) {
if (!visited[i]) {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// FOUND NEW PROVINCE! ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
dfs(i, isConnected, visited);
provinces++;
}
}
return provinces;
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// DFS: MARK ALL CITIES IN SAME PROVINCE ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
private void dfs(int city, int[][] isConnected, boolean[] visited) {
visited[city] = true;
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// VISIT ALL CONNECTED CITIES ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
for (int other = 0; other < isConnected.length; other++) {
if (isConnected[city][other] == 1 && !visited[other]) {
dfs(other, isConnected, visited);
}
}
}
}
Why This Works ๐ฏ
DFS explores entire province: ๐ - Start from unvisited city - Visit all connected cities recursively - Marks entire province as visited!
Count DFS starts: ๐ - Each DFS start = found new province - Already visited cities don't start new DFS - Total starts = total provinces!
Simple and intuitive: โจ - Just connected components counting - Standard graph algorithm!
๐ฏ Approach 2: BFS (Also Easy!)
The Key Insight ๐ก
Use BFS for level-by-level exploration ๐. Same idea as DFS but with queue! ๐ For each unvisited city, BFS marks all connected cities in same province ๐. Time: O(nยฒ) โก, Space: O(n) ๐ฆ!
BFS alternative! ๐
Complete Implementation
class Solution {
public int findCircleNum(int[][] isConnected) {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// BFS APPROACH - FIND CONNECTED COMPONENTS ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
int n = isConnected.length;
boolean[] visited = new boolean[n];
int provinces = 0;
for (int i = 0; i < n; i++) {
if (!visited[i]) {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// FOUND NEW PROVINCE! BFS to mark all ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
bfs(i, isConnected, visited);
provinces++;
}
}
return provinces;
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// BFS: EXPLORE PROVINCE LEVEL BY LEVEL ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
private void bfs(int start, int[][] isConnected, boolean[] visited) {
Queue<Integer> queue = new LinkedList<>();
queue.offer(start);
visited[start] = true;
while (!queue.isEmpty()) {
int city = queue.poll();
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// CHECK ALL POTENTIAL CONNECTIONS ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
for (int other = 0; other < isConnected.length; other++) {
if (isConnected[city][other] == 1 && !visited[other]) {
queue.offer(other);
visited[other] = true;
}
}
}
}
}
๐ฏ Approach 3: Union-Find (The New Pattern!)
The Key Insight ๐ก
Use Union-Find data structure for dynamic connectivity! ๐ Initialize each city as its own set ๐๏ธ. For each connection, union the two cities ๐ค. Count final number of disjoint sets = provinces! ๐ Time: O(nยฒ ร ฮฑ(n)) where ฮฑ is inverse Ackermann (โ O(1)) โก, Space: O(n) ๐ฆ!
Efficient and elegant! ๐
Complete Implementation
class Solution {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// UNION-FIND DATA STRUCTURE ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
private int[] parent;
private int[] rank; // For union by rank optimization
public int findCircleNum(int[][] isConnected) {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// UNION-FIND APPROACH ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Process connections, union cities
// Count disjoint sets at the end!
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
int n = isConnected.length;
parent = new int[n];
rank = new int[n];
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// INITIALIZE: Each city is its own set ๐๏ธ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
for (int i = 0; i < n; i++) {
parent[i] = i; // Each is its own parent (leader)
rank[i] = 0; // Initial rank is 0
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// PROCESS ALL CONNECTIONS ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) { // j > i to avoid duplicates
if (isConnected[i][j] == 1) {
union(i, j); // Merge their sets!
}
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// COUNT DISJOINT SETS ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
int provinces = 0;
for (int i = 0; i < n; i++) {
if (parent[i] == i) { // Is this a leader?
provinces++;
}
}
return provinces;
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// FIND: Get the leader (root) of the set ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// With PATH COMPRESSION optimization! โก
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
private int find(int x) {
if (parent[x] != x) {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// PATH COMPRESSION: Make x point directly to root
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
parent[x] = find(parent[x]);
}
return parent[x];
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// UNION: Merge two sets ๐ค
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// With UNION BY RANK optimization! โก
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
private void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return; // Already in same set!
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// UNION BY RANK: Attach smaller tree under larger
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
Why This Works ๐ฏ
Union-Find maintains disjoint sets: ๐ - Each city starts in its own set - Union merges sets when cities connected - Find tells us which set a city belongs to!
Path compression optimization: โก
- Makes trees flat
- Future finds become O(1)!
- parent[x] = find(parent[x])
Union by rank optimization: ๐ - Keeps trees balanced - Attach smaller tree to larger - Prevents long chains!
Count leaders = count sets: ๐ฏ
- Leaders: nodes where parent[i] == i
- Each leader represents one province!
๐ Detailed Dry Run - Union-Find Approach
Let's trace Union-Find on:
isConnected = [[1,1,0],
[1,1,0],
[0,0,1]]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ INITIALIZATION โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
n = 3 cities (0, 1, 2)
Initialize parent and rank:
parent = [0, 1, 2] // Each city is its own parent
rank = [0, 0, 0] // All ranks start at 0
Visualization:
0 1 2 (3 separate sets)
Sets: {0}, {1}, {2}
Count: 3 provinces initially
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PROCESS CONNECTIONS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CHECK: isConnected[0][1] = 1 โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
โ Connection found: 0 โ 1 โ
โ Call: union(0, 1) โ
โ โ
โ Step 1: Find leaders โ
โ rootX = find(0) = 0 โ
โ rootY = find(1) = 1 โ
โ โ
โ Step 2: Compare ranks โ
โ rank[0] = 0 โ
โ rank[1] = 0 โ
โ Equal ranks! Attach 1 to 0 โ
โ โ
โ Step 3: Union โ
โ parent[1] = 0 โ
โ rank[0]++ (0 โ 1) โ
โ โ
โ Result: โ
โ parent = [0, 0, 2] โ
โ rank = [1, 0, 0] โ
โ โ
โ Visualization: โ
โ 0 โ
โ โ โ
โ 1 2 โ
โ โ
โ Sets: {0, 1}, {2} โ
โ Count: 2 provinces now! โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CHECK: isConnected[0][2] = 0 โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
โ No connection! Skip! โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ CHECK: isConnected[1][2] = 0 โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
โ No connection! Skip! โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ COUNT PROVINCES โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Final state:
parent = [0, 0, 2]
Count leaders (parent[i] == i):
parent[0] == 0? YES โ
Leader found! Count = 1
parent[1] == 1? NO โ (parent[1] = 0)
parent[2] == 2? YES โ
Leader found! Count = 2
Total provinces: 2 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FINAL RESULT: 2 โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
โ โ
โ Province 1: Cities {0, 1} (leader: 0) โ
โ Province 2: City {2} (leader: 2) โ
โ โ
โ Visual: โ
โ 0 โ 1 2 โ
โ โ
โ Answer: 2 provinces! ๐ฏ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ Key Observations:
-
Union merges sets: ๐ค Cities 0 and 1 merged into one set
-
Path compression not needed here: โก Trees are already flat (depth 1)
-
Union by rank keeps balanced: ๐ Prevented unnecessary depth
-
Counting leaders gives answer: ๐ฏ 2 leaders = 2 provinces
-
Efficient processing: โจ Each connection processed in O(ฮฑ(n)) โ O(1)
๐ Complexity Analysis
DFS/BFS Approaches
Time Complexity: O(nยฒ) โก
Where n = number of cities
For each city: O(n)
DFS/BFS visits: O(n) in worst case
Check all connections: O(n)
Total: O(n) cities ร O(n) = O(nยฒ)
Example: n = 200
Operations: 40,000 โ
Very fast!
Space Complexity: O(n) ๐ฆ
Recursion stack (DFS): O(n) worst case
Queue (BFS): O(n) worst case
Visited array: O(n)
Total: O(n) ๐ฆ
Union-Find Approach
Time Complexity: O(nยฒ ร ฮฑ(n)) โก
Where ฮฑ(n) is inverse Ackermann function
Initialize: O(n)
Process connections: O(nยฒ) pairs
Each union/find: O(ฮฑ(n)) โ O(1) amortized!
Total: O(nยฒ) practically!
ฮฑ(n) is INCREDIBLY small:
n = 10^80 (atoms in universe): ฮฑ(n) โค 4!
For all practical n: ฮฑ(n) โค 5
So effectively O(nยฒ)! โก
Space Complexity: O(n) ๐ฆ
Parent array: O(n)
Rank array: O(n)
Total: O(n) ๐ฆ
๐ Union-Find Pattern Deep Dive
When to Use Union-Find ๐ฏ
โ
Perfect for Union-Find:
โข "Count connected components"
โข "Are X and Y connected?"
โข "Merge two groups"
โข "Dynamic connectivity"
โข "Disjoint sets"
๐ Recognition keywords:
โข "Connected"
โข "Group"
โข "Component"
โข "Cluster"
โข "Merge"
โข "Union"
๐ Problem types:
โข Network connectivity
โข Friend circles
โข Island counting (alternative)
โข Redundant connections
โข Graph connectivity
Union-Find Template ๐
class UnionFind {
private int[] parent;
private int[] rank;
// Constructor: Initialize
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i; // Each is own parent
}
}
// Find with path compression
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // Path compression!
}
return parent[x];
}
// Union with rank
public boolean union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return false; // Already connected
}
// Union by rank
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
return true; // Successfully united
}
// Check if connected
public boolean connected(int x, int y) {
return find(x) == find(y);
}
// Count components
public int countComponents() {
int count = 0;
for (int i = 0; i < parent.length; i++) {
if (parent[i] == i) count++;
}
return count;
}
}
Optimizations Explained ๐
Path Compression: โก
Without path compression:
Chain: 0 โ 1 โ 2 โ 3 โ 4
find(4): 4 hops to reach 0!
With path compression:
After find(4):
All point directly to 0!
0
/ | \ \
1 2 3 4
Next find(4): 1 hop! โก
Implementation:
parent[x] = find(parent[x])
This makes trees flat!
Future operations: O(1)! ๐
Union by Rank: ๐
Without union by rank:
Might create long chains:
0 โ 1 โ 2 โ 3 ... โ n
Bad! O(n) finds!
With union by rank:
Always attach smaller tree to larger:
Keeps trees balanced!
Example:
Tree A (rank 2): Tree B (rank 1):
0 5
/ \ |
1 2 6
|
3
Union A and B:
Attach B under A (A has higher rank):
0
/ | \
1 2 5
| |
3 6
Still balanced! Depth increases slowly!
Guarantees: O(log n) depth worst case
With path compression: O(ฮฑ(n)) amortized! ๐
๐ Quick Revision Notes
๐ฏ Core Concept
Number of Provinces finds connected components in undirected graph ๐. Three approaches: DFS (start from each unvisited, explore component) ๐, BFS (queue-based exploration) ๐, Union-Find (merge connected cities, count disjoint sets) ๐. Union-Find uses parent array to track set leaders ๐ฅ. Path compression makes trees flat โก. Union by rank keeps balanced ๐. Count nodes where parent[i] == i = answer! ๐ฏ Time: O(nยฒ) โก, Space: O(n) ๐ฆ!
โก Quick Implementation (Union-Find)
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
public class Solution {
public int findCircleNum(int[][] isConnected) {
// // Approach 1 - DFS
// return dfs(isConnected);
// Approach 2 - BFS
return bfs(isConnected);
}
private int bfs(int[][] cities) {
int count = 0;
HashSet<Integer> visited = new HashSet<>();
Queue<Integer> queue = new LinkedList<>();
for (int city1 = 0; city1 < cities.length; city1++) {
if (!visited.contains(city1)) {
visited.add(city1);
queue.offer(city1);
while (!queue.isEmpty()) {
int curr = queue.poll();
for (int city2 = 0; city2 < cities.length; city2++) {
if (!visited.contains(city2) && cities[curr][city2] == 1) {
visited.add(city2);
queue.offer(city2);
}
}
}
count++;
}
}
return count;
}
private int dfs(int[][] cities) {
int count = 0;
HashSet<Integer> visited = new HashSet<>();
for (int city1 = 0; city1 < cities.length; city1++) {
if (!visited.contains(city1)) {
visited.add(city1);
dfsUtil(city1, cities, visited);
count++;
}
}
return count;
}
private void dfsUtil(int city1, int[][] cities, HashSet<Integer> visited) {
for (int city2 = 0; city2 < cities.length; city2++) {
if (!visited.contains(city2) && cities[city1][city2] == 1) {
visited.add(city2);
dfsUtil(city2, cities, visited);
}
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.findCircleNum(new int[][] { { 1, 1, 0 }, { 1, 1, 0 }, { 0, 0, 1 } })); // 2
System.out.println(s.findCircleNum(new int[][] { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } })); // 3
}
}
๐ Key Insights
Union-Find Three Operations: ๐
The foundation of Union-Find:
1๏ธโฃ MAKE-SET: parent[i] = i
Create individual sets
2๏ธโฃ FIND: Follow parent pointers to root
find(x):
if (parent[x] != x)
parent[x] = find(parent[x]) // Path compression!
return parent[x]
3๏ธโฃ UNION: Merge two sets
union(x, y):
rootX = find(x)
rootY = find(y)
parent[rootX] = rootY // Simple version
These three operations solve ALL connectivity problems! ๐ฏ
Path Compression Magic: โก
Why it's brilliant:
Without compression:
Long chain: 0 โ 1 โ 2 โ 3 โ 4 โ 5
find(5): 5 hops! โ
With compression:
First find(5): 5 hops
But flattens tree:
0
/|\\\
1 2 3 4 5
Next find(5): 1 hop! โ
Next find(4): 1 hop! โ
Next find(3): 1 hop! โ
All future finds: O(1)! โก
Implementation: Just one line!
parent[x] = find(parent[x])
This recursively flattens entire path!
Genius! ๐
Union by Rank Strategy: ๐
Why rank matters:
Bad union (always attach right to left):
Union(0,1): 0 โ 1
Union(1,2): 0 โ 1 โ 2
Union(2,3): 0 โ 1 โ 2 โ 3
Result: Long chain! โ
Good union (by rank):
Track tree "height" (rank)
Always attach shorter tree to taller!
Union trees of rank 1 and 2:
Attach rank-1 under rank-2
Final rank: still 2! โ
Union trees of equal rank:
Attach either way
Final rank: increase by 1
Keeps trees balanced! ๐
Guarantees: depth โค log n
With path compression + rank:
Amortized: O(ฮฑ(n)) โ O(1)! โก
Nearly constant time! Magic! โจ
Counting Components: ๐ฏ
The beautiful trick:
After all unions, count leaders!
Leader = node where parent[i] == i
Example:
parent = [0, 0, 2, 0, 4]
Check each:
parent[0] == 0? YES โ
Leader 1
parent[1] == 1? NO โ
parent[2] == 2? YES โ
Leader 2
parent[3] == 3? NO โ
parent[4] == 4? YES โ
Leader 3
Count = 3 components! ๐ฏ
Why this works:
Each leader represents one disjoint set
Non-leaders point to their leader
Simple and elegant! ๐
๐ช Memory Aid
"Union-Find keeps sets apart" ๐
"Path compression makes it smart" โก
"Union by rank keeps trees flat" ๐
"Count leaders, and that's that!" ๐ฏ โจ
โ ๏ธ Don't Forget
- Initialize parent[i] = i - everyone starts separate! ๐๏ธ
- Path compression in find - flatten while finding! โก
- Union by rank - attach smaller to larger! ๐
- Check rootX != rootY - before union! ๐
- Count where parent[i] == i - these are leaders! ๐
- Three approaches all work - choose based on context! ๐ฏ
๐ Congratulations!
You've learned Union-Find - a fundamental algorithm pattern! ๐
What You Learned:
โ
Union-Find data structure - parent array tracking! ๐
โ
Path compression - O(1) amortized finds! โก
โ
Union by rank - balanced trees! ๐
โ
Connected components - in any graph! ๐ฏ
โ
Dynamic connectivity - real-time queries! ๐
The Beautiful Pattern:
Union-Find solves connectivity in 3 operations:
1. MAKE-SET: Initialize
2. FIND: Get leader
3. UNION: Merge sets
With optimizations:
- Path compression: O(ฮฑ(n)) finds
- Union by rank: balanced trees
Result: Nearly O(1) operations! โก
This will be your foundation for:
โ
Redundant connections
โ
Accounts merge
โ
Number of islands (alternative)
โ
Graph connectivity
โ
Minimum spanning tree
Master this pattern = unlock graph algorithms! ๐
Next problems will build on this foundation! ๐
Union-Find is your new superpower! ๐ชโจ๐ฏ