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:
-
Ranks start from 1 - The smallest rank assigned is 1, not 0.
-
Same row/column ordering must be preserved - For any two elements
p
andq
that are in the same row OR the same column:- If
p < q
, thenrank(p) < rank(q)
- If
p == q
, thenrank(p) == rank(q)
- If
p > q
, thenrank(p) > rank(q)
- If
-
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 23
must have rank > 1 (same column as 1, and 3 > 1), so rank 24
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.
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:
- Group cells by their values
- Process groups in ascending order of values
- For each group, use Union-Find to identify connected components (cells that share rows/columns)
- For each component, find the maximum existing rank in any affected row or column
- Assign
max_existing_rank + 1
to all cells in the component - Update the row and column maximum ranks for future iterations
- 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 elementx
union(a, b)
: Merges two sets using union by size to keep the tree balancedreset(x)
: Resets elementx
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 EvaluatorExample 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))
wherek
is the number of unique values, worst casek = mn
, soO(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 functionfind(i)
:O(Ξ±(m+n))
per operationreset(i)
andreset(j+m)
:O(1)
per operation
- Total Union-Find operations:
O(mn * Ξ±(m+n))
- Total cells processed across all values:
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
What's the relationship between a tree and a graph?
Recommended Readings
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
Topological Sort Topological Order Prereq Breadth First Search Review problems graph_bfs Directed Graph Before we get started on topological order let's talk about directed graphs first A graph is directed when its edges have directions these edges are also called arcs Suppose that v w are vertices in a directed
Want a Structured Path to Master System Design Too? Donβt Miss This!