Facebook Pixel

2713. Maximum Strictly Increasing Cells in a Matrix

Problem Description

You are given a 1-indexed m x n integer matrix mat. Your goal is to find the maximum number of cells you can visit by following a specific movement rule.

Movement Rules:

  • You can start from any cell in the matrix
  • From your current cell, you can move to any other cell in the same row or same column
  • You can only move to a cell if its value is strictly greater than your current cell's value
  • You can continue moving as long as valid moves exist

The task is to determine the maximum number of cells that can be visited in a single path, starting from the optimal starting cell.

Example Understanding: If you're at cell (i, j) with value v, you can move to:

  • Any cell in row i with value > v
  • Any cell in column j with value > v

Each move must go to a strictly increasing value, and you want to find the longest possible path through the matrix following these rules.

The function should return an integer representing the maximum number of cells that can be visited in a single path.

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

Intuition

Since we can only move to cells with strictly greater values, we need to process cells in increasing order of their values. This ensures that when we process a cell with value v, we've already computed the maximum path lengths for all cells with values less than v.

The key insight is that for any cell (i, j), the longest path starting from it depends on:

  1. The longest path we can achieve by moving within row i to cells with greater values
  2. The longest path we can achieve by moving within column j to cells with greater values

We take the maximum of these two options and add 1 (for the current cell itself).

To efficiently track this information, we maintain:

  • rowMax[i]: the length of the longest path found so far in row i
  • colMax[j]: the length of the longest path found so far in column j

When processing cells with the same value, we need to handle them together as a group. We first calculate the new path lengths for all cells in this group (since they all have the same value and could potentially be starting points), then update the rowMax and colMax arrays. This two-step process prevents cells with the same value from incorrectly benefiting from each other's path lengths.

By processing values from smallest to largest and maintaining these maximum lengths for each row and column, we build up the solution dynamically. Each cell's maximum path length is 1 + max(rowMax[i], colMax[j]), representing the best we can do by either continuing along the row or column from previously processed smaller-valued cells.

Learn more about Memoization, Binary Search, Dynamic Programming and Sorting patterns.

Solution Approach

The solution uses sorting combined with dynamic programming to efficiently compute the maximum path length.

Step 1: Group cells by value

g = defaultdict(list)
for i in range(m):
    for j in range(n):
        g[mat[i][j]].append((i, j))

We create a hash table g where each key is a cell value, and the value is a list of all positions (i, j) that contain that value. This allows us to process cells in order of their values.

Step 2: Initialize tracking arrays

rowMax = [0] * m
colMax = [0] * n
  • rowMax[i] tracks the maximum path length achieved in row i so far
  • colMax[j] tracks the maximum path length achieved in column j so far

Step 3: Process values in ascending order

for _, pos in sorted(g.items()):

By sorting the hash table by keys, we process all cells with smaller values before cells with larger values. This ensures when we compute the path length for a cell, all potential previous cells in the path have already been processed.

Step 4: Calculate path lengths for cells with the same value

mx = []
for i, j in pos:
    mx.append(1 + max(rowMax[i], colMax[j]))
    ans = max(ans, mx[-1])

For each group of cells with the same value:

  • Calculate the maximum path length ending at each cell as 1 + max(rowMax[i], colMax[j])
  • The 1 accounts for the current cell itself
  • max(rowMax[i], colMax[j]) gives the best path length we can inherit from either the row or column
  • Store these values temporarily in mx array to avoid updating rowMax and colMax prematurely

Step 5: Update row and column maximums

for k, (i, j) in enumerate(pos):
    rowMax[i] = max(rowMax[i], mx[k])
    colMax[j] = max(colMax[j], mx[k])

After calculating all path lengths for cells with the same value, update the rowMax and colMax arrays. This two-phase approach ensures cells with the same value don't incorrectly influence each other's calculations.

The algorithm returns the maximum path length found across all cells. The time complexity is O(mn log(mn)) due to sorting, and space complexity is O(mn) for storing the cell positions.

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

Consider the matrix:

mat = [[3, 1],
       [2, 4]]

Step 1: Group cells by value We create a dictionary grouping cells by their values:

  • g[1] = [(0,1)] - value 1 is at position (0,1)
  • g[2] = [(1,0)] - value 2 is at position (1,0)
  • g[3] = [(0,0)] - value 3 is at position (0,0)
  • g[4] = [(1,1)] - value 4 is at position (1,1)

Step 2: Initialize tracking arrays

  • rowMax = [0, 0] - maximum path lengths for row 0 and row 1
  • colMax = [0, 0] - maximum path lengths for column 0 and column 1
  • ans = 0 - global maximum

Step 3: Process values in ascending order (1, 2, 3, 4)

Processing value 1 at position (0,1):

  • Calculate path length: 1 + max(rowMax[0], colMax[1]) = 1 + max(0, 0) = 1
  • Update ans = 1
  • Update arrays: rowMax[0] = 1, colMax[1] = 1
  • Arrays now: rowMax = [1, 0], colMax = [0, 1]

Processing value 2 at position (1,0):

  • Calculate path length: 1 + max(rowMax[1], colMax[0]) = 1 + max(0, 0) = 1
  • ans remains 1
  • Update arrays: rowMax[1] = 1, colMax[0] = 1
  • Arrays now: rowMax = [1, 1], colMax = [1, 1]

Processing value 3 at position (0,0):

  • Calculate path length: 1 + max(rowMax[0], colMax[0]) = 1 + max(1, 1) = 2
    • This means we can reach cell (0,0) from either cell (0,1) with value 1 (same row) or cell (1,0) with value 2 (same column)
  • Update ans = 2
  • Update arrays: rowMax[0] = 2, colMax[0] = 2
  • Arrays now: rowMax = [2, 1], colMax = [2, 1]

Processing value 4 at position (1,1):

  • Calculate path length: 1 + max(rowMax[1], colMax[1]) = 1 + max(1, 1) = 2
    • From row 1: best previous path has length 1 (from cell (1,0) with value 2)
    • From column 1: best previous path has length 1 (from cell (0,1) with value 1)
  • ans remains 2
  • Update arrays: rowMax[1] = 2, colMax[1] = 2

Final Result: 2

The maximum path length is 2. There are two possible maximum paths:

  1. Start at (0,1) with value 1 → move to (0,0) with value 3 (same row)
  2. Start at (1,0) with value 2 → move to (0,0) with value 3 (same column)

Both paths visit exactly 2 cells, which is the maximum possible for this matrix.

Solution Implementation

1class Solution:
2    def maxIncreasingCells(self, mat: List[List[int]]) -> int:
3        # Get matrix dimensions
4        m, n = len(mat), len(mat[0])
5      
6        # Group cells by their values
7        value_to_positions = defaultdict(list)
8        for i in range(m):
9            for j in range(n):
10                value_to_positions[mat[i][j]].append((i, j))
11      
12        # Track maximum path length ending at each row and column
13        row_max = [0] * m
14        col_max = [0] * n
15      
16        # Track the overall maximum path length
17        ans = 0
18      
19        # Process cells in ascending order of their values
20        for value in sorted(value_to_positions.keys()):
21            positions = value_to_positions[value]
22          
23            # Calculate max path length for all cells with current value
24            current_max_lengths = []
25            for i, j in positions:
26                # Max path length is 1 + max of previous paths in same row or column
27                max_length = 1 + max(row_max[i], col_max[j])
28                current_max_lengths.append(max_length)
29                ans = max(ans, max_length)
30          
31            # Update row and column maximums with calculated values
32            # Done after all calculations to handle cells with same value correctly
33            for k, (i, j) in enumerate(positions):
34                row_max[i] = max(row_max[i], current_max_lengths[k])
35                col_max[j] = max(col_max[j], current_max_lengths[k])
36      
37        return ans
38
1class Solution {
2    public int maxIncreasingCells(int[][] mat) {
3        int numRows = mat.length;
4        int numCols = mat[0].length;
5      
6        // Group all cells by their values in ascending order
7        // TreeMap ensures values are processed from smallest to largest
8        TreeMap<Integer, List<int[]>> valueToPositions = new TreeMap<>();
9        for (int row = 0; row < numRows; row++) {
10            for (int col = 0; col < numCols; col++) {
11                valueToPositions.computeIfAbsent(mat[row][col], k -> new ArrayList<>())
12                               .add(new int[] {row, col});
13            }
14        }
15      
16        // Track the maximum path length ending at each row and column
17        int[] maxPathInRow = new int[numRows];
18        int[] maxPathInCol = new int[numCols];
19        int maxPathLength = 0;
20      
21        // Process cells in ascending order of their values
22        for (var entry : valueToPositions.entrySet()) {
23            List<int[]> positions = entry.getValue();
24          
25            // Calculate the maximum path length for each cell with current value
26            // Store temporarily to avoid updating row/col max while still processing
27            int[] currentLevelMax = new int[positions.size()];
28            int index = 0;
29          
30            for (int[] position : positions) {
31                int row = position[0];
32                int col = position[1];
33              
34                // Path length = max(previous max in same row, previous max in same col) + 1
35                currentLevelMax[index] = Math.max(maxPathInRow[row], maxPathInCol[col]) + 1;
36                maxPathLength = Math.max(maxPathLength, currentLevelMax[index]);
37                index++;
38            }
39          
40            // Update row and column maximums with the computed values
41            // Done after all cells with same value are processed
42            for (int i = 0; i < currentLevelMax.length; i++) {
43                int row = positions.get(i)[0];
44                int col = positions.get(i)[1];
45                maxPathInRow[row] = Math.max(maxPathInRow[row], currentLevelMax[i]);
46                maxPathInCol[col] = Math.max(maxPathInCol[col], currentLevelMax[i]);
47            }
48        }
49      
50        return maxPathLength;
51    }
52}
53
1class Solution {
2public:
3    int maxIncreasingCells(vector<vector<int>>& mat) {
4        int numRows = mat.size();
5        int numCols = mat[0].size();
6      
7        // Group cells by their values, storing positions for each unique value
8        map<int, vector<pair<int, int>>> valueToPositions;
9        for (int row = 0; row < numRows; ++row) {
10            for (int col = 0; col < numCols; ++col) {
11                valueToPositions[mat[row][col]].emplace_back(row, col);
12            }
13        }
14      
15        // Track the maximum path length ending at each row and column
16        vector<int> maxPathInRow(numRows);
17        vector<int> maxPathInCol(numCols);
18      
19        int maxPathLength = 0;
20      
21        // Process cells in ascending order of their values
22        for (auto& [value, positions] : valueToPositions) {
23            // Calculate new path lengths for all cells with the current value
24            vector<int> newPathLengths;
25            for (auto& [row, col] : positions) {
26                // Path length is 1 + max of the longest path in the same row or column
27                int currentPathLength = max(maxPathInRow[row], maxPathInCol[col]) + 1;
28                newPathLengths.push_back(currentPathLength);
29                maxPathLength = max(maxPathLength, currentPathLength);
30            }
31          
32            // Update row and column maximums with the new path lengths
33            for (int idx = 0; idx < newPathLengths.size(); ++idx) {
34                auto& [row, col] = positions[idx];
35                maxPathInRow[row] = max(maxPathInRow[row], newPathLengths[idx]);
36                maxPathInCol[col] = max(maxPathInCol[col], newPathLengths[idx]);
37            }
38        }
39      
40        return maxPathLength;
41    }
42};
43
1/**
2 * Finds the maximum number of cells that can be visited starting from any cell,
3 * where you can move to another cell in the same row or column with a strictly greater value
4 * @param mat - The input matrix
5 * @returns The maximum number of cells that can be visited
6 */
7function maxIncreasingCells(mat: number[][]): number {
8    const rows: number = mat.length;
9    const cols: number = mat[0].length;
10  
11    // Group cells by their values for processing in ascending order
12    const valueToPositions: Map<number, Array<[number, number]>> = new Map();
13
14    // Populate the map with positions grouped by their values
15    for (let row = 0; row < rows; row++) {
16        for (let col = 0; col < cols; col++) {
17            const value = mat[row][col];
18            if (!valueToPositions.has(value)) {
19                valueToPositions.set(value, []);
20            }
21            valueToPositions.get(value)!.push([row, col]);
22        }
23    }
24
25    // Track the maximum path length achievable from each row and column
26    const maxPathFromRow: number[] = Array(rows).fill(0);
27    const maxPathFromCol: number[] = Array(cols).fill(0);
28    let maxPathLength: number = 0;
29
30    // Get all unique values and sort them in ascending order
31    const sortedValues: number[] = Array.from(valueToPositions.keys()).sort((a, b) => a - b);
32
33    // Process cells in ascending order of their values
34    for (const currentValue of sortedValues) {
35        const positions: Array<[number, number]> = valueToPositions.get(currentValue)!;
36        const currentPathLengths: number[] = [];
37
38        // Calculate the maximum path length for each position with the current value
39        for (const [row, col] of positions) {
40            // The path length is 1 plus the maximum of paths from the same row or column
41            const pathLength = 1 + Math.max(maxPathFromRow[row], maxPathFromCol[col]);
42            currentPathLengths.push(pathLength);
43            maxPathLength = Math.max(maxPathLength, pathLength);
44        }
45
46        // Update the maximum path lengths for rows and columns
47        for (let index = 0; index < positions.length; index++) {
48            const [row, col] = positions[index];
49            const pathLength = currentPathLengths[index];
50            maxPathFromRow[row] = Math.max(maxPathFromRow[row], pathLength);
51            maxPathFromCol[col] = Math.max(maxPathFromCol[col], pathLength);
52        }
53    }
54
55    return maxPathLength;
56}
57

Time and Space Complexity

Time Complexity: O(m × n × log(m × n))

The algorithm processes each cell in the matrix exactly once to build the dictionary g, which takes O(m × n) time. The dictionary groups cells by their values, storing their coordinates.

The sorting operation sorted(g.items()) sorts all unique values in the matrix. In the worst case, all m × n cells have unique values, so sorting takes O(m × n × log(m × n)) time.

After sorting, the algorithm iterates through each group of cells with the same value. For each cell, it performs constant-time operations: calculating the maximum from rowMax[i] and colMax[j], and updating these arrays. Since there are m × n cells total, this iteration takes O(m × n) time.

The dominant operation is the sorting step, giving an overall time complexity of O(m × n × log(m × n)).

Space Complexity: O(m × n)

The dictionary g stores the coordinates of all m × n cells, requiring O(m × n) space. The arrays rowMax and colMax use O(m) and O(n) space respectively, and the temporary list mx can store at most O(m × n) values in the worst case when all cells have the same value. Since O(m) + O(n) ≤ O(m × n), the overall space complexity is O(m × n).

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

Common Pitfalls

Pitfall 1: Updating row/column maximums immediately

The Problem: A critical mistake is updating row_max and col_max arrays immediately while processing cells with the same value:

# INCORRECT approach
for i, j in positions:
    max_length = 1 + max(row_max[i], col_max[j])
    row_max[i] = max(row_max[i], max_length)  # Wrong! Updates too early
    col_max[j] = max(col_max[j], max_length)  # Wrong! Updates too early
    ans = max(ans, max_length)

Why it fails: Consider a matrix where cells (0,0) and (0,1) both have value 5. If we process (0,0) first and immediately update row_max[0], then when processing (0,1), it would incorrectly inherit the updated value from row_max[0], creating a false dependency between cells of equal value.

Example failure case:

Matrix: [[5, 5],
         [1, 2]]
  • Processing cell (0,0) with value 5: calculates length as 1, updates row_max[0] = 1
  • Processing cell (0,1) with value 5: incorrectly calculates 1 + row_max[0] = 2 instead of 1

The Solution: Store calculated values in a temporary array first, then update all row/column maximums after processing all cells with the same value:

current_max_lengths = []
for i, j in positions:
    max_length = 1 + max(row_max[i], col_max[j])
    current_max_lengths.append(max_length)
    ans = max(ans, max_length)

# Update only after all calculations are done
for k, (i, j) in enumerate(positions):
    row_max[i] = max(row_max[i], current_max_lengths[k])
    col_max[j] = max(col_max[j], current_max_lengths[k])

Pitfall 2: Not handling duplicate values correctly

The Problem: Assuming each cell value is unique and processing them individually rather than in groups:

# INCORRECT approach
for i in range(m):
    for j in range(n):
        # Process each cell independently
        max_length = 1 + max(row_max[i], col_max[j])
        row_max[i] = max_length
        col_max[j] = max_length

Why it fails: This approach doesn't ensure cells are processed in ascending order of values, and it doesn't handle cells with equal values correctly. You cannot move between cells with equal values, so they must be processed as independent starting points at the same "level" of the dynamic programming.

The Solution: Group cells by value and process each group together, ensuring all cells with the same value are treated as potential path endpoints at the same level, with no dependencies between them.

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

How does merge sort divide the problem into subproblems?


Recommended Readings

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

Load More