Facebook Pixel

1632. Rank Transform of a Matrix

Problem Description

You are given an m x n matrix of integers. Your task is to create a new matrix where each element is replaced by its rank.

The rank of an element follows these rules:

  1. Ranks start from 1 - The smallest rank assigned is 1, not 0.

  2. Same row/column ordering must be preserved - For any two elements p and q that are in the same row OR the same column:

    • If p < q, then rank(p) < rank(q)
    • If p == q, then rank(p) == rank(q)
    • If p > q, then rank(p) > rank(q)
  3. Ranks should be as small as possible - While maintaining the ordering constraints, use the smallest possible rank values.

For example, if you have a matrix:

[[1, 2],
 [3, 4]]

The rank matrix would be:

[[1, 2],
 [2, 3]]

Here's why:

  • 1 gets rank 1 (smallest element)
  • 2 must have rank > 1 (same row as 1, and 2 > 1), so rank 2
  • 3 must have rank > 1 (same column as 1, and 3 > 1), so rank 2
  • 4 must have rank > 2 (same row as 3 which has rank 2, and 4 > 3), and rank > 2 (same column as 2 which has rank 2, and 4 > 2), so rank 3

The key challenge is handling equal values - they must receive the same rank even if they appear in different positions, as long as they can "see" each other through shared rows or columns. This creates connected components of cells that must all have the same rank.

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

Intuition

The key insight is to process elements by their values in ascending order. This ensures that when we assign a rank to an element, all smaller elements have already been assigned their ranks, making it easy to maintain the ordering constraint.

Consider what happens when we process all cells with the same value. These cells might be connected through shared rows or columns. For instance, if cells (0,1) and (0,2) have the same value, they're in the same row. If cell (1,2) also has the same value, then all three cells form a connected group - (0,1) and (0,2) share row 0, while (0,2) and (1,2) share column 2. All cells in such a connected group must receive the same rank.

This naturally leads us to think about Union-Find (Disjoint Set Union). We can treat each row and column as a node, and when we encounter a cell at position (i,j), we union row i with column j. All cells that end up in the same connected component must have the same rank.

To determine what rank to assign to a connected component, we need to look at the maximum rank already assigned in any row or column that the component touches. Why? Because our new rank must be greater than all previously assigned ranks in those rows and columns (since we're processing in ascending order of values).

Here's the step-by-step thought process:

  1. Group cells by their values
  2. Process groups in ascending order of values
  3. For each group, use Union-Find to identify connected components (cells that share rows/columns)
  4. For each component, find the maximum existing rank in any affected row or column
  5. Assign max_existing_rank + 1 to all cells in the component
  6. Update the row and column maximum ranks for future iterations
  7. Reset the Union-Find structure for the next group

The Union-Find structure elegantly handles the transitive connections - if cell A shares a row with cell B, and cell B shares a column with cell C, then all three must have the same rank even if A and C don't directly share a row or column.

Learn more about Union Find, Graph, Topological Sort and Sorting patterns.

Solution Approach

The implementation uses a Union-Find data structure and processes matrix elements grouped by their values in ascending order.

Step 1: Group cells by values

d = defaultdict(list)
for i, row in enumerate(matrix):
    for j, v in enumerate(row):
        d[v].append((i, j))

We create a dictionary where keys are cell values and values are lists of (row, column) positions. This allows us to process all cells with the same value together.

Step 2: Initialize tracking arrays

row_max = [0] * m  # Track maximum rank in each row
col_max = [0] * n  # Track maximum rank in each column
ans = [[0] * n for _ in range(m)]  # Result matrix

Step 3: Create Union-Find structure

uf = UnionFind(m + n)

We create a Union-Find with m + n nodes. Rows are represented by indices [0, m-1] and columns by indices [m, m+n-1]. This clever indexing allows us to union rows and columns in the same structure.

Step 4: Process values in ascending order

for v in sorted(d):

By processing values from smallest to largest, we ensure that when assigning ranks to cells with value v, all cells with smaller values already have their ranks assigned.

Step 5: Union cells with the same value

for i, j in d[v]:
    uf.union(i, j + m)

For each cell at position (i, j), we union row i with column j + m. This creates connected components where all cells that share rows or columns (directly or transitively) are grouped together.

Step 6: Calculate rank for each component

rank = defaultdict(int)
for i, j in d[v]:
    rank[uf.find(i)] = max(rank[uf.find(i)], row_max[i], col_max[j])

For each connected component (identified by its root in Union-Find), we find the maximum rank already present in any row or column that the component touches. The new rank will be this maximum plus 1.

Step 7: Assign ranks and update maximums

for i, j in d[v]:
    ans[i][j] = row_max[i] = col_max[j] = 1 + rank[uf.find(i)]

All cells in the same connected component get the same rank: 1 + maximum_existing_rank. We simultaneously update the row_max and col_max arrays for future iterations.

Step 8: Reset Union-Find for next value group

for i, j in d[v]:
    uf.reset(i)
    uf.reset(j + m)

After processing all cells with value v, we reset the Union-Find structure so it can be reused for the next value group. This is more efficient than creating a new Union-Find for each value.

Union-Find Implementation Details:

  • find(x): Uses path compression to find the root of element x
  • union(a, b): Merges two sets using union by size to keep the tree balanced
  • reset(x): Resets element x to be its own parent (standalone set)

The time complexity is O(mn * log(mn) * Ξ±(mn)) where Ξ± is the inverse Ackermann function (practically constant). The sorting dominates the complexity.

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 a small example to illustrate the solution approach:

Given matrix:

[[7, 3, 6],
 [1, 4, 5],
 [9, 8, 2]]

Step 1: Group cells by values

1: [(1,0)]
2: [(2,2)]
3: [(0,1)]
4: [(1,1)]
5: [(1,2)]
6: [(0,2)]
7: [(0,0)]
8: [(2,1)]
9: [(2,0)]

Step 2: Initialize tracking arrays

row_max = [0, 0, 0]  # Maximum rank in each row
col_max = [0, 0, 0]  # Maximum rank in each column
ans = [[0, 0, 0],
       [0, 0, 0],
       [0, 0, 0]]

Step 3: Process value 1 at position (1,0)

  • Union row 1 with column 0 (union(1, 0+3=3))
  • Component contains only this cell
  • Max existing rank in row 1: 0, column 0: 0
  • Assign rank: 1 + max(0, 0) = 1
  • Update: ans[1][0] = 1, row_max[1] = 1, col_max[0] = 1
  • Reset Union-Find

Step 4: Process value 2 at position (2,2)

  • Union row 2 with column 2 (union(2, 2+3=5))
  • Max existing rank in row 2: 0, column 2: 0
  • Assign rank: 1 + max(0, 0) = 1
  • Update: ans[2][2] = 1, row_max[2] = 1, col_max[2] = 1

Step 5: Process value 3 at position (0,1)

  • Union row 0 with column 1 (union(0, 1+3=4))
  • Max existing rank in row 0: 0, column 1: 0
  • Assign rank: 1 + max(0, 0) = 1
  • Update: ans[0][1] = 1, row_max[0] = 1, col_max[1] = 1

Step 6: Process value 4 at position (1,1)

  • Union row 1 with column 1 (union(1, 1+3=4))
  • Max existing rank in row 1: 1 (from value 1), column 1: 1 (from value 3)
  • Assign rank: 1 + max(1, 1) = 2
  • Update: ans[1][1] = 2, row_max[1] = 2, col_max[1] = 2

Step 7: Process value 5 at position (1,2)

  • Union row 1 with column 2 (union(1, 2+3=5))
  • Max existing rank in row 1: 2 (from value 4), column 2: 1 (from value 2)
  • Assign rank: 1 + max(2, 1) = 3
  • Update: ans[1][2] = 3, row_max[1] = 3, col_max[2] = 3

Step 8: Process value 6 at position (0,2)

  • Union row 0 with column 2 (union(0, 2+3=5))
  • Max existing rank in row 0: 1 (from value 3), column 2: 3 (from value 5)
  • Assign rank: 1 + max(1, 3) = 4
  • Update: ans[0][2] = 4, row_max[0] = 4, col_max[2] = 4

Step 9: Process value 7 at position (0,0)

  • Union row 0 with column 0 (union(0, 0+3=3))
  • Max existing rank in row 0: 4 (from value 6), column 0: 1 (from value 1)
  • Assign rank: 1 + max(4, 1) = 5
  • Update: ans[0][0] = 5, row_max[0] = 5, col_max[0] = 5

Step 10: Process value 8 at position (2,1)

  • Union row 2 with column 1 (union(2, 1+3=4))
  • Max existing rank in row 2: 1 (from value 2), column 1: 2 (from value 4)
  • Assign rank: 1 + max(1, 2) = 3
  • Update: ans[2][1] = 3, row_max[2] = 3, col_max[1] = 3

Step 11: Process value 9 at position (2,0)

  • Union row 2 with column 0 (union(2, 0+3=3))
  • Max existing rank in row 2: 3 (from value 8), column 0: 5 (from value 7)
  • Assign rank: 1 + max(3, 5) = 6
  • Update: ans[2][0] = 6, row_max[2] = 6, col_max[0] = 6

Final Result:

[[5, 1, 4],
 [1, 2, 3],
 [6, 3, 1]]

The algorithm ensures that:

  • Each element's rank respects the ordering in its row and column
  • Equal values that can "see" each other get the same rank
  • Ranks are minimized while maintaining constraints

Solution Implementation

1from typing import List
2from collections import defaultdict
3
4
5class UnionFind:
6    """Union-Find (Disjoint Set Union) data structure with path compression and union by size."""
7  
8    def __init__(self, n: int) -> None:
9        """Initialize Union-Find structure with n elements."""
10        self.parent = list(range(n))  # Each element is its own parent initially
11        self.size = [1] * n  # Size of each component is 1 initially
12  
13    def find(self, x: int) -> int:
14        """Find the root of element x with path compression."""
15        if self.parent[x] != x:
16            # Path compression: make all nodes point directly to root
17            self.parent[x] = self.find(self.parent[x])
18        return self.parent[x]
19  
20    def union(self, a: int, b: int) -> None:
21        """Union two elements a and b by size (attach smaller tree to larger)."""
22        root_a, root_b = self.find(a), self.find(b)
23      
24        if root_a != root_b:
25            # Union by size: attach smaller component to larger one
26            if self.size[root_a] > self.size[root_b]:
27                self.parent[root_b] = root_a
28                self.size[root_a] += self.size[root_b]
29            else:
30                self.parent[root_a] = root_b
31                self.size[root_b] += self.size[root_a]
32  
33    def reset(self, x: int) -> None:
34        """Reset element x to be its own parent with size 1."""
35        self.parent[x] = x
36        self.size[x] = 1
37
38
39class Solution:
40    def matrixRankTransform(self, matrix: List[List[int]]) -> List[List[int]]:
41        """
42        Transform matrix so that each element's rank is the smallest possible positive integer
43        respecting the constraints that larger values get larger ranks in the same row/column.
44        """
45        m, n = len(matrix), len(matrix[0])
46      
47        # Group all positions by their values
48        value_to_positions = defaultdict(list)
49        for i, row in enumerate(matrix):
50            for j, value in enumerate(row):
51                value_to_positions[value].append((i, j))
52      
53        # Track maximum rank achieved in each row and column
54        row_max_rank = [0] * m
55        col_max_rank = [0] * n
56      
57        # Initialize result matrix
58        result = [[0] * n for _ in range(m)]
59      
60        # Union-Find to handle rows and columns (rows: 0 to m-1, columns: m to m+n-1)
61        uf = UnionFind(m + n)
62      
63        # Process values in ascending order
64        for value in sorted(value_to_positions.keys()):
65            # Dictionary to store the maximum rank for each connected component
66            component_max_rank = defaultdict(int)
67          
68            # Union all positions with the same value that share row or column
69            for row, col in value_to_positions[value]:
70                # Connect row and column (column index offset by m)
71                uf.union(row, col + m)
72          
73            # Find maximum existing rank for each connected component
74            for row, col in value_to_positions[value]:
75                root = uf.find(row)
76                # Component's rank should be at least 1 more than max of row/col
77                component_max_rank[root] = max(
78                    component_max_rank[root], 
79                    row_max_rank[row], 
80                    col_max_rank[col]
81                )
82          
83            # Assign ranks to all positions with current value
84            for row, col in value_to_positions[value]:
85                root = uf.find(row)
86                new_rank = 1 + component_max_rank[root]
87                result[row][col] = new_rank
88                row_max_rank[row] = new_rank
89                col_max_rank[col] = new_rank
90          
91            # Reset Union-Find for next value group
92            for row, col in value_to_positions[value]:
93                uf.reset(row)
94                uf.reset(col + m)
95      
96        return result
97
1/**
2 * Union-Find (Disjoint Set Union) data structure with path compression and union by size
3 */
4class UnionFind {
5    private int[] parent;      // parent[i] stores the parent of node i
6    private int[] size;         // size[i] stores the size of the tree rooted at i
7  
8    /**
9     * Initialize Union-Find structure with n elements
10     * @param n number of elements
11     */
12    public UnionFind(int n) {
13        parent = new int[n];
14        size = new int[n];
15        for (int i = 0; i < n; i++) {
16            parent[i] = i;      // Initially, each element is its own parent
17            size[i] = 1;        // Initially, each set has size 1
18        }
19    }
20  
21    /**
22     * Find the root of the set containing element x with path compression
23     * @param x the element to find
24     * @return the root of the set containing x
25     */
26    public int find(int x) {
27        if (parent[x] != x) {
28            // Path compression: make every node point directly to root
29            parent[x] = find(parent[x]);
30        }
31        return parent[x];
32    }
33  
34    /**
35     * Union two sets containing elements a and b using union by size
36     * @param a first element
37     * @param b second element
38     */
39    public void union(int a, int b) {
40        int rootA = find(a);
41        int rootB = find(b);
42      
43        if (rootA != rootB) {
44            // Union by size: attach smaller tree under larger tree
45            if (size[rootA] > size[rootB]) {
46                parent[rootB] = rootA;
47                size[rootA] += size[rootB];
48            } else {
49                parent[rootA] = rootB;
50                size[rootB] += size[rootA];
51            }
52        }
53    }
54  
55    /**
56     * Reset element x to be its own parent (disconnect from current set)
57     * @param x the element to reset
58     */
59    public void reset(int x) {
60        parent[x] = x;
61        size[x] = 1;
62    }
63}
64
65/**
66 * Solution for Matrix Rank Transform problem
67 * Assigns ranks to matrix elements where smallest elements get rank 1,
68 * and equal elements in same row/column must have same rank
69 */
70class Solution {
71    public int[][] matrixRankTransform(int[][] matrix) {
72        int rows = matrix.length;
73        int cols = matrix[0].length;
74      
75        // Group cells by their values using TreeMap to process in ascending order
76        TreeMap<Integer, List<int[]>> valueToPositions = new TreeMap<>();
77        for (int i = 0; i < rows; i++) {
78            for (int j = 0; j < cols; j++) {
79                valueToPositions.computeIfAbsent(matrix[i][j], k -> new ArrayList<>())
80                               .add(new int[] {i, j});
81            }
82        }
83      
84        // Track maximum rank achieved in each row and column
85        int[] maxRankInRow = new int[rows];
86        int[] maxRankInCol = new int[cols];
87      
88        // Result matrix to store ranks
89        int[][] result = new int[rows][cols];
90      
91        // Union-Find to group cells with same value in same row/column
92        // Uses indices 0 to rows-1 for rows, and rows to rows+cols-1 for columns
93        UnionFind unionFind = new UnionFind(rows + cols);
94        int[] componentRank = new int[rows + cols];
95      
96        // Process each value group in ascending order
97        for (List<int[]> positions : valueToPositions.values()) {
98            // Union all cells with same value that share row or column
99            for (int[] position : positions) {
100                int row = position[0];
101                int col = position[1];
102                // Connect row index with column index (offset by rows)
103                unionFind.union(row, col + rows);
104            }
105          
106            // Find maximum rank for each connected component
107            for (int[] position : positions) {
108                int row = position[0];
109                int col = position[1];
110                int root = unionFind.find(row);
111                // Update component's rank to maximum of current ranks in its rows/columns
112                componentRank[root] = Math.max(componentRank[root], 
113                                              Math.max(maxRankInRow[row], maxRankInCol[col]));
114            }
115          
116            // Assign ranks to all cells in this value group
117            for (int[] position : positions) {
118                int row = position[0];
119                int col = position[1];
120                // New rank is 1 plus the maximum rank in the component
121                result[row][col] = 1 + componentRank[unionFind.find(row)];
122                // Update maximum ranks for this row and column
123                maxRankInRow[row] = result[row][col];
124                maxRankInCol[col] = result[row][col];
125            }
126          
127            // Reset Union-Find for next value group
128            for (int[] position : positions) {
129                unionFind.reset(position[0]);
130                unionFind.reset(position[1] + rows);
131            }
132        }
133      
134        return result;
135    }
136}
137
1class UnionFind {
2public:
3    // Initialize Union-Find structure with n elements
4    UnionFind(int n) {
5        parent = vector<int>(n);
6        setSize = 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    void unite(int a, int b) {
13        int rootA = find(a);
14        int rootB = find(b);
15      
16        if (rootA != rootB) {
17            // Union by size: attach smaller tree under larger tree
18            if (setSize[rootA] > setSize[rootB]) {
19                parent[rootB] = rootA;
20                setSize[rootA] += setSize[rootB];
21            } else {
22                parent[rootA] = rootB;
23                setSize[rootB] += setSize[rootA];
24            }
25        }
26    }
27
28    // Find root of element x with path compression
29    int find(int x) {
30        if (parent[x] != x) {
31            // Path compression: make all nodes point directly to root
32            parent[x] = find(parent[x]);
33        }
34        return parent[x];
35    }
36
37    // Reset element x to be its own parent (disconnect from set)
38    void reset(int x) {
39        parent[x] = x;
40        setSize[x] = 1;
41    }
42
43private:
44    vector<int> parent;   // Parent array for union-find
45    vector<int> setSize;  // Size of each set for union by size
46};
47
48class Solution {
49public:
50    vector<vector<int>> matrixRankTransform(vector<vector<int>>& matrix) {
51        int numRows = matrix.size();
52        int numCols = matrix[0].size();
53      
54        // Group all positions by their values
55        map<int, vector<pair<int, int>>> valueToPositions;
56        for (int row = 0; row < numRows; ++row) {
57            for (int col = 0; col < numCols; ++col) {
58                valueToPositions[matrix[row][col]].push_back({row, col});
59            }
60        }
61      
62        // Track maximum rank achieved in each row and column
63        vector<int> maxRankInRow(numRows);
64        vector<int> maxRankInCol(numCols);
65      
66        // Result matrix to store ranks
67        vector<vector<int>> result(numRows, vector<int>(numCols));
68      
69        // Union-Find to connect rows and columns
70        // Rows: [0, numRows-1], Columns: [numRows, numRows+numCols-1]
71        UnionFind unionFind(numRows + numCols);
72        vector<int> componentRank(numRows + numCols);
73      
74        // Process values in ascending order
75        for (auto& [value, positions] : valueToPositions) {
76            // Connect all positions with same value that share row or column
77            for (auto& [row, col] : positions) {
78                // Unite row index with column index (shifted by numRows)
79                unionFind.unite(row, col + numRows);
80            }
81          
82            // Find maximum rank for each connected component
83            for (auto& [row, col] : positions) {
84                int root = unionFind.find(row);
85                // Component rank is max of current component rank, row max, and column max
86                componentRank[root] = max({componentRank[root], 
87                                          maxRankInRow[row], 
88                                          maxRankInCol[col]});
89            }
90          
91            // Assign ranks to all positions with current value
92            for (auto& [row, col] : positions) {
93                int newRank = 1 + componentRank[unionFind.find(row)];
94                result[row][col] = newRank;
95                maxRankInRow[row] = newRank;
96                maxRankInCol[col] = newRank;
97            }
98          
99            // Reset union-find for next value group
100            for (auto& [row, col] : positions) {
101                unionFind.reset(row);
102                unionFind.reset(col + numRows);
103            }
104        }
105      
106        return result;
107    }
108};
109
1// Parent array for union-find structure
2let parent: number[];
3// Size of each set for union by size optimization
4let setSize: number[];
5
6// Initialize Union-Find structure with n elements
7function initUnionFind(n: number): void {
8    parent = new Array(n);
9    setSize = new Array(n).fill(1);
10    // Initialize each element as its own parent (self-loop)
11    for (let i = 0; i < n; i++) {
12        parent[i] = i;
13    }
14}
15
16// Unite two sets containing elements a and b
17function unite(a: number, b: number): void {
18    const rootA = find(a);
19    const rootB = find(b);
20  
21    if (rootA !== rootB) {
22        // Union by size: attach smaller tree under larger tree
23        if (setSize[rootA] > setSize[rootB]) {
24            parent[rootB] = rootA;
25            setSize[rootA] += setSize[rootB];
26        } else {
27            parent[rootA] = rootB;
28            setSize[rootB] += setSize[rootA];
29        }
30    }
31}
32
33// Find root of element x with path compression
34function find(x: number): number {
35    if (parent[x] !== x) {
36        // Path compression: make all nodes point directly to root
37        parent[x] = find(parent[x]);
38    }
39    return parent[x];
40}
41
42// Reset element x to be its own parent (disconnect from set)
43function reset(x: number): void {
44    parent[x] = x;
45    setSize[x] = 1;
46}
47
48function matrixRankTransform(matrix: number[][]): number[][] {
49    const numRows = matrix.length;
50    const numCols = matrix[0].length;
51  
52    // Group all positions by their values
53    const valueToPositions = new Map<number, Array<[number, number]>>();
54    for (let row = 0; row < numRows; row++) {
55        for (let col = 0; col < numCols; col++) {
56            const value = matrix[row][col];
57            if (!valueToPositions.has(value)) {
58                valueToPositions.set(value, []);
59            }
60            valueToPositions.get(value)!.push([row, col]);
61        }
62    }
63  
64    // Sort values in ascending order
65    const sortedValues = Array.from(valueToPositions.keys()).sort((a, b) => a - b);
66  
67    // Track maximum rank achieved in each row and column
68    const maxRankInRow = new Array(numRows).fill(0);
69    const maxRankInCol = new Array(numCols).fill(0);
70  
71    // Result matrix to store ranks
72    const result: number[][] = Array(numRows).fill(null).map(() => Array(numCols).fill(0));
73  
74    // Initialize Union-Find to connect rows and columns
75    // Rows: [0, numRows-1], Columns: [numRows, numRows+numCols-1]
76    initUnionFind(numRows + numCols);
77    const componentRank = new Array(numRows + numCols).fill(0);
78  
79    // Process values in ascending order
80    for (const value of sortedValues) {
81        const positions = valueToPositions.get(value)!;
82      
83        // Connect all positions with same value that share row or column
84        for (const [row, col] of positions) {
85            // Unite row index with column index (shifted by numRows)
86            unite(row, col + numRows);
87        }
88      
89        // Find maximum rank for each connected component
90        for (const [row, col] of positions) {
91            const root = find(row);
92            // Component rank is max of current component rank, row max, and column max
93            componentRank[root] = Math.max(
94                componentRank[root],
95                maxRankInRow[row],
96                maxRankInCol[col]
97            );
98        }
99      
100        // Assign ranks to all positions with current value
101        for (const [row, col] of positions) {
102            const newRank = 1 + componentRank[find(row)];
103            result[row][col] = newRank;
104            maxRankInRow[row] = newRank;
105            maxRankInCol[col] = newRank;
106        }
107      
108        // Reset union-find for next value group
109        for (const [row, col] of positions) {
110            reset(row);
111            reset(col + numRows);
112            componentRank[find(row)] = 0;
113            componentRank[find(col + numRows)] = 0;
114        }
115    }
116  
117    return result;
118}
119

Time and Space Complexity

Time Complexity: O(mn * log(mn) + mn * Ξ±(m+n))

Breaking down the time complexity:

  • Creating the dictionary d by iterating through all matrix elements: O(mn)
  • Sorting the unique values in the dictionary: O(k * log(k)) where k is the number of unique values, worst case k = mn, so O(mn * log(mn))
  • For each unique value, we process all cells with that value:
    • Total cells processed across all values: O(mn)
    • For each cell, we perform Union-Find operations:
      • union(i, j+m): O(Ξ±(m+n)) per operation where Ξ± is the inverse Ackermann function
      • find(i): O(Ξ±(m+n)) per operation
      • reset(i) and reset(j+m): O(1) per operation
    • Total Union-Find operations: O(mn * Ξ±(m+n))

The dominant term is O(mn * log(mn)) from sorting, giving us overall time complexity of O(mn * log(mn) + mn * Ξ±(m+n)). Since Ξ±(m+n) grows extremely slowly and is practically constant for all reasonable inputs, this can be simplified to O(mn * log(mn)).

Space Complexity: O(mn)

Breaking down the space complexity:

  • Dictionary d storing all cell positions: O(mn)
  • UnionFind data structure with parent array and size array: O(m+n)
  • row_max array: O(m)
  • col_max array: O(n)
  • Answer matrix ans: O(mn)
  • rank dictionary in the worst case when all elements in a group belong to different components: O(m+n)

The dominant term is O(mn) from the dictionary and answer matrix, giving us overall space complexity of O(mn).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Incorrectly Handling Connected Components of Equal Values

The Problem: A common mistake is failing to recognize that equal values form transitive connections through shared rows and columns. For example, if cells A and B are in the same row with equal values, and cells B and C are in the same column with equal values, then A, B, and C must all have the same rank, even though A and C don't directly share a row or column.

Incorrect Approach:

# WRONG: Only considering direct row/column relationships
for row, col in value_to_positions[value]:
    new_rank = 1 + max(row_max_rank[row], col_max_rank[col])
    result[row][col] = new_rank
    row_max_rank[row] = new_rank
    col_max_rank[col] = new_rank

This fails on cases like:

[[7, 3, 6],
 [1, 4, 5],
 [9, 8, 2]]

Where multiple equal values might need to coordinate their ranks through transitive connections.

The Solution: Use Union-Find to group all cells with the same value that are connected through shared rows/columns, then assign the same rank to the entire connected component.

Pitfall 2: Not Resetting the Union-Find Structure

The Problem: Forgetting to reset the Union-Find after processing each value group causes incorrect unions between different values, leading to wrong rank assignments.

Incorrect Code:

# WRONG: Missing reset
for value in sorted(value_to_positions.keys()):
    for row, col in value_to_positions[value]:
        uf.union(row, col + m)
    # Process and assign ranks...
    # MISSING: uf.reset() calls

Why It Fails: Without resetting, when processing value v2 after v1, the Union-Find still maintains connections from v1, causing cells with different values to be incorrectly grouped together.

The Solution: Always reset the Union-Find nodes after processing each value group:

for row, col in value_to_positions[value]:
    uf.reset(row)
    uf.reset(col + m)

Pitfall 3: Incorrect Index Mapping for Rows and Columns

The Problem: Using the same index space for both rows and columns in the Union-Find structure causes collisions.

Incorrect Code:

# WRONG: Row and column indices overlap
uf = UnionFind(max(m, n))  # Not enough space
for row, col in value_to_positions[value]:
    uf.union(row, col)  # Collision when row index equals column index

Why It Fails: If you have a 3Γ—3 matrix, row 2 and column 2 would both map to index 2 in the Union-Find, causing incorrect unions.

The Solution: Use separate index spaces: rows use indices [0, m-1] and columns use indices [m, m+n-1]:

uf = UnionFind(m + n)
for row, col in value_to_positions[value]:
    uf.union(row, col + m)  # Offset column index by m

Pitfall 4: Processing Values in Wrong Order

The Problem: Processing values in random or descending order violates the rank ordering constraint.

Incorrect Code:

# WRONG: Processing in arbitrary order
for value in value_to_positions.keys():  # Unordered
    # Process cells with this value

Why It Fails: If you process larger values before smaller ones, you might assign a rank to a large value that's too small, making it impossible to assign correct ranks to smaller values later.

The Solution: Always process values in ascending order to ensure smaller values get smaller ranks:

for value in sorted(value_to_positions.keys()):
    # Process cells with this value
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the relationship between a tree and a graph?


Recommended Readings

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

Load More