Facebook Pixel

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.

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

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:

  1. We can efficiently merge stones that share rows/columns into the same component
  2. Each successful union operation represents one stone that can be removed
  3. 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 where p[x] stores the parent of node x
  • 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:

  1. Initialize a Union-Find structure with n nodes (one for each stone)

  2. 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]):
  3. For each pair, check if they share a row (x1 == x2) or column (y1 == y2)

  4. If stones share a row or column, attempt to union them:

    if x1 == x2 or y1 == y2:
        ans += uf.union(i, j)
  5. 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 Evaluator

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

  1. Remove stone 1 [0,2] (shares row with 0, column with 4)
  2. Remove stone 3 [2,0] (shares column with 0, row with 4)
  3. Remove stone 4 [2,2] (shares row with remaining stone 0)
  4. 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 with enumerate(stones)
  • The inner loop iterates through all previous stones with enumerate(stones[:i]), which in the worst case iterates through i-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 is O(1)
  • A union operation which internally calls find 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 and self.size, each of size n
  • The recursive call stack for find operation can go up to O(n) depth in the worst case before path compression
  • Other variables (ans, loop indices) use O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

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

Load More