1937. Maximum Number of Points with Cost
Problem Description
You have an m x n
matrix of integers called points
. Your goal is to collect the maximum possible score by selecting exactly one cell from each row.
The scoring works as follows:
- When you pick a cell at position
(r, c)
, you gainpoints[r][c]
points - However, there's a penalty for picking cells that are far apart horizontally between consecutive rows
- If you pick cell at column
c1
in rowr
and cell at columnc2
in rowr+1
, you loseabs(c1 - c2)
points
Your task is to find the maximum total score possible.
For example, with the matrix:
[[1,2,3], [1,5,1], [3,1,1]]
If you pick cells at positions (0,2), (1,1), and (2,0), you would:
- Gain: 3 + 5 + 3 = 11 points from the cell values
- Lose: |2-1| + |1-0| = 1 + 1 = 2 points from column differences
- Final score: 11 - 2 = 9
The challenge is to find the optimal path through the matrix that maximizes your final score after accounting for both the cell values and the penalties for horizontal movement between rows.
Intuition
This problem screams dynamic programming because we need to make optimal choices row by row, and our decision at each row depends on what we chose in the previous row.
Let's think about it step by step. If we define f[j]
as the maximum score we can get ending at column j
in the current row, then for the next row, we need to consider all possible previous columns we could have come from.
The naive approach would be: for each cell (r, c2)
in the current row, check all possible previous columns c1
and calculate f[c1] + points[r][c2] - abs(c1 - c2)
. But this gives us O(n²) time per row, which is too slow given the constraints.
The key insight is recognizing that the penalty term abs(c1 - c2)
can be split based on whether we're coming from the left or right:
- If coming from left (c1 ≤ c2): penalty is
c2 - c1
- If coming from right (c1 > c2): penalty is
c1 - c2
This allows us to rewrite the transition as:
- From left:
f[c1] + points[r][c2] - (c2 - c1) = (f[c1] + c1) + points[r][c2] - c2
- From right:
f[c1] + points[r][c2] - (c1 - c2) = (f[c1] - c1) + points[r][c2] + c2
Notice how we can separate the terms that depend on c1
from those that depend on c2
. This means:
- For transitions from the left, we want to maximize
f[c1] + c1
for allc1 ≤ c2
- For transitions from the right, we want to maximize
f[c1] - c1
for allc1 > c2
We can compute these maximum values efficiently by doing two passes:
- Left-to-right pass: maintain the running maximum of
f[j] + j
- Right-to-left pass: maintain the running maximum of
f[j] - j
This optimization reduces our time complexity from O(mn²) to O(mn), making the solution efficient enough for the given constraints.
Learn more about Dynamic Programming patterns.
Solution Approach
The implementation uses dynamic programming with space optimization, storing only the previous row's results instead of the entire DP table.
Initial Setup:
n = len(points[0])
: Get the number of columnsf = points[0][:]
: Initializef
with the first row's values (no previous row to consider penalties)
Processing Each Subsequent Row:
For each row starting from the second one (points[1:]
), we calculate the new DP values in array g
:
1. Left-to-Right Pass:
lmx = -inf
for j in range(n):
lmx = max(lmx, f[j] + j)
g[j] = max(g[j], p[j] + lmx - j)
lmx
tracks the maximum value off[j] + j
seen so far from the left- For each column
j
, we compute the best score if we came from any column to the left or the current column - The formula
p[j] + lmx - j
represents:p[j]
: points gained at current celllmx
: best value of(previous_score + previous_column)
-j
: subtract current column (part of the distance penalty)
2. Right-to-Left Pass:
rmx = -inf
for j in range(n - 1, -1, -1):
rmx = max(rmx, f[j] - j)
g[j] = max(g[j], p[j] + rmx + j)
rmx
tracks the maximum value off[j] - j
seen so far from the right- For each column
j
, we compute the best score if we came from any column to the right - The formula
p[j] + rmx + j
represents:p[j]
: points gained at current cellrmx
: best value of(previous_score - previous_column)
+j
: add current column (part of the distance penalty when coming from right)
3. Update for Next Iteration:
f = g
After processing both passes, g
contains the maximum scores for the current row, which becomes f
for the next iteration.
Final Result:
return max(f)
After processing all rows, return the maximum value in f
, which represents the best score achievable ending at any column in the last row.
Time Complexity: O(m × n) where m is the number of rows and n is the number of columns
Space Complexity: O(n) for storing the DP arrays f
and g
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.
Given matrix:
[[2, 1, 4], [3, 5, 2]]
Step 1: Initialize with first row
f = [2, 1, 4]
(no penalties since it's the first row)
Step 2: Process second row [3, 5, 2]
- Create
g = [-inf, -inf, -inf]
to store new values
Left-to-Right Pass:
j = 0
:lmx = max(-inf, f[0] + 0) = max(-inf, 2 + 0) = 2
g[0] = max(-inf, 3 + 2 - 0) = 5
j = 1
:lmx = max(2, f[1] + 1) = max(2, 1 + 1) = 2
g[1] = max(-inf, 5 + 2 - 1) = 6
j = 2
:lmx = max(2, f[2] + 2) = max(2, 4 + 2) = 6
g[2] = max(-inf, 2 + 6 - 2) = 6
After left pass: g = [5, 6, 6]
Right-to-Left Pass:
j = 2
:rmx = max(-inf, f[2] - 2) = max(-inf, 4 - 2) = 2
g[2] = max(6, 2 + 2 + 2) = max(6, 6) = 6
j = 1
:rmx = max(2, f[1] - 1) = max(2, 1 - 1) = 2
g[1] = max(6, 5 + 2 + 1) = max(6, 8) = 8
j = 0
:rmx = max(2, f[0] - 0) = max(2, 2 - 0) = 2
g[0] = max(5, 3 + 2 + 0) = max(5, 5) = 5
After right pass: g = [5, 8, 6]
Step 3: Final result
f = [5, 8, 6]
- Maximum score = 8
This corresponds to choosing:
- Row 0, Column 2: gain 4 points
- Row 1, Column 1: gain 5 points
- Penalty: |2 - 1| = 1
- Total: 4 + 5 - 1 = 8
The algorithm efficiently computes the optimal path by considering transitions from both left and right directions separately, avoiding the need to check all possible previous columns individually.
Solution Implementation
1class Solution:
2 def maxPoints(self, points: List[List[int]]) -> int:
3 num_cols = len(points[0])
4 # Initialize dp array with first row values
5 prev_row_max = points[0][:]
6
7 # Process each row starting from the second row
8 for current_row_points in points[1:]:
9 # Array to store maximum points for current row
10 current_row_max = [0] * num_cols
11
12 # Left to right pass: consider coming from left positions
13 # For position j, we can come from any position i where i <= j
14 # The cost is prev_row_max[i] + current_row_points[j] - abs(i - j)
15 # Since i <= j, abs(i - j) = j - i
16 # So cost = prev_row_max[i] + i + current_row_points[j] - j
17 left_max = float('-inf')
18 for col in range(num_cols):
19 # Update the maximum of (prev_row_max[i] + i) for all i <= col
20 left_max = max(left_max, prev_row_max[col] + col)
21 # Calculate max points coming from left
22 current_row_max[col] = max(current_row_max[col],
23 current_row_points[col] + left_max - col)
24
25 # Right to left pass: consider coming from right positions
26 # For position j, we can come from any position i where i >= j
27 # The cost is prev_row_max[i] + current_row_points[j] - abs(i - j)
28 # Since i >= j, abs(i - j) = i - j
29 # So cost = prev_row_max[i] - i + current_row_points[j] + j
30 right_max = float('-inf')
31 for col in range(num_cols - 1, -1, -1):
32 # Update the maximum of (prev_row_max[i] - i) for all i >= col
33 right_max = max(right_max, prev_row_max[col] - col)
34 # Calculate max points coming from right
35 current_row_max[col] = max(current_row_max[col],
36 current_row_points[col] + right_max + col)
37
38 # Update previous row max for next iteration
39 prev_row_max = current_row_max
40
41 # Return the maximum points achievable in the last row
42 return max(prev_row_max)
43
1class Solution {
2 public long maxPoints(int[][] points) {
3 int numColumns = points[0].length;
4
5 // dp[j] represents the maximum points we can get ending at column j in the previous row
6 long[] previousRowMax = new long[numColumns];
7 final long NEGATIVE_INFINITY = -(1L << 60);
8
9 // Process each row
10 for (int[] currentRow : points) {
11 // currentRowMax[j] will store the maximum points we can get ending at column j in current row
12 long[] currentRowMax = new long[numColumns];
13
14 // Left-to-right pass: consider coming from left or same position
15 // For position j, we can come from any position k where k <= j
16 // The cost is: previousRowMax[k] + currentRow[j] - |k - j|
17 // Since k <= j, |k - j| = j - k
18 // So cost = previousRowMax[k] + currentRow[j] - j + k
19 // = currentRow[j] - j + (previousRowMax[k] + k)
20 // We maximize (previousRowMax[k] + k) for all k <= j
21 long leftMax = NEGATIVE_INFINITY;
22 for (int j = 0; j < numColumns; j++) {
23 // Update the maximum value of (previousRowMax[k] + k) for k <= j
24 leftMax = Math.max(leftMax, previousRowMax[j] + j);
25 // Calculate points if we come from the left side (or same column)
26 currentRowMax[j] = Math.max(currentRowMax[j], currentRow[j] + leftMax - j);
27 }
28
29 // Right-to-left pass: consider coming from right
30 // For position j, we can come from any position k where k >= j
31 // The cost is: previousRowMax[k] + currentRow[j] - |k - j|
32 // Since k >= j, |k - j| = k - j
33 // So cost = previousRowMax[k] + currentRow[j] - k + j
34 // = currentRow[j] + j + (previousRowMax[k] - k)
35 // We maximize (previousRowMax[k] - k) for all k >= j
36 long rightMax = NEGATIVE_INFINITY;
37 for (int j = numColumns - 1; j >= 0; j--) {
38 // Update the maximum value of (previousRowMax[k] - k) for k >= j
39 rightMax = Math.max(rightMax, previousRowMax[j] - j);
40 // Calculate points if we come from the right side
41 // Take the maximum between coming from left and coming from right
42 currentRowMax[j] = Math.max(currentRowMax[j], currentRow[j] + rightMax + j);
43 }
44
45 // Move to the next row
46 previousRowMax = currentRowMax;
47 }
48
49 // Find the maximum points in the last row
50 long maxPoints = 0;
51 for (long points : previousRowMax) {
52 maxPoints = Math.max(maxPoints, points);
53 }
54
55 return maxPoints;
56 }
57}
58
1class Solution {
2public:
3 long long maxPoints(vector<vector<int>>& points) {
4 using ll = long long;
5
6 int numCols = points[0].size();
7
8 // dp[j] represents the maximum points we can get ending at column j in the current row
9 vector<ll> dp(numCols, 0);
10
11 const ll INF = 1e18;
12
13 // Process each row
14 for (auto& currentRow : points) {
15 // newDp[j] will store the maximum points for the current row ending at column j
16 vector<ll> newDp(numCols, 0);
17
18 // leftMax tracks the maximum value of (dp[k] + k) for all k from 0 to j
19 // This helps calculate transitions from left positions efficiently
20 ll leftMax = -INF;
21
22 // Process transitions from left to right
23 // For position j, we consider all positions k <= j from the previous row
24 // The score would be: dp[k] + currentRow[j] - abs(k - j)
25 // When k <= j: abs(k - j) = j - k
26 // So the score becomes: dp[k] + currentRow[j] - j + k = (dp[k] + k) + currentRow[j] - j
27 for (int j = 0; j < numCols; ++j) {
28 leftMax = max(leftMax, dp[j] + j);
29 newDp[j] = max(newDp[j], currentRow[j] + leftMax - j);
30 }
31
32 // rightMax tracks the maximum value of (dp[k] - k) for all k from j to n-1
33 // This helps calculate transitions from right positions efficiently
34 ll rightMax = -INF;
35
36 // Process transitions from right to left
37 // For position j, we consider all positions k >= j from the previous row
38 // When k >= j: abs(k - j) = k - j
39 // So the score becomes: dp[k] + currentRow[j] - k + j = (dp[k] - k) + currentRow[j] + j
40 for (int j = numCols - 1; j >= 0; --j) {
41 rightMax = max(rightMax, dp[j] - j);
42 newDp[j] = max(newDp[j], currentRow[j] + rightMax + j);
43 }
44
45 // Move to the next row by updating dp with the computed values
46 dp = move(newDp);
47 }
48
49 // Return the maximum points achievable in the last row
50 return *max_element(dp.begin(), dp.end());
51 }
52};
53
1function maxPoints(points: number[][]): number {
2 // Get the number of columns
3 const numColumns = points[0].length;
4
5 // Initialize dp array to store maximum points for previous row
6 // dp[j] represents the maximum points we can get ending at column j
7 let previousRowMax: number[] = new Array(numColumns).fill(0);
8
9 // Process each row of points
10 for (const currentRow of points) {
11 // Initialize dp array for current row
12 const currentRowMax: number[] = new Array(numColumns).fill(0);
13
14 // Left to right pass: consider moving from left columns
15 // We want to maximize: previousRowMax[k] + currentRow[j] - |k - j|
16 // When moving from left (k <= j): previousRowMax[k] + currentRow[j] - (j - k)
17 // Which equals: (previousRowMax[k] + k) + currentRow[j] - j
18 let leftMax = -Infinity;
19 for (let j = 0; j < numColumns; ++j) {
20 // Update leftMax to track the maximum of (previousRowMax[k] + k) for all k <= j
21 leftMax = Math.max(leftMax, previousRowMax[j] + j);
22 // Calculate maximum points for position j considering moves from left
23 currentRowMax[j] = Math.max(currentRowMax[j], currentRow[j] + leftMax - j);
24 }
25
26 // Right to left pass: consider moving from right columns
27 // When moving from right (k >= j): previousRowMax[k] + currentRow[j] - (k - j)
28 // Which equals: (previousRowMax[k] - k) + currentRow[j] + j
29 let rightMax = -Infinity;
30 for (let j = numColumns - 1; j >= 0; --j) {
31 // Update rightMax to track the maximum of (previousRowMax[k] - k) for all k >= j
32 rightMax = Math.max(rightMax, previousRowMax[j] - j);
33 // Calculate maximum points for position j considering moves from right
34 currentRowMax[j] = Math.max(currentRowMax[j], currentRow[j] + rightMax + j);
35 }
36
37 // Update previousRowMax with current row's results
38 previousRowMax.splice(0, numColumns, ...currentRowMax);
39 }
40
41 // Return the maximum points achievable at any column in the last row
42 return Math.max(...previousRowMax);
43}
44
Time and Space Complexity
Time Complexity: O(m * n)
where m
is the number of rows and n
is the number of columns in the points matrix.
The algorithm iterates through each row of the points matrix (excluding the first row which is used for initialization), resulting in m - 1
iterations. For each row, it performs two passes through all columns:
- First pass from left to right:
O(n)
operations - Second pass from right to left:
O(n)
operations
Since we have m - 1
rows and each row requires 2n
operations, the total time complexity is O((m - 1) * 2n) = O(m * n)
.
Space Complexity: O(n)
where n
is the number of columns.
The algorithm uses:
- Array
f
to store the maximum points for the previous row:O(n)
space - Array
g
to store the maximum points for the current row:O(n)
space - Variables
lmx
andrmx
for tracking maximum values:O(1)
space
The total auxiliary space used is O(n) + O(n) + O(1) = O(n)
. Note that we're not counting the input array in the space complexity analysis, only the additional space used by the algorithm.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Distance Penalty Calculation
A common mistake is misunderstanding how the absolute distance penalty works when transitioning between columns in consecutive rows. Developers often incorrectly apply the penalty formula or confuse when to add vs. subtract the column indices.
Pitfall Example:
# WRONG: Incorrectly calculating penalty
for j in range(n):
for prev_j in range(n):
# This naive approach might use wrong signs or formula
current_row_max[j] = max(current_row_max[j],
prev_row_max[prev_j] + current_row_points[j] - (j - prev_j))
Why it's wrong: The absolute value abs(prev_j - j)
needs careful handling. When prev_j < j
, the penalty is j - prev_j
, but when prev_j > j
, it's prev_j - j
. The naive subtraction (j - prev_j)
doesn't account for this.
Solution: Separate the calculation into two passes:
- Left-to-right pass: Handle transitions from columns to the left (where
prev_col <= current_col
) - Right-to-left pass: Handle transitions from columns to the right (where
prev_col >= current_col
)
This way, you can algebraically manipulate the formula to avoid explicitly calculating absolute values for each pair.
2. Not Initializing the Current Row Array Properly
Pitfall Example:
# WRONG: Reusing the same array reference current_row_max = prev_row_max # This creates a reference, not a copy!
Why it's wrong: This doesn't create a new array but rather points to the same array in memory. Modifications to current_row_max
will affect prev_row_max
, leading to incorrect calculations.
Solution: Always create a fresh array for each row:
current_row_max = [0] * num_cols # Create new array
# OR
current_row_max = [float('-inf')] * num_cols # If you want to be explicit about initialization
3. Attempting O(n²) Solution Without Optimization
Pitfall Example:
# WRONG: Brute force O(n²) per row - will cause Time Limit Exceeded for large inputs
for current_col in range(num_cols):
for prev_col in range(num_cols):
current_row_max[current_col] = max(
current_row_max[current_col],
prev_row_max[prev_col] + current_row_points[current_col] - abs(prev_col - current_col)
)
Why it's wrong: While logically correct, this has O(m × n²) time complexity, which is too slow for large matrices.
Solution: Use the two-pass optimization technique that reduces the inner loop from O(n²) to O(n) by maintaining running maximums (left_max
and right_max
) that efficiently track the best previous column choice.
What does the following code do?
1def f(arr1, arr2):
2 i, j = 0, 0
3 new_arr = []
4 while i < len(arr1) and j < len(arr2):
5 if arr1[i] < arr2[j]:
6 new_arr.append(arr1[i])
7 i += 1
8 else:
9 new_arr.append(arr2[j])
10 j += 1
11 new_arr.extend(arr1[i:])
12 new_arr.extend(arr2[j:])
13 return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2 int i = 0, j = 0;
3 List<Integer> newArr = new ArrayList<>();
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.add(arr1[i]);
8 i++;
9 } else {
10 newArr.add(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.add(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.add(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
1function f(arr1, arr2) {
2 let i = 0, j = 0;
3 let newArr = [];
4
5 while (i < arr1.length && j < arr2.length) {
6 if (arr1[i] < arr2[j]) {
7 newArr.push(arr1[i]);
8 i++;
9 } else {
10 newArr.push(arr2[j]);
11 j++;
12 }
13 }
14
15 while (i < arr1.length) {
16 newArr.push(arr1[i]);
17 i++;
18 }
19
20 while (j < arr2.length) {
21 newArr.push(arr2[j]);
22 j++;
23 }
24
25 return newArr;
26}
27
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!