947. Most Stones Removed with Same Row or Column
Problem Description
You are given n
stones placed on a 2D coordinate plane. Each stone is positioned at an integer coordinate point [x, y]
, and no two stones can occupy the same position.
A stone can be removed from the plane if it shares either the same row (same x-coordinate) or the same column (same y-coordinate) with at least one other stone that hasn't been removed yet.
Given an array stones
where stones[i] = [xi, yi]
represents the position of the i-th
stone, you need to find the maximum number of stones that can be removed.
For example, if you have stones at positions [0,0]
, [0,1]
, and [1,0]
:
- Stone at
[0,0]
shares row 0 with[0,1]
and column 0 with[1,0]
- Stone at
[0,1]
shares row 0 with[0,0]
- Stone at
[1,0]
shares column 0 with[0,0]
Since all stones are connected through shared rows or columns, you can remove 2 stones (leaving 1 remaining).
The key insight is that stones sharing rows or columns form connected groups. Within each connected group, you can remove all stones except one. The solution uses Union-Find to identify these connected components - stones in the same row or column are merged into the same component. The maximum removable stones equals the total number of successful union operations, which is equivalent to total stones - number of connected components
.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem can be modeled as a graph where stones are nodes, and edges exist between stones that share the same row or column.
Is it a tree?
- No: The graph formed by stones is not necessarily a tree. It can have cycles (e.g., stones forming a rectangle pattern) and may consist of multiple disconnected components.
Is the problem related to directed acyclic graphs?
- No: The relationships between stones are undirected - if stone A shares a row/column with stone B, then B also shares that row/column with A.
Is the problem related to shortest paths?
- No: We're not finding shortest paths between stones. We're trying to find connected components and determine the maximum number of removable stones.
Does the problem involve connectivity?
- Yes: The core of the problem is about connectivity - stones that share rows or columns form connected components, and within each component, all but one stone can be removed.
Disjoint Set Union
- Yes: DSU (Union-Find) is the suggested approach for handling connectivity problems efficiently.
Conclusion: The flowchart leads us to use Disjoint Set Union (DSU/Union-Find) for this connectivity problem. While the solution uses Union-Find rather than explicit DFS, the underlying concept is the same - we're exploring connected components in a graph. DFS could alternatively be used to identify connected components by traversing from each unvisited stone and marking all stones reachable through shared rows/columns.
Intuition
The key insight comes from understanding what makes a stone removable. A stone can be removed if it shares a row or column with another stone that hasn't been removed. This creates a dependency chain - as long as stones are connected through shared rows or columns, we can keep removing them until only one remains in that connected group.
Think of it this way: if we have stones at positions [0,0]
, [0,2]
, and [2,0]
, they form a connected component because:
[0,0]
and[0,2]
share row 0[0,0]
and[2,0]
share column 0
In this connected component of 3 stones, we can remove 2 of them, leaving just 1.
This pattern holds for any connected component: if a component has k
stones, we can remove exactly k-1
stones from it. The last stone in each component cannot be removed because it would have no other stones in its row or column.
Therefore, if we have n
total stones divided into c
connected components:
- Each component must keep 1 stone
- Total stones that must remain =
c
- Maximum removable stones =
n - c
To find these connected components, we can model the problem as a graph where stones are nodes and edges exist between stones sharing rows or columns. Union-Find is perfect for this because:
- We can efficiently merge stones that share rows/columns into the same component
- Each successful union operation represents one stone that can be removed
- The total number of successful unions equals the maximum removable stones
The beauty of this approach is that we don't need to simulate the actual removal process - we just need to count how many stones can theoretically be removed based on the connectivity structure.
Learn more about Depth-First Search, Union Find and Graph patterns.
Solution Approach
The solution uses the Union-Find (Disjoint Set Union) data structure to efficiently track and merge connected components of stones.
Union-Find Implementation:
The UnionFind
class maintains two arrays:
p
: Parent array wherep[x]
stores the parent of nodex
size
: Size array to track the size of each component for optimization
The find(x)
method locates the root of the component containing x
, using path compression to optimize future lookups:
if self.p[x] != x: self.p[x] = self.find(self.p[x]) # Path compression return self.p[x]
The union(a, b)
method merges two components and returns True
if they were previously separate (successful union) or False
if already connected:
pa, pb = self.find(a), self.find(b) if pa == pb: return False # Already in same component
It uses union by size optimization - the smaller component is attached to the larger one to keep the tree balanced.
Main Algorithm:
-
Initialize a Union-Find structure with
n
nodes (one for each stone) -
Use a nested loop to check all pairs of stones:
for i, (x1, y1) in enumerate(stones): for j, (x2, y2) in enumerate(stones[:i]):
-
For each pair, check if they share a row (
x1 == x2
) or column (y1 == y2
) -
If stones share a row or column, attempt to union them:
if x1 == x2 or y1 == y2: ans += uf.union(i, j)
-
Count successful unions - each successful union represents one stone that can be removed
The time complexity is O(nΒ² Γ Ξ±(n))
where Ξ±
is the inverse Ackermann function (practically constant), and space complexity is O(n)
for the Union-Find structure.
The elegance of this approach is that we don't need to simulate the removal process. Each successful union operation directly corresponds to a removable stone, giving us the final answer.
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 concrete example with stones at positions: [[0,0], [0,2], [1,1], [2,0], [2,2]]
Step 1: Initialize Union-Find
- Create a Union-Find structure with 5 nodes (indices 0-4)
- Initially, each stone is its own component:
p = [0, 1, 2, 3, 4]
Step 2: Check all pairs of stones
Examining stone pairs:
- Stones 0
[0,0]
and 1[0,2]
: Share row 0 β Union(0,1) succeeds β Components: {0,1}, {2}, {3}, {4} - Stones 0
[0,0]
and 2[1,1]
: Different row and column β No union - Stones 0
[0,0]
and 3[2,0]
: Share column 0 β Union(0,3) succeeds β Components: {0,1,3}, {2}, {4} - Stones 0
[0,0]
and 4[2,2]
: Different row and column β No union - Stones 1
[0,2]
and 2[1,1]
: Different row and column β No union - Stones 1
[0,2]
and 3[2,0]
: Already connected through stone 0 β Union returns False - Stones 1
[0,2]
and 4[2,2]
: Share column 2 β Union(1,4) succeeds β Components: {0,1,3,4}, {2} - Stones 2
[1,1]
and 3[2,0]
: Different row and column β No union - Stones 2
[1,1]
and 4[2,2]
: Different row and column β No union - Stones 3
[2,0]
and 4[2,2]
: Share row 2, but already connected β Union returns False
Step 3: Count successful unions
- Total successful unions: 3
- This means we can remove 3 stones
Verification:
- We have 2 connected components:
- Component 1: {0,1,3,4} - stones connected through shared rows/columns
- Component 2: {2} - isolated stone at
[1,1]
- From 5 stones with 2 components: max removable = 5 - 2 = 3 β
The stones could be removed in this order:
- Remove stone 1
[0,2]
(shares row with 0, column with 4) - Remove stone 3
[2,0]
(shares column with 0, row with 4) - Remove stone 4
[2,2]
(shares row with remaining stone 0) - Remaining stones: 0
[0,0]
and 2[1,1]
cannot be removed
Solution Implementation
1class UnionFind:
2 """
3 Union-Find (Disjoint Set Union) data structure with path compression and union by size.
4 """
5
6 def __init__(self, n: int) -> None:
7 """
8 Initialize the Union-Find structure with n elements.
9
10 Args:
11 n: Number of elements (0 to n-1)
12 """
13 # Each element is initially its own parent
14 self.parent = list(range(n))
15 # Track the size of each component for union by size optimization
16 self.size = [1] * n
17
18 def find(self, x: int) -> int:
19 """
20 Find the root/representative of the component containing element x.
21 Uses path compression to optimize future lookups.
22
23 Args:
24 x: The element to find the root of
25
26 Returns:
27 The root element of the component containing x
28 """
29 if self.parent[x] != x:
30 # Path compression: make x point directly to the root
31 self.parent[x] = self.find(self.parent[x])
32 return self.parent[x]
33
34 def union(self, a: int, b: int) -> bool:
35 """
36 Unite the components containing elements a and b.
37 Uses union by size to keep the tree balanced.
38
39 Args:
40 a: First element
41 b: Second element
42
43 Returns:
44 True if the elements were in different components (union performed),
45 False if they were already in the same component
46 """
47 # Find the roots of both elements
48 root_a = self.find(a)
49 root_b = self.find(b)
50
51 # Already in the same component
52 if root_a == root_b:
53 return False
54
55 # Union by size: attach smaller tree to larger tree
56 if self.size[root_a] > self.size[root_b]:
57 self.parent[root_b] = root_a
58 self.size[root_a] += self.size[root_b]
59 else:
60 self.parent[root_a] = root_b
61 self.size[root_b] += self.size[root_a]
62
63 return True
64
65
66class Solution:
67 def removeStones(self, stones: List[List[int]]) -> int:
68 """
69 Calculate the maximum number of stones that can be removed.
70 Stones on the same row or column form a connected component.
71 The maximum removable stones equals total stones minus number of components.
72
73 Args:
74 stones: List of stone positions [x, y]
75
76 Returns:
77 Maximum number of stones that can be removed
78 """
79 # Initialize Union-Find with one element per stone
80 union_find = UnionFind(len(stones))
81
82 # Counter for successful unions (edges in the graph)
83 successful_unions = 0
84
85 # Connect stones that share the same row or column
86 for i, (x1, y1) in enumerate(stones):
87 # Only check previously processed stones to avoid duplicate comparisons
88 for j, (x2, y2) in enumerate(stones[:i]):
89 # If stones share the same row (x) or column (y), connect them
90 if x1 == x2 or y1 == y2:
91 # Union returns True if connection was made (stones were in different components)
92 successful_unions += union_find.union(i, j)
93
94 # Number of removable stones equals the number of edges/connections made
95 return successful_unions
96
1/**
2 * Union-Find (Disjoint Set Union) data structure with path compression and union by size.
3 * Used to efficiently manage and merge disjoint sets.
4 */
5class UnionFind {
6 // Parent array where parent[i] represents the parent of node i
7 private final int[] parent;
8 // Size array where size[i] represents the size of the set rooted at i
9 private final int[] size;
10
11 /**
12 * Initialize Union-Find structure with n elements (0 to n-1).
13 * Initially, each element is its own parent (separate set).
14 *
15 * @param n the number of elements
16 */
17 public UnionFind(int n) {
18 parent = new int[n];
19 size = new int[n];
20
21 // Initialize each element as its own parent with size 1
22 for (int i = 0; i < n; ++i) {
23 parent[i] = i;
24 size[i] = 1;
25 }
26 }
27
28 /**
29 * Find the root/representative of the set containing element x.
30 * Implements path compression for optimization.
31 *
32 * @param x the element to find the root of
33 * @return the root of the set containing x
34 */
35 public int find(int x) {
36 // Path compression: make every node point directly to the root
37 if (parent[x] != x) {
38 parent[x] = find(parent[x]);
39 }
40 return parent[x];
41 }
42
43 /**
44 * Union two sets containing elements a and b.
45 * Uses union by size to keep the tree balanced.
46 *
47 * @param a first element
48 * @param b second element
49 * @return true if union was performed (elements were in different sets), false otherwise
50 */
51 public boolean union(int a, int b) {
52 // Find roots of both elements
53 int rootA = find(a);
54 int rootB = find(b);
55
56 // Already in the same set
57 if (rootA == rootB) {
58 return false;
59 }
60
61 // Union by size: attach smaller tree to larger tree
62 if (size[rootA] > size[rootB]) {
63 parent[rootB] = rootA;
64 size[rootA] += size[rootB];
65 } else {
66 parent[rootA] = rootB;
67 size[rootB] += size[rootA];
68 }
69
70 return true;
71 }
72}
73
74/**
75 * Solution for removing stones problem.
76 * Stones on the same row or column can be connected and form a group.
77 * The maximum number of stones that can be removed equals total stones minus number of groups.
78 */
79class Solution {
80 /**
81 * Calculate the maximum number of stones that can be removed.
82 * Two stones can be in the same group if they share the same row or column.
83 *
84 * @param stones 2D array where stones[i] = [row_i, col_i] represents position of i-th stone
85 * @return maximum number of stones that can be removed
86 */
87 public int removeStones(int[][] stones) {
88 int n = stones.length;
89 UnionFind unionFind = new UnionFind(n);
90 int removedStones = 0;
91
92 // Connect stones that share the same row or column
93 for (int i = 0; i < n; ++i) {
94 for (int j = 0; j < i; ++j) {
95 // Check if stones share the same row or column
96 if (stones[i][0] == stones[j][0] || stones[i][1] == stones[j][1]) {
97 // If union is successful (stones were in different groups), increment count
98 removedStones += unionFind.union(i, j) ? 1 : 0;
99 }
100 }
101 }
102
103 return removedStones;
104 }
105}
106
1class UnionFind {
2public:
3 // Constructor: Initialize union-find structure with n elements
4 UnionFind(int n) {
5 parent = vector<int>(n);
6 rank = vector<int>(n, 1);
7 // Initialize each element as its own parent (self-loop)
8 iota(parent.begin(), parent.end(), 0);
9 }
10
11 // Unite two sets containing elements a and b
12 // Returns true if they were in different sets (union performed)
13 // Returns false if they were already in the same set
14 bool unite(int a, int b) {
15 int rootA = find(a);
16 int rootB = find(b);
17
18 // Already in the same set
19 if (rootA == rootB) {
20 return false;
21 }
22
23 // Union by rank: attach smaller tree under root of larger tree
24 if (rank[rootA] > rank[rootB]) {
25 parent[rootB] = rootA;
26 rank[rootA] += rank[rootB];
27 } else {
28 parent[rootA] = rootB;
29 rank[rootB] += rank[rootA];
30 }
31
32 return true;
33 }
34
35 // Find the root of the set containing element x
36 // Uses path compression for optimization
37 int find(int x) {
38 if (parent[x] != x) {
39 // Path compression: make every node point directly to root
40 parent[x] = find(parent[x]);
41 }
42 return parent[x];
43 }
44
45private:
46 vector<int> parent; // Parent array for union-find
47 vector<int> rank; // Rank/size array for union by rank optimization
48};
49
50class Solution {
51public:
52 // Remove maximum number of stones that share same row or column
53 // Returns the maximum number of stones that can be removed
54 int removeStones(vector<vector<int>>& stones) {
55 int n = stones.size();
56 UnionFind uf(n);
57 int removedCount = 0;
58
59 // Check all pairs of stones
60 for (int i = 0; i < n; ++i) {
61 for (int j = 0; j < i; ++j) {
62 // Unite stones if they share the same row or column
63 if (stones[i][0] == stones[j][0] || stones[i][1] == stones[j][1]) {
64 // If unite succeeds, one more stone can be removed
65 removedCount += uf.unite(i, j);
66 }
67 }
68 }
69
70 return removedCount;
71 }
72};
73
1// Parent array for Union-Find structure - stores the parent of each element
2let parent: number[];
3// Size array for Union-Find structure - stores the size of each component
4let componentSize: number[];
5
6/**
7 * Initializes the Union-Find data structure
8 * @param n - Number of elements to initialize
9 */
10function initializeUnionFind(n: number): void {
11 // Initialize each element as its own parent (self-loop)
12 parent = Array.from({ length: n }, (_, index) => index);
13 // Initialize each component with size 1
14 componentSize = Array(n).fill(1);
15}
16
17/**
18 * Finds the root parent of an element with path compression
19 * @param x - Element to find the root parent for
20 * @returns The root parent of element x
21 */
22function find(x: number): number {
23 // Path compression: if x is not its own parent, recursively find and update
24 if (parent[x] !== x) {
25 parent[x] = find(parent[x]);
26 }
27 return parent[x];
28}
29
30/**
31 * Unions two elements by connecting their root parents
32 * Uses union by size optimization
33 * @param a - First element to union
34 * @param b - Second element to union
35 * @returns true if union was performed, false if elements were already in same component
36 */
37function union(a: number, b: number): boolean {
38 // Find root parents of both elements
39 const rootA = find(a);
40 const rootB = find(b);
41
42 // If already in same component, no union needed
43 if (rootA === rootB) {
44 return false;
45 }
46
47 // Union by size: attach smaller tree under larger tree
48 if (componentSize[rootA] > componentSize[rootB]) {
49 parent[rootB] = rootA;
50 componentSize[rootA] += componentSize[rootB];
51 } else {
52 parent[rootA] = rootB;
53 componentSize[rootB] += componentSize[rootA];
54 }
55
56 return true;
57}
58
59/**
60 * Removes the maximum number of stones from a 2D plane
61 * Stones on same row or column can be removed if connected
62 * @param stones - Array of stone positions [x, y]
63 * @returns Maximum number of stones that can be removed
64 */
65function removeStones(stones: number[][]): number {
66 const numberOfStones = stones.length;
67
68 // Initialize Union-Find structure for all stones
69 initializeUnionFind(numberOfStones);
70
71 // Counter for successful unions (stones that can be removed)
72 let removableStones = 0;
73
74 // Check all pairs of stones
75 for (let i = 0; i < numberOfStones; ++i) {
76 for (let j = 0; j < i; ++j) {
77 // If stones share same row or column, they can be connected
78 if (stones[i][0] === stones[j][0] || stones[i][1] === stones[j][1]) {
79 // If union is successful, increment removable stones count
80 removableStones += union(i, j) ? 1 : 0;
81 }
82 }
83 }
84
85 return removableStones;
86}
87
Time and Space Complexity
Time Complexity: O(nΒ² Γ Ξ±(n))
The code uses a nested loop structure where:
- The outer loop iterates through all
n
stones withenumerate(stones)
- The inner loop iterates through all previous stones with
enumerate(stones[:i])
, which in the worst case iterates throughi-1
stones - This creates a total of
1 + 2 + 3 + ... + (n-1) = n(n-1)/2 = O(nΒ²)
iterations
Within each iteration, the code performs:
- A comparison check
(x1 == x2 or y1 == y2)
which isO(1)
- A
union
operation which internally callsfind
twice
The find
operation uses path compression and has an amortized time complexity of O(Ξ±(n))
, where Ξ±(n)
is the inverse Ackermann function. The union
operation calls find
twice and performs constant-time operations, so it's also O(Ξ±(n))
.
Therefore, the overall time complexity is O(nΒ²) Γ O(Ξ±(n)) = O(nΒ² Γ Ξ±(n))
.
Space Complexity: O(n)
The space usage consists of:
- The UnionFind data structure stores two arrays:
self.p
andself.size
, each of sizen
- The recursive call stack for
find
operation can go up toO(n)
depth in the worst case before path compression - Other variables (
ans
, loop indices) useO(1)
space
The dominant space usage is O(n)
for the UnionFind arrays, giving an overall space complexity of O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Coordinate Mapping Confusion
A common mistake is trying to directly use coordinates as node indices in the Union-Find structure. Since coordinates can be large (up to 10^4), developers might attempt:
# WRONG: This could create a massive Union-Find structure uf = UnionFind(10001 * 2) # Trying to accommodate all possible x and y values for x, y in stones: uf.union(x, y + 10001) # Mapping y coordinates to avoid collision
Solution: Use stone indices (0 to n-1) as nodes in Union-Find, not the coordinates themselves. Each stone gets a unique index, and we connect indices of stones that share rows/columns.
2. Inefficient Component Counting
After building the Union-Find structure, developers often incorrectly count components:
# WRONG: This counts all unique parents, including non-root nodes
components = len(set(uf.parent))
# Also WRONG: Iterating through all possible coordinates
components = 0
for i in range(len(stones)):
if uf.find(i) == i:
components += 1
Solution: Count successful unions directly. Each successful union reduces the number of components by 1, so removable_stones = successful_unions = n - components
.
3. Duplicate Pair Checking
Processing all pairs twice leads to incorrect counting:
# WRONG: This checks each pair twice (i,j) and (j,i)
for i in range(len(stones)):
for j in range(len(stones)):
if i != j and (stones[i][0] == stones[j][0] or stones[i][1] == stones[j][1]):
result += uf.union(i, j)
Solution: Use triangular iteration to check each pair only once:
for i in range(len(stones)):
for j in range(i): # Only check previous stones
# ... union logic
4. Missing Path Compression
Implementing Union-Find without path compression severely degrades performance:
# WRONG: No path compression
def find(self, x):
while self.parent[x] != x:
x = self.parent[x]
return x
Solution: Always implement path compression to maintain near-constant time complexity:
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # Compress path
return self.parent[x]
5. Incorrect Answer Calculation
A subtle but critical error is returning the number of components instead of removable stones:
# WRONG: Returns number of components
components = len(set(uf.find(i) for i in range(len(stones))))
return components
# WRONG: Off by one error
return len(stones) - components - 1
Solution: The answer is exactly the number of successful unions, or equivalently total_stones - number_of_components
. Track successful unions directly for clarity.
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
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!