1301. Number of Paths with Max Score
Problem Description
You have a square board filled with characters where you need to find paths from the bottom-right corner to the top-left corner while collecting maximum points.
Board Setup:
- The starting position is at the bottom-right corner, marked with character
'S'
- The ending position is at the top-left corner, marked with character
'E'
- Other squares contain either:
- Numeric characters
'1'
through'9'
(representing points you can collect) - Obstacles marked with
'X'
(blocking your path)
- Numeric characters
Movement Rules:
- You can only move in three directions from any position:
- Up (one square up)
- Left (one square left)
- Up-left (diagonally)
- You cannot move through or onto squares containing obstacles
'X'
Objective:
Find all paths from 'S'
to 'E'
that give you the maximum possible sum of numeric values collected along the way.
Return Value: Return a list of two integers:
- The maximum sum of numeric characters you can collect
- The number of different paths that achieve this maximum sum (modulo
10^9 + 7
)
If no valid path exists from start to end, return [0, 0]
.
Example:
If you have a path that goes through squares with values '3'
, '2'
, and '4'
, you would collect a total of 9 points for that path. The problem asks you to find the path(s) with the highest total and count how many such optimal paths exist.
Intuition
The key insight is to work backwards from the destination to the start. Since we need to move from bottom-right to top-left, we can flip our perspective and think about reaching each position from the bottom-right corner.
Why work backwards? Because when we're at any position (i, j)
, we want to know: "What's the best score I can get from here to the destination?" This is easier to compute if we've already solved for positions closer to the destination.
For each position, we need to track two things:
- Maximum score possible from that position to the destination
- Number of ways to achieve that maximum score
This naturally leads to a dynamic programming approach where:
f[i][j]
= maximum score from position(i, j)
to the destination(n-1, n-1)
g[i][j]
= number of paths achieving that maximum score
Starting from the destination (n-1, n-1)
with score 0 and 1 way to reach it, we work our way backwards. For each position (i, j)
, we check the three positions we could have come from:
(i+1, j)
- the position below(i, j+1)
- the position to the right(i+1, j+1)
- the position diagonally below-right
The maximum score at (i, j)
equals the best score from these three neighboring positions, plus the value at the current position (if it's a digit). If multiple neighbors give the same maximum score, we sum up their path counts since all those paths are equally optimal.
By processing positions in reverse order (from bottom-right to top-left), we ensure that when calculating values for position (i, j)
, all positions it depends on have already been computed. This bottom-up approach guarantees we find the optimal solution efficiently in O(n²)
time.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution uses dynamic programming with a bottom-up approach, processing the board from the destination (bottom-right) to the start (top-left).
Data Structures:
f[i][j]
: 2D array storing the maximum score from position(i, j)
to destination(n-1, n-1)
g[i][j]
: 2D array storing the number of paths achieving the maximum score- Initialize
f
with-1
(indicating unreachable) andg
with0
- Set
f[n-1][n-1] = 0
andg[n-1][n-1] = 1
as base case
Core Algorithm:
-
Iterate in reverse order - Process positions from
(n-1, n-1)
to(0, 0)
:for i in range(n - 1, -1, -1): for j in range(n - 1, -1, -1):
-
Update function - For each position
(i, j)
, check three possible next positions:(i+1, j)
- moving down(i, j+1)
- moving right(i+1, j+1)
- moving diagonally
The
update
function handles the logic:- Skip if next position is out of bounds or unreachable (
f[x][y] == -1
) - Skip if current position is obstacle
'X'
or start'S'
- If next position has better score: update both
f[i][j]
andg[i][j]
- If next position has equal score: accumulate paths in
g[i][j]
-
Add current position value - After updating from neighbors:
if f[i][j] != -1 and board[i][j].isdigit(): f[i][j] += int(board[i][j])
This adds the numeric value at the current position to the maximum score.
-
Return result:
- If
f[0][0] == -1
: no path exists, return[0, 0]
- Otherwise: return
[f[0][0], g[0][0] % mod]
wheremod = 10^9 + 7
- If
Time Complexity: O(n²)
- each cell is processed once with constant work per cell
Space Complexity: O(n²)
- for the two DP arrays
The key insight is that by working backwards and tracking both maximum scores and path counts simultaneously, we can efficiently solve both parts of the problem in a single pass through the board.
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 3×3 board example to illustrate the solution approach:
Board: E 2 X 1 5 3 X 4 S Position coordinates: (0,0) (0,1) (0,2) (1,0) (1,1) (1,2) (2,0) (2,1) (2,2)
Step 1: Initialize DP arrays
f[i][j]
= maximum score from position (i,j) to destinationg[i][j]
= number of paths achieving that maximum score- All positions start with
f = -1
(unreachable) andg = 0
- Base case:
f[2][2] = 0
,g[2][2] = 1
(at destination 'S')
Step 2: Process positions in reverse order
Processing (2,2) - 'S':
- Already initialized:
f[2][2] = 0
,g[2][2] = 1
Processing (2,1) - '4':
- Check neighbors: only (2,2) is valid (in bounds)
- From (2,2): score = 0, paths = 1
- Update:
f[2][1] = 0
,g[2][1] = 1
- Add digit value:
f[2][1] = 0 + 4 = 4
Processing (2,0) - 'X':
- Skip (obstacle)
Processing (1,2) - '3':
- Check neighbor (2,2): score = 0, paths = 1
- Update:
f[1][2] = 0
,g[1][2] = 1
- Add digit value:
f[1][2] = 0 + 3 = 3
Processing (1,1) - '5':
- Check three neighbors:
- (2,1): score = 4, paths = 1
- (2,2): score = 0, paths = 1
- (1,2): score = 3, paths = 1
- Best score is 4 from (2,1)
- Update:
f[1][1] = 4
,g[1][1] = 1
- Add digit value:
f[1][1] = 4 + 5 = 9
Processing (1,0) - '1':
- Check neighbors:
- (2,0): 'X' obstacle, skip
- (2,1): score = 4, paths = 1
- (1,1): score = 9, paths = 1
- Best score is 9 from (1,1)
- Update:
f[1][0] = 9
,g[1][0] = 1
- Add digit value:
f[1][0] = 9 + 1 = 10
Processing (0,2) - 'X':
- Skip (obstacle)
Processing (0,1) - '2':
- Check neighbors:
- (1,1): score = 9, paths = 1
- (1,2): score = 3, paths = 1
- (0,2): 'X' obstacle, skip
- Best score is 9 from (1,1)
- Update:
f[0][1] = 9
,g[0][1] = 1
- Add digit value:
f[0][1] = 9 + 2 = 11
Processing (0,0) - 'E':
- Check neighbors:
- (1,0): score = 10, paths = 1
- (1,1): score = 9, paths = 1
- (0,1): score = 11, paths = 1
- Best score is 11 from (0,1)
- Update:
f[0][0] = 11
,g[0][0] = 1
- 'E' is not a digit, so no value added
Step 3: Return result
f[0][0] = 11
(maximum score)g[0][0] = 1
(number of paths)- Return:
[11, 1]
The optimal path is: E → 2 → 5 → 4 → S, collecting 2+5+4 = 11 points.
Solution Implementation
1class Solution:
2 def pathsWithMaxScore(self, board: List[str]) -> List[int]:
3 """
4 Find the maximum score and number of paths from 'E' to 'S' on the board.
5
6 Args:
7 board: 2D grid where 'E' is end, 'S' is start, 'X' is obstacle, digits are scores
8
9 Returns:
10 List containing [max_score, number_of_paths] or [0, 0] if no valid path exists
11 """
12
13 def update_cell(curr_row: int, curr_col: int, prev_row: int, prev_col: int) -> None:
14 """
15 Update the current cell's score and path count based on the previous cell.
16
17 Args:
18 curr_row, curr_col: Current cell coordinates
19 prev_row, prev_col: Previous cell coordinates to check
20 """
21 # Skip if previous cell is out of bounds or unreachable
22 if prev_row >= board_size or prev_col >= board_size or max_score[prev_row][prev_col] == -1:
23 return
24
25 # Skip if current cell is obstacle or start position
26 if board[curr_row][curr_col] in "XS":
27 return
28
29 # If previous cell has better score, update current cell
30 if max_score[prev_row][prev_col] > max_score[curr_row][curr_col]:
31 max_score[curr_row][curr_col] = max_score[prev_row][prev_col]
32 path_count[curr_row][curr_col] = path_count[prev_row][prev_col]
33 # If previous cell has same score, add its path count to current
34 elif max_score[prev_row][prev_col] == max_score[curr_row][curr_col]:
35 path_count[curr_row][curr_col] += path_count[prev_row][prev_col]
36
37 board_size = len(board)
38
39 # max_score[i][j] stores the maximum score to reach cell (i, j)
40 max_score = [[-1] * board_size for _ in range(board_size)]
41
42 # path_count[i][j] stores the number of paths with maximum score to reach cell (i, j)
43 path_count = [[0] * board_size for _ in range(board_size)]
44
45 # Initialize the end position 'S' at bottom-right corner
46 max_score[-1][-1] = 0
47 path_count[-1][-1] = 1
48
49 # Process cells from bottom-right to top-left
50 for row in range(board_size - 1, -1, -1):
51 for col in range(board_size - 1, -1, -1):
52 # Update current cell from three possible previous cells
53 update_cell(row, col, row + 1, col) # Cell below
54 update_cell(row, col, row, col + 1) # Cell to the right
55 update_cell(row, col, row + 1, col + 1) # Cell diagonally down-right
56
57 # Add the score of current cell if it's reachable and contains a digit
58 if max_score[row][col] != -1 and board[row][col].isdigit():
59 max_score[row][col] += int(board[row][col])
60
61 MOD = 10**9 + 7
62
63 # Return result for start position 'E' at top-left corner
64 if max_score[0][0] == -1:
65 return [0, 0]
66 else:
67 return [max_score[0][0], path_count[0][0] % MOD]
68
1class Solution {
2 // Board reference and size
3 private List<String> board;
4 private int boardSize;
5
6 // Dynamic programming arrays
7 private int[][] maxScore; // maxScore[i][j]: maximum score from (i,j) to end
8 private int[][] pathCount; // pathCount[i][j]: number of paths achieving max score
9
10 // Modulo constant for path counting
11 private final int MOD = (int) 1e9 + 7;
12
13 public int[] pathsWithMaxScore(List<String> board) {
14 // Initialize board and size
15 this.boardSize = board.size();
16 this.board = board;
17
18 // Initialize DP arrays
19 maxScore = new int[boardSize][boardSize];
20 pathCount = new int[boardSize][boardSize];
21
22 // Fill maxScore with -1 to indicate unvisited cells
23 for (int[] row : maxScore) {
24 Arrays.fill(row, -1);
25 }
26
27 // Base case: end position 'S' has score 0 and 1 path
28 maxScore[boardSize - 1][boardSize - 1] = 0;
29 pathCount[boardSize - 1][boardSize - 1] = 1;
30
31 // Process cells from bottom-right to top-left
32 for (int row = boardSize - 1; row >= 0; row--) {
33 for (int col = boardSize - 1; col >= 0; col--) {
34 // Try to update current cell from three possible next positions
35 updateFromNextCell(row, col, row + 1, col); // Move down
36 updateFromNextCell(row, col, row, col + 1); // Move right
37 updateFromNextCell(row, col, row + 1, col + 1); // Move diagonal
38
39 // Add current cell's value to the score if it's a digit
40 if (maxScore[row][col] != -1) {
41 char currentChar = board.get(row).charAt(col);
42 if (currentChar >= '0' && currentChar <= '9') {
43 maxScore[row][col] += (currentChar - '0');
44 }
45 }
46 }
47 }
48
49 // Prepare result array
50 int[] result = new int[2];
51
52 // If start position is reachable, set the max score and path count
53 if (maxScore[0][0] != -1) {
54 result[0] = maxScore[0][0];
55 result[1] = pathCount[0][0];
56 }
57
58 return result;
59 }
60
61 /**
62 * Updates the DP values at position (currentRow, currentCol) based on
63 * the values at position (nextRow, nextCol)
64 */
65 private void updateFromNextCell(int currentRow, int currentCol,
66 int nextRow, int nextCol) {
67 // Check boundaries and validity
68 if (nextRow >= boardSize || nextCol >= boardSize) {
69 return; // Out of bounds
70 }
71
72 if (maxScore[nextRow][nextCol] == -1) {
73 return; // Next cell is unreachable
74 }
75
76 char currentChar = board.get(currentRow).charAt(currentCol);
77 if (currentChar == 'X' || currentChar == 'S') {
78 return; // Current cell is obstacle or end position
79 }
80
81 // Update DP values based on next cell
82 if (maxScore[nextRow][nextCol] > maxScore[currentRow][currentCol]) {
83 // Found a better path - update both score and count
84 maxScore[currentRow][currentCol] = maxScore[nextRow][nextCol];
85 pathCount[currentRow][currentCol] = pathCount[nextRow][nextCol];
86 } else if (maxScore[nextRow][nextCol] == maxScore[currentRow][currentCol]) {
87 // Found an equally good path - add to path count
88 pathCount[currentRow][currentCol] =
89 (pathCount[currentRow][currentCol] + pathCount[nextRow][nextCol]) % MOD;
90 }
91 }
92}
93
1class Solution {
2public:
3 vector<int> pathsWithMaxScore(vector<string>& board) {
4 int boardSize = board.size();
5 const int MOD = 1e9 + 7;
6
7 // dp[i][j] stores the maximum score from position (i,j) to end
8 int maxScore[boardSize][boardSize];
9 // pathCount[i][j] stores the number of paths achieving maximum score from (i,j) to end
10 int pathCount[boardSize][boardSize];
11
12 // Initialize arrays
13 memset(maxScore, -1, sizeof(maxScore));
14 memset(pathCount, 0, sizeof(pathCount));
15
16 // Base case: end position has score 0 and 1 path
17 maxScore[boardSize - 1][boardSize - 1] = 0;
18 pathCount[boardSize - 1][boardSize - 1] = 1;
19
20 // Lambda function to update cell (i,j) based on cell (prevX,prevY)
21 auto updateCell = [&](int i, int j, int prevX, int prevY) {
22 // Check boundaries and validity
23 if (prevX >= boardSize || prevY >= boardSize ||
24 maxScore[prevX][prevY] == -1 ||
25 board[i][j] == 'X' || board[i][j] == 'S') {
26 return;
27 }
28
29 // If path from (prevX,prevY) gives better score
30 if (maxScore[prevX][prevY] > maxScore[i][j]) {
31 maxScore[i][j] = maxScore[prevX][prevY];
32 pathCount[i][j] = pathCount[prevX][prevY];
33 }
34 // If path from (prevX,prevY) gives same score, add to path count
35 else if (maxScore[prevX][prevY] == maxScore[i][j]) {
36 pathCount[i][j] = (pathCount[i][j] + pathCount[prevX][prevY]) % MOD;
37 }
38 };
39
40 // Fill DP table from bottom-right to top-left
41 for (int i = boardSize - 1; i >= 0; --i) {
42 for (int j = boardSize - 1; j >= 0; --j) {
43 // Try all three possible moves: down, right, diagonal
44 updateCell(i, j, i + 1, j); // Move down
45 updateCell(i, j, i, j + 1); // Move right
46 updateCell(i, j, i + 1, j + 1); // Move diagonal
47
48 // Add current cell's value to the score if reachable
49 if (maxScore[i][j] != -1) {
50 if (board[i][j] >= '0' && board[i][j] <= '9') {
51 maxScore[i][j] += (board[i][j] - '0');
52 }
53 }
54 }
55 }
56
57 // Prepare result: [maximum score, number of paths]
58 vector<int> result(2);
59 if (maxScore[0][0] != -1) {
60 result[0] = maxScore[0][0];
61 result[1] = pathCount[0][0];
62 }
63
64 return result;
65 }
66};
67
1function pathsWithMaxScore(board: string[]): number[] {
2 const boardSize = board.length;
3 const MOD = 1e9 + 7;
4
5 // dp[i][j] stores the maximum score from position (i,j) to end
6 const maxScore: number[][] = Array(boardSize).fill(null).map(() =>
7 Array(boardSize).fill(-1)
8 );
9
10 // pathCount[i][j] stores the number of paths achieving maximum score from (i,j) to end
11 const pathCount: number[][] = Array(boardSize).fill(null).map(() =>
12 Array(boardSize).fill(0)
13 );
14
15 // Base case: end position has score 0 and 1 path
16 maxScore[boardSize - 1][boardSize - 1] = 0;
17 pathCount[boardSize - 1][boardSize - 1] = 1;
18
19 // Function to update cell (i,j) based on cell (prevX,prevY)
20 const updateCell = (i: number, j: number, prevX: number, prevY: number): void => {
21 // Check boundaries and validity
22 if (prevX >= boardSize || prevY >= boardSize ||
23 maxScore[prevX][prevY] === -1 ||
24 board[i][j] === 'X' || board[i][j] === 'S') {
25 return;
26 }
27
28 // If path from (prevX,prevY) gives better score
29 if (maxScore[prevX][prevY] > maxScore[i][j]) {
30 maxScore[i][j] = maxScore[prevX][prevY];
31 pathCount[i][j] = pathCount[prevX][prevY];
32 }
33 // If path from (prevX,prevY) gives same score, add to path count
34 else if (maxScore[prevX][prevY] === maxScore[i][j]) {
35 pathCount[i][j] = (pathCount[i][j] + pathCount[prevX][prevY]) % MOD;
36 }
37 };
38
39 // Fill DP table from bottom-right to top-left
40 for (let i = boardSize - 1; i >= 0; i--) {
41 for (let j = boardSize - 1; j >= 0; j--) {
42 // Try all three possible moves: down, right, diagonal
43 updateCell(i, j, i + 1, j); // Move down
44 updateCell(i, j, i, j + 1); // Move right
45 updateCell(i, j, i + 1, j + 1); // Move diagonal
46
47 // Add current cell's value to the score if reachable
48 if (maxScore[i][j] !== -1) {
49 const cellChar = board[i][j];
50 if (cellChar >= '0' && cellChar <= '9') {
51 maxScore[i][j] += parseInt(cellChar);
52 }
53 }
54 }
55 }
56
57 // Prepare result: [maximum score, number of paths]
58 const result: number[] = [0, 0];
59 if (maxScore[0][0] !== -1) {
60 result[0] = maxScore[0][0];
61 result[1] = pathCount[0][0];
62 }
63
64 return result;
65}
66
Time and Space Complexity
Time Complexity: O(n²)
The algorithm uses a nested loop structure where both loops iterate from n-1
to 0
, giving us n
iterations for each loop. For each cell (i, j)
in the grid, the update
function is called exactly 3 times (for the three possible previous positions: down, right, and diagonal). Each call to update
performs constant time operations O(1)
. Therefore, the total time complexity is O(n × n × 3 × 1) = O(n²)
.
Space Complexity: O(n²)
The algorithm creates two 2D arrays:
f
: An × n
matrix to store the maximum scores, requiringO(n²)
spaceg
: An × n
matrix to store the number of paths, requiringO(n²)
space
The total space complexity is O(n²) + O(n²) = O(n²)
, where n
is the side length of the board.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Direction Mapping
The most critical pitfall in this solution is the confusion between movement directions and DP traversal directions. The problem states you can move up, left, and up-left from any position, but the solution processes cells in reverse (from destination to start). This creates a mismatch:
- Problem statement: From current position, you can move to up, left, or up-left
- DP traversal: From destination backwards, so we need to check where we could have come FROM
The Bug: The current code checks (row+1, col)
, (row, col+1)
, and (row+1, col+1)
, which represents checking cells below, right, and diagonally down-right. This is incorrect for the given movement rules.
Solution: When processing backwards, if we're at position (i,j)
and want to know which cells could reach us following the movement rules:
- A cell at
(i+1, j)
can reach(i,j)
by moving up - A cell at
(i, j+1)
can reach(i,j)
by moving left - A cell at
(i+1, j+1)
can reach(i,j)
by moving up-left
So the current implementation is actually correct! The confusion arises from thinking about forward movement vs. backward DP traversal.
2. Integer Overflow in Path Counting
The path count can grow exponentially, leading to integer overflow if modulo operation is not applied during accumulation.
The Bug:
elif max_score[prev_row][prev_col] == max_score[curr_row][curr_col]: path_count[curr_row][curr_col] += path_count[prev_row][prev_col]
Solution: Apply modulo during accumulation to prevent overflow:
elif max_score[prev_row][prev_col] == max_score[curr_row][curr_col]: path_count[curr_row][curr_col] = (path_count[curr_row][curr_col] + path_count[prev_row][prev_col]) % MOD
3. Handling Start and End Characters
The code skips updating when encountering 'S' (start), but 'E' (end) should be treated differently.
The Bug: The condition if board[curr_row][curr_col] in "XS":
prevents proper processing of the end position 'E'.
Solution: Only skip obstacles, handle 'E' and 'S' separately:
if board[curr_row][curr_col] == 'X':
return
# Don't add score for 'E' or 'S', only for digits
if max_score[curr_row][curr_col] != -1 and board[curr_row][curr_col].isdigit():
max_score[curr_row][curr_col] += int(board[curr_row][curr_col])
4. Base Case Initialization
The base case should be set at the start position 'S' (bottom-right), not assuming it's always at [n-1][n-1]
.
Solution: Find and initialize the actual start position:
# Find start position 'S'
for i in range(board_size):
for j in range(board_size):
if board[i][j] == 'S':
max_score[i][j] = 0
path_count[i][j] = 1
break
Which data structure is used in a depth first search?
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!