1380. Lucky Numbers in a Matrix
Problem Description
You are given an m x n
matrix containing distinct numbers. Your task is to find all the lucky numbers in this matrix.
A lucky number is defined as an element that satisfies both of these conditions:
- It is the smallest number in its row
- It is the largest number in its column
For example, consider this matrix:
[3, 7, 8] [9, 11, 13] [15, 16, 17]
The number 15
is a lucky number because:
- It's the minimum in its row:
min(15, 16, 17) = 15
- It's the maximum in its column:
max(3, 9, 15) = 15
The solution works by:
- Finding the minimum value in each row and storing them in a set
rows
- Finding the maximum value in each column and storing them in a set
cols
- Finding the intersection of these two sets - any number that appears in both sets must be both a row minimum and a column maximum
The code zip(*matrix)
transposes the matrix to easily access columns, allowing us to find the maximum of each column. The intersection operation rows & cols
finds elements that are both row minimums and column maximums, which are exactly the lucky numbers we're looking for.
You need to return all such lucky numbers in any order. Note that there might be zero, one, or multiple lucky numbers in a given matrix.
Intuition
The key insight comes from understanding what makes a number "lucky" - it must be simultaneously the smallest in its row and the largest in its column. This dual constraint creates a unique property: if a number satisfies both conditions, it must be special.
Think about it this way: if a number is the minimum in its row, we can collect all such row minimums. Similarly, if a number is the maximum in its column, we can collect all such column maximums. Now here's the clever part - a lucky number must appear in both collections.
Why? Because a lucky number needs to satisfy both conditions at the same time. If we have the number 15
that is the minimum of its row, it goes into our row minimums collection. If that same 15
is also the maximum of its column, it goes into our column maximums collection. The only way a number can appear in both collections is if it's both a row minimum AND a column maximum - which is exactly our definition of a lucky number.
This leads us to a simple approach: instead of checking each element against all elements in its row and column (which would be inefficient), we can:
- Pre-compute all row minimums
- Pre-compute all column maximums
- Find which numbers appear in both sets
The intersection of these two sets gives us all the lucky numbers, because only lucky numbers can be present in both sets. This transforms a potentially complex nested loop problem into a clean set operation problem.
Solution Approach
We implement the solution using two sets to maintain row minimums and column maximums, then find their intersection.
Step 1: Find Row Minimums
We iterate through each row in the matrix and find the minimum value in that row using the min()
function. We store all these minimums in a set called rows
:
rows = {min(row) for row in matrix}
This set comprehension processes each row and keeps only the minimum values.
Step 2: Find Column Maximums
To find the maximum in each column, we need to transpose the matrix first. The expression zip(*matrix)
achieves this:
- The
*
operator unpacks the matrix rows zip()
then groups elements by their column position
For example, if matrix is [[1,2], [3,4]]
, then zip(*matrix)
gives us [(1,3), (2,4)]
- each tuple represents a column.
We then find the maximum of each column:
cols = {max(col) for col in zip(*matrix)}
Step 3: Find Intersection
The lucky numbers are exactly those that appear in both sets. We use the set intersection operator &
to find common elements:
return list(rows & cols)
Why This Works:
- If a number
x
is in therows
set, it meansx
is the minimum of some row - If the same
x
is in thecols
set, it meansx
is the maximum of some column - When
x
appears in both sets, it satisfies both conditions for being a lucky number
Time Complexity: O(m × n)
where m
is the number of rows and n
is the number of columns, as we need to traverse the entire matrix.
Space Complexity: O(m + n)
for storing at most m
row minimums and n
column maximums.
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.
Consider this 3×3 matrix:
matrix = [[3, 7, 8], [9, 11, 13], [15, 16, 17]]
Step 1: Find Row Minimums
We examine each row and find its minimum value:
- Row 0:
[3, 7, 8]
→ minimum is3
- Row 1:
[9, 11, 13]
→ minimum is9
- Row 2:
[15, 16, 17]
→ minimum is15
So our set of row minimums is: rows = {3, 9, 15}
Step 2: Find Column Maximums
First, we transpose the matrix using zip(*matrix)
:
- Column 0:
[3, 9, 15]
- Column 1:
[7, 11, 16]
- Column 2:
[8, 13, 17]
Now we find the maximum of each column:
- Column 0:
[3, 9, 15]
→ maximum is15
- Column 1:
[7, 11, 16]
→ maximum is16
- Column 2:
[8, 13, 17]
→ maximum is17
So our set of column maximums is: cols = {15, 16, 17}
Step 3: Find Intersection
We need to find numbers that appear in both sets:
rows = {3, 9, 15}
cols = {15, 16, 17}
rows & cols = {15}
The only number that appears in both sets is 15
.
Verification:
Let's verify that 15
is indeed a lucky number:
- Is
15
the minimum in its row? Yes,min(15, 16, 17) = 15
✓ - Is
15
the maximum in its column? Yes,max(3, 9, 15) = 15
✓
Therefore, [15]
is our answer - the list containing the single lucky number in this matrix.
Solution Implementation
1class Solution:
2 def luckyNumbers(self, matrix: List[List[int]]) -> List[int]:
3 # Find the minimum value in each row
4 # These are potential lucky numbers (must be min in their row)
5 row_minimums = {min(row) for row in matrix}
6
7 # Find the maximum value in each column
8 # zip(*matrix) transposes the matrix to iterate through columns
9 # These are potential lucky numbers (must be max in their column)
10 column_maximums = {max(column) for column in zip(*matrix)}
11
12 # A lucky number must be both:
13 # - the minimum in its row AND
14 # - the maximum in its column
15 # Set intersection (&) finds values that appear in both sets
16 lucky_numbers = list(row_minimums & column_maximums)
17
18 return lucky_numbers
19
1class Solution {
2 public List<Integer> luckyNumbers(int[][] matrix) {
3 // Get matrix dimensions
4 int numRows = matrix.length;
5 int numCols = matrix[0].length;
6
7 // Arrays to store minimum value of each row and maximum value of each column
8 int[] minInRows = new int[numRows];
9 int[] maxInCols = new int[numCols];
10
11 // Initialize row minimums with a large value (equivalent to Integer.MAX_VALUE)
12 Arrays.fill(minInRows, 1 << 30);
13
14 // First pass: Find minimum in each row and maximum in each column
15 for (int row = 0; row < numRows; row++) {
16 for (int col = 0; col < numCols; col++) {
17 // Update minimum value for current row
18 minInRows[row] = Math.min(minInRows[row], matrix[row][col]);
19 // Update maximum value for current column
20 maxInCols[col] = Math.max(maxInCols[col], matrix[row][col]);
21 }
22 }
23
24 // List to store lucky numbers
25 List<Integer> luckyNumbers = new ArrayList<>();
26
27 // Second pass: Find lucky numbers (minimum in row equals maximum in column)
28 for (int row = 0; row < numRows; row++) {
29 for (int col = 0; col < numCols; col++) {
30 // A lucky number is the minimum in its row and maximum in its column
31 if (minInRows[row] == maxInCols[col]) {
32 luckyNumbers.add(minInRows[row]);
33 }
34 }
35 }
36
37 return luckyNumbers;
38 }
39}
40
1class Solution {
2public:
3 vector<int> luckyNumbers(vector<vector<int>>& matrix) {
4 // Get matrix dimensions
5 int rowCount = matrix.size();
6 int colCount = matrix[0].size();
7
8 // Arrays to store minimum value of each row and maximum value of each column
9 vector<int> minInRow(rowCount, INT_MAX); // Initialize with maximum possible value
10 vector<int> maxInCol(colCount, INT_MIN); // Initialize with minimum possible value
11
12 // First pass: find minimum in each row and maximum in each column
13 for (int i = 0; i < rowCount; ++i) {
14 for (int j = 0; j < colCount; ++j) {
15 minInRow[i] = min(minInRow[i], matrix[i][j]);
16 maxInCol[j] = max(maxInCol[j], matrix[i][j]);
17 }
18 }
19
20 // Result vector to store lucky numbers
21 vector<int> result;
22
23 // Second pass: find numbers that are both row minimum and column maximum
24 for (int i = 0; i < rowCount; ++i) {
25 for (int j = 0; j < colCount; ++j) {
26 // A lucky number is minimum in its row and maximum in its column
27 if (minInRow[i] == maxInCol[j]) {
28 result.push_back(minInRow[i]);
29 }
30 }
31 }
32
33 return result;
34 }
35};
36
1/**
2 * Finds all lucky numbers in a matrix.
3 * A lucky number is the minimum element in its row and maximum element in its column.
4 * @param matrix - The input m x n matrix of distinct numbers
5 * @returns An array containing all lucky numbers in the matrix
6 */
7function luckyNumbers(matrix: number[][]): number[] {
8 // Get matrix dimensions
9 const rowCount: number = matrix.length;
10 const colCount: number = matrix[0].length;
11
12 // Initialize arrays to store minimum value of each row and maximum value of each column
13 const rowMinimums: number[] = new Array(rowCount).fill(1 << 30); // Initialize with large value
14 const colMaximums: number[] = new Array(colCount).fill(0); // Initialize with small value
15
16 // Find minimum value in each row and maximum value in each column
17 for (let row = 0; row < rowCount; ++row) {
18 for (let col = 0; col < colCount; col++) {
19 rowMinimums[row] = Math.min(rowMinimums[row], matrix[row][col]);
20 colMaximums[col] = Math.max(colMaximums[col], matrix[row][col]);
21 }
22 }
23
24 // Store the result - lucky numbers that are both row minimum and column maximum
25 const luckyNumbersList: number[] = [];
26
27 // Check if any row minimum equals any column maximum
28 for (let row = 0; row < rowCount; ++row) {
29 for (let col = 0; col < colCount; col++) {
30 if (rowMinimums[row] === colMaximums[col]) {
31 luckyNumbersList.push(rowMinimums[row]);
32 }
33 }
34 }
35
36 return luckyNumbersList;
37}
38
Time and Space Complexity
Time Complexity: O(m × n)
The algorithm consists of three main operations:
- Finding the minimum of each row: This requires iterating through all
m
rows, and for each row, examining alln
elements. This takesO(m × n)
time. - Finding the maximum of each column: The
zip(*matrix)
operation transposes the matrix, which internally iterates through all elements once, takingO(m × n)
time. Then finding the maximum of each of then
columns requires examining allm
elements per column, which is alsoO(m × n)
time. - Finding the intersection of two sets: This operation takes
O(min(m, n))
time, which is bounded byO(m × n)
.
Therefore, the overall time complexity is O(m × n) + O(m × n) + O(min(m, n)) = O(m × n)
.
Space Complexity: O(m + n)
The algorithm uses additional space for:
- The
rows
set: Stores at mostm
elements (one minimum value per row), requiringO(m)
space. - The
cols
set: Stores at mostn
elements (one maximum value per column), requiringO(n)
space. - The
zip(*matrix)
operation creates an iterator that generates tuples on-the-fly, but the intermediate column tuples exist temporarily in memory during max computation, which usesO(m)
space per column (but processed one at a time). - The final list from the intersection: Contains at most
min(m, n)
elements, which is bounded byO(m + n)
.
The dominant space usage comes from storing the row minimums and column maximums, giving us a total space complexity of O(m + n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Assuming There's Always Exactly One Lucky Number
A common misconception is that every matrix will have exactly one lucky number. In reality, a matrix can have:
- Zero lucky numbers: When no element satisfies both conditions
- Multiple lucky numbers: Though rare with distinct numbers, it's theoretically possible
Example of zero lucky numbers:
matrix = [[1, 10, 4, 2], [9, 3, 8, 7], [15, 16, 17, 12]] # Row minimums: {1, 3, 12} # Column maximums: {15, 16, 17, 12} # Intersection: {12} - but wait, let's verify: # 12 is NOT the min in its row [15, 16, 17, 12], min is 12 ✓ # 12 is the max in column 3: [2, 7, 12] ✓ # So there is one lucky number here
Better example with no lucky numbers:
matrix = [[7, 8], [1, 2]] # Row minimums: {7, 1} # Column maximums: {7, 8} # Intersection: {7} # But 7 is in position [0,0], and max of column 0 is 7 ✓, min of row 0 is 7 ✓ # So this has one lucky number
Actual example with no lucky numbers:
matrix = [[3, 6], [7, 2]] # Row minimums: {3, 2} # Column maximums: {7, 6} # Intersection: {} - empty! # No lucky numbers exist
2. Forgetting That Numbers Must Be Distinct
The problem states numbers are distinct, which simplifies the logic. If numbers weren't distinct, you'd need to handle duplicates carefully:
# If duplicates were allowed (hypothetical): matrix = [[5, 5], [5, 5]] # Every 5 would be both min of its row AND max of its column # Would need to track positions, not just values
3. Inefficient Column Access Without Transposition
Without using zip(*matrix)
, developers might write inefficient code:
Inefficient approach:
# DON'T DO THIS - inefficient column access
column_maximums = set()
for j in range(len(matrix[0])):
col_max = matrix[0][j]
for i in range(1, len(matrix)):
col_max = max(col_max, matrix[i][j])
column_maximums.add(col_max)
Better approach (as in solution):
# DO THIS - elegant transposition
column_maximums = {max(col) for col in zip(*matrix)}
4. Returning Sets Instead of Lists
The problem asks for a list return type, but the intersection operation produces a set:
Wrong:
def luckyNumbers(self, matrix):
rows = {min(row) for row in matrix}
cols = {max(col) for col in zip(*matrix)}
return rows & cols # Returns a set, not a list!
Correct:
def luckyNumbers(self, matrix):
rows = {min(row) for row in matrix}
cols = {max(col) for col in zip(*matrix)}
return list(rows & cols) # Converts set to list
5. Memory Issues with Large Matrices
While the current solution is optimal, for extremely large matrices, you might want to find lucky numbers without storing all minimums/maximums:
Memory-optimized alternative:
def luckyNumbers(self, matrix):
m, n = len(matrix), len(matrix[0])
result = []
for i in range(m):
# Find minimum in row i and its column index
row_min = min(matrix[i])
min_col = matrix[i].index(row_min)
# Check if this minimum is also the maximum in its column
is_col_max = all(matrix[row][min_col] <= row_min for row in range(m))
if is_col_max:
result.append(row_min)
return result
This approach trades time complexity for space efficiency when needed.
Which of the following uses divide and conquer strategy?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!