2684. Maximum Number of Moves in a Grid
Problem Description
You have a 2D matrix grid
with m
rows and n
columns, where all values are positive integers. Your task is to find the maximum number of moves you can make while traversing through the matrix.
Starting Position: You can start from any cell in the first column (column 0).
Movement Rules: From any cell at position (row, col)
, you can move to one of three cells in the next column:
- Move diagonally up-right to
(row - 1, col + 1)
- Move right to
(row, col + 1)
- Move diagonally down-right to
(row + 1, col + 1)
Movement Constraint: You can only move to a cell if its value is strictly greater than your current cell's value. In other words, if you're at cell (row, col)
with value grid[row][col]
, you can only move to cell (row', col + 1)
if grid[row'][col + 1] > grid[row][col]
.
Goal: Find the maximum number of moves (steps to the right) you can make following these rules. Each move takes you exactly one column to the right.
For example, if you can traverse from column 0 to column 3, you've made 3 moves. The problem asks for the maximum possible moves achievable by choosing the optimal starting cell and path.
Intuition
The key insight is that we want to find the maximum distance (in terms of columns) we can travel from the first column. Since we can start from any row in the first column, we need to explore all possible starting positions.
Think of this problem as a level-by-level exploration. At each column (level), we want to know: "Which rows are still reachable?" We start with all rows in column 0 being reachable since we can start from any of them.
For each subsequent column, we check: from all currently reachable rows, which rows in the next column can we reach? A row in the next column is reachable if:
- It's a valid neighbor (diagonal up, straight, or diagonal down)
- Its value is strictly greater than the current cell's value
The process continues column by column. If at any point we can't reach any row in the next column, we've found our maximum moves - it's the current column index. If we successfully reach the last column, then the maximum moves is n - 1
(where n
is the number of columns).
Why does this work? Because we're essentially performing a breadth-first search (BFS) where each "level" represents a column. We maintain all possible positions we could be at in the current column, then expand to find all possible positions for the next column. The use of a set ensures we don't track duplicate positions, making the solution efficient.
The beauty of this approach is that we don't need to track individual paths or use dynamic programming to remember the best path to each cell. We only care about whether a cell is reachable at each column level, which significantly simplifies the problem.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses a BFS approach to traverse the grid column by column. Here's how the implementation works:
Initialization:
- Create a set
q
containing all row indices from0
tom-1
, representing that we can start from any row in the first column - Get the dimensions
m
(rows) andn
(columns) of the grid
Column-by-Column Traversal:
for j in range(n - 1):
We iterate through columns from 0
to n-2
(since we're checking moves to the next column j+1
).
Finding Reachable Cells in Next Column:
For each column j
, we:
- Create an empty set
t
to store reachable rows in columnj+1
- For each currently reachable row
i
in setq
:- Check three possible moves:
k
ranges fromi-1
toi+1
(representing diagonal up, straight, and diagonal down) - For each potential row
k
:- Verify it's within bounds:
0 <= k < m
- Check if the move is valid:
grid[i][j] < grid[k][j + 1]
- If both conditions are met, add
k
to sett
- Verify it's within bounds:
- Check three possible moves:
Early Termination:
if not t: return j
If set t
is empty after checking all possible moves, it means we cannot move to column j+1
from any reachable cell in column j
. Therefore, we've made j
moves (from column 0 to column j
).
Update for Next Iteration:
q = t
If we can continue, update q
with the newly reachable rows for the next iteration.
Final Result:
return n - 1
If we successfully traverse all columns without getting stuck, we've made n-1
moves (from column 0 to column n-1
).
The use of sets instead of lists is crucial here - it automatically handles duplicates when multiple cells in column j
can reach the same cell in column j+1
, preventing redundant checks and improving efficiency.
Ready to land your dream job?
Unlock your dream job with a 5-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×4 grid:
grid = [[2, 4, 3, 5], [5, 4, 9, 3], [3, 4, 2, 11]]
Initial Setup:
m = 3
(rows),n = 4
(columns)q = {0, 1, 2}
- we can start from any row in column 0
Iteration 1: From column 0 to column 1 (j = 0)
- Create empty set
t
for reachable rows in column 1 - Check from row 0 (value = 2):
- Can move to row 0, col 1? 2 < 4 ✓ → add 0 to
t
- Can move to row 1, col 1? 2 < 4 ✓ → add 1 to
t
- Can move to row 0, col 1? 2 < 4 ✓ → add 0 to
- Check from row 1 (value = 5):
- Can move to row 0, col 1? 5 < 4 ✗
- Can move to row 1, col 1? 5 < 4 ✗
- Can move to row 2, col 1? 5 < 4 ✗
- Check from row 2 (value = 3):
- Can move to row 1, col 1? 3 < 4 ✓ → add 1 to
t
(already there) - Can move to row 2, col 1? 3 < 4 ✓ → add 2 to
t
- Can move to row 1, col 1? 3 < 4 ✓ → add 1 to
- Result:
t = {0, 1, 2}
, updateq = {0, 1, 2}
Iteration 2: From column 1 to column 2 (j = 1)
- Create empty set
t
for reachable rows in column 2 - Check from row 0 (value = 4):
- Can move to row 0, col 2? 4 < 3 ✗
- Can move to row 1, col 2? 4 < 9 ✓ → add 1 to
t
- Check from row 1 (value = 4):
- Can move to row 0, col 2? 4 < 3 ✗
- Can move to row 1, col 2? 4 < 9 ✓ → add 1 to
t
(already there) - Can move to row 2, col 2? 4 < 2 ✗
- Check from row 2 (value = 4):
- Can move to row 1, col 2? 4 < 9 ✓ → add 1 to
t
(already there) - Can move to row 2, col 2? 4 < 2 ✗
- Can move to row 1, col 2? 4 < 9 ✓ → add 1 to
- Result:
t = {1}
, updateq = {1}
Iteration 3: From column 2 to column 3 (j = 2)
- Create empty set
t
for reachable rows in column 3 - Check from row 1 (value = 9):
- Can move to row 0, col 3? 9 < 5 ✗
- Can move to row 1, col 3? 9 < 3 ✗
- Can move to row 2, col 3? 9 < 11 ✓ → add 2 to
t
- Result:
t = {2}
, updateq = {2}
End of Loop:
We've successfully traversed all columns (reached column 3), so we return n - 1 = 3
.
The maximum number of moves is 3, achieved by following the path:
- Start at (0, 0) with value 2
- Move to (1, 1) with value 4
- Move to (1, 2) with value 9
- Move to (2, 3) with value 11
Note how the algorithm doesn't track specific paths but instead maintains all reachable positions at each column level, efficiently finding the maximum possible moves.
Solution Implementation
1class Solution:
2 def maxMoves(self, grid: List[List[int]]) -> int:
3 """
4 Find the maximum number of moves that can be made in the grid.
5 Starting from any cell in the first column, we can move to the next column
6 to cells (row-1, col+1), (row, col+1), or (row+1, col+1) if the value
7 in the destination cell is strictly greater than the current cell.
8
9 Args:
10 grid: 2D list of integers representing the grid
11
12 Returns:
13 Maximum number of moves possible
14 """
15 rows, cols = len(grid), len(grid[0])
16
17 # Initialize with all row indices from the first column as possible starting positions
18 reachable_rows = set(range(rows))
19
20 # Iterate through each column (except the last one)
21 for col in range(cols - 1):
22 # Set to store reachable rows in the next column
23 next_reachable_rows = set()
24
25 # For each currently reachable row in the current column
26 for current_row in reachable_rows:
27 # Check three possible moves: up-right, right, down-right
28 for next_row in range(current_row - 1, current_row + 2):
29 # Check if the next row is within bounds
30 if 0 <= next_row < rows:
31 # Check if we can move to this cell (value must be strictly greater)
32 if grid[current_row][col] < grid[next_row][col + 1]:
33 next_reachable_rows.add(next_row)
34
35 # If no cells are reachable in the next column, return current column as max moves
36 if not next_reachable_rows:
37 return col
38
39 # Update reachable rows for the next iteration
40 reachable_rows = next_reachable_rows
41
42 # If we reached the last column, return the maximum possible moves
43 return cols - 1
44
1class Solution {
2 public int maxMoves(int[][] grid) {
3 int numRows = grid.length;
4 int numCols = grid[0].length;
5
6 // Initialize set with all row indices (starting positions in column 0)
7 Set<Integer> currentReachableRows = IntStream.range(0, numRows)
8 .boxed()
9 .collect(Collectors.toSet());
10
11 // Iterate through columns from left to right (except the last column)
12 for (int col = 0; col < numCols - 1; col++) {
13 // Set to store reachable row indices in the next column
14 Set<Integer> nextReachableRows = new HashSet<>();
15
16 // For each reachable row in current column
17 for (int currentRow : currentReachableRows) {
18 // Check three possible moves: up-right, right, down-right
19 for (int nextRow = currentRow - 1; nextRow <= currentRow + 1; nextRow++) {
20 // Validate row bounds and check if value increases
21 if (nextRow >= 0 && nextRow < numRows &&
22 grid[currentRow][col] < grid[nextRow][col + 1]) {
23 nextReachableRows.add(nextRow);
24 }
25 }
26 }
27
28 // If no cells are reachable in next column, return current column as max moves
29 if (nextReachableRows.isEmpty()) {
30 return col;
31 }
32
33 // Update reachable rows for next iteration
34 currentReachableRows = nextReachableRows;
35 }
36
37 // If we can reach the last column, return maximum possible moves
38 return numCols - 1;
39 }
40}
41
1class Solution {
2public:
3 int maxMoves(vector<vector<int>>& grid) {
4 int rows = grid.size();
5 int cols = grid[0].size();
6
7 // Set to store reachable row indices in current column
8 unordered_set<int> currentReachableRows;
9 // Set to store reachable row indices in next column
10 unordered_set<int> nextReachableRows;
11
12 // Initialize: all rows in first column are reachable
13 for (int row = 0; row < rows; ++row) {
14 currentReachableRows.insert(row);
15 }
16
17 // Process each column from left to right (except the last column)
18 for (int col = 0; col < cols - 1; ++col) {
19 nextReachableRows.clear();
20
21 // For each reachable position in current column
22 for (int currentRow : currentReachableRows) {
23 // Check three possible moves: up-right, right, down-right
24 for (int nextRow = currentRow - 1; nextRow <= currentRow + 1; ++nextRow) {
25 // Check if next position is valid and value is strictly greater
26 if (nextRow >= 0 && nextRow < rows &&
27 grid[currentRow][col] < grid[nextRow][col + 1]) {
28 nextReachableRows.insert(nextRow);
29 }
30 }
31 }
32
33 // If no positions are reachable in next column, return current column index
34 if (nextReachableRows.empty()) {
35 return col;
36 }
37
38 // Move to next column
39 currentReachableRows.swap(nextReachableRows);
40 }
41
42 // If we reached the last column, return maximum moves possible
43 return cols - 1;
44 }
45};
46
1/**
2 * Finds the maximum number of moves that can be made in a grid
3 * Starting from any cell in the first column, move to adjacent cells in the next column
4 * if the value is strictly greater than the current cell
5 * @param grid - 2D array representing the grid
6 * @returns Maximum number of moves possible
7 */
8function maxMoves(grid: number[][]): number {
9 const rowCount: number = grid.length;
10 const colCount: number = grid[0].length;
11
12 // Initialize with all row indices from the first column as starting positions
13 let currentReachableRows: Set<number> = new Set<number>(
14 Array.from({ length: rowCount }, (_, index) => index)
15 );
16
17 // Iterate through each column (except the last one)
18 for (let col = 0; col < colCount - 1; col++) {
19 const nextReachableRows: Set<number> = new Set<number>();
20
21 // For each reachable row in the current column
22 for (const currentRow of currentReachableRows) {
23 // Check three possible moves: up-right, right, down-right
24 for (let nextRow = currentRow - 1; nextRow <= currentRow + 1; nextRow++) {
25 // Validate row bounds and check if move is valid (next cell value must be greater)
26 if (nextRow >= 0 &&
27 nextRow < rowCount &&
28 grid[currentRow][col] < grid[nextRow][col + 1]) {
29 nextReachableRows.add(nextRow);
30 }
31 }
32 }
33
34 // If no valid moves exist, return the current column index as max moves
35 if (nextReachableRows.size === 0) {
36 return col;
37 }
38
39 // Update reachable rows for the next iteration
40 currentReachableRows = nextReachableRows;
41 }
42
43 // If we reached the last column, return the maximum possible moves
44 return colCount - 1;
45}
46
Time and Space Complexity
Time Complexity: O(m × n)
The algorithm uses a BFS-like approach that processes the grid column by column from left to right. For each column j
(from 0 to n-2), it examines all valid row positions in set q
. For each row position i
in q
, it checks at most 3 adjacent positions (i-1, i, i+1) in the next column. In the worst case, all m
rows could be valid positions at each column, leading to examining up to 3m
positions per column. Since we process n-1
columns, the total time complexity is O(3m × (n-1))
, which simplifies to O(m × n)
.
Space Complexity: O(m)
The algorithm maintains two sets: q
and t
. Set q
stores the valid row indices that can be reached in the current column, while set t
temporarily stores the valid row indices for the next column. In the worst case, both sets could contain all m
row indices, but they never exceed this size. Since we only keep track of positions for the current and next columns (not the entire path), the space complexity is O(m)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Using Visited Tracking Unnecessarily
A common mistake is treating this problem like a typical graph traversal and maintaining a visited set across all iterations:
Incorrect Approach:
visited = set() # Global visited set - WRONG!
for col in range(cols - 1):
next_reachable_rows = set()
for current_row in reachable_rows:
if (current_row, col) in visited:
continue
visited.add((current_row, col))
# ... rest of logic
Why it's wrong: Since we're moving strictly from left to right (column by column), we can never revisit a cell. Each column is processed exactly once, and we only move forward. Adding visited tracking not only adds unnecessary complexity but can also prevent valid paths from being explored if multiple paths converge at the same cell.
Correct Approach: Simply track reachable rows for each column without any visited set, as shown in the original solution.
2. Confusing Move Count with Column Index
Another pitfall is incorrectly calculating the number of moves:
Incorrect:
if not next_reachable_rows: return col + 1 # WRONG - this would count columns, not moves
Why it's wrong: If we can't move from column j
to column j+1
, we've made exactly j
moves (from column 0 to column j). The move count represents transitions between columns, not the column indices themselves.
Correct: Return col
when no further moves are possible, and cols - 1
if we reach the last column.
3. Using Lists Instead of Sets for Reachable Rows
Inefficient Approach:
reachable_rows = list(range(rows)) # Using list instead of set
for col in range(cols - 1):
next_reachable_rows = []
for current_row in reachable_rows:
for next_row in range(current_row - 1, current_row + 2):
if 0 <= next_row < rows:
if grid[current_row][col] < grid[next_row][col + 1]:
next_reachable_rows.append(next_row) # Duplicates!
Why it's problematic: Multiple cells in column j
might be able to reach the same cell in column j+1
. Using a list would store duplicates, leading to:
- Redundant processing in subsequent iterations
- Time complexity degradation from O(mn) to potentially O(m²n)
- Unnecessary memory usage
Correct: Use sets to automatically handle duplicates and maintain optimal performance.
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
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
Want a Structured Path to Master System Design Too? Don’t Miss This!