1937. Maximum Number of Points with Cost - Explanation
Description
You are given an m x n integer matrix points (0-indexed). Starting with 0 points, you want to maximize the number of points you can get from the matrix.
To gain points, you must pick one cell in each row. Picking the cell at coordinates (r, c) will add points[r][c] to your score.
However, you will lose points if you pick a cell too far from the cell that you picked in the previous row. For every two adjacent rows r and r + 1 (where 0 <= r < m - 1), picking cells at coordinates (r, c1) and (r + 1, c2) will subtract abs(c1 - c2) from your score.
Return the maximum number of points you can achieve.
abs(x) is defined as:
xforx >= 0.-xforx < 0.
Example 1:
Input: points = [
[1,2,3],
[1,5,1],
[3,1,1]
]
Output: 9Explanation: The optimal cells to pick are (0, 2), (1, 1), and (2, 0).
You add 3 + 5 + 3 = 11 to your score.
However, you must subtract abs(2 - 1) + abs(1 - 0) = 2 from your score.
Your final score is 11 - 2 = 9.
Example 2:
Input: points = [
[1,5],
[2,3],
[4,2]
]
Output: 11Explanation: The optimal cells to pick are (0, 1), (1, 1), and (2, 0).
You add 5 + 3 + 4 = 12 to your score.
However, you must subtract abs(1 - 1) + abs(1 - 0) = 1 from your score.
Your final score is 12 - 1 = 11.
Constraints:
m == points.lengthn == points[r].length1 <= m, n <= 100,0001 <= m * n <= 100,0000 <= points[r][c] <= 100,000
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- Dynamic Programming - Building solutions from overlapping subproblems with optimal substructure
- Memoization - Caching recursive results to avoid redundant computation
- Prefix/Suffix Maximum Arrays - Precomputing running maximums from left-to-right and right-to-left
- 2D Array Traversal - Processing matrices row by row with state transitions
1. Recursion
Intuition
We need to pick one cell from each row such that the total points are maximized. The tricky part is the penalty: if we pick column c in row r and column c' in row r+1, we lose |c - c'| points.
A natural approach is to try all possibilities. For each starting column in the first row, we recursively explore all column choices in subsequent rows, subtracting the movement penalty and adding the cell value. We return the maximum sum found.
Algorithm
- Define a recursive function
dfs(r, c)that returns the maximum points obtainable from rowr+1to the last row, given we are currently at columncin rowr. - Base case: if
ris the last row, return0(no more rows to process). - For each column
colin the next row, computepoints[r+1][col] - |col - c| + dfs(r+1, col). - Return the maximum over all column choices.
- The answer is the maximum of
points[0][c] + dfs(0, c)for all starting columnsc.
class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
m, n = len(points), len(points[0])
def dfs(r, c):
if r == m - 1:
return 0
res = 0
for col in range(n):
res = max(res, points[r + 1][col] - abs(col - c) + dfs(r + 1, col))
return res
ans = 0
for c in range(n):
ans = max(ans, points[0][c] + dfs(0, c))
return ansTime & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
Where is the number of rows, and is the number of columns.
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution recalculates the same subproblems multiple times. For a given (row, column) pair, the maximum points from that position onward is always the same, regardless of how we got there.
By storing (memoizing) the result for each (r, c) pair, we avoid redundant computation. This transforms the exponential solution into a polynomial one.
Algorithm
- Create a memoization table
memoto cache results for each(r, c)pair. - Define
dfs(r, c)as before, but first check if the result is already cached. - If cached, return it immediately.
- Otherwise, compute the result by trying all columns in the next row, cache it, and return.
- The final answer is computed the same way as the recursive approach.
class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
m, n = len(points), len(points[0])
memo = {}
def dfs(r, c):
if (r, c) in memo:
return memo[(r, c)]
if r == m - 1:
return 0
res = 0
for col in range(n):
res = max(res, points[r + 1][col] - abs(col - c) + dfs(r + 1, col))
memo[(r, c)] = res
return res
ans = 0
for c in range(n):
ans = max(ans, points[0][c] + dfs(0, c))
return ansTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows, and is the number of columns.
3. Dynamic Programming (Bottom-Up)
Intuition
Computing max(dp[c'] - |c - c'|) for each column c normally takes O(n) time per cell, making the total O(m * n^2). We can optimize this using prefix maximums.
The penalty |c - c'| splits into two cases:
- If
c' <= c: the penalty isc - c', so we wantmax(dp[c'] + c')for allc' <= c. - If
c' > c: the penalty isc' - c, so we wantmax(dp[c'] - c')for allc' > c.
By precomputing a left array (max of dp[c'] + c' from the left) and a right array (max of dp[c'] - c' from the right), we can answer each column's query in O(1) time.
Algorithm
- Initialize
dpwith the first row's values. - For each subsequent row:
- Build
left[c]= max over allc' <= cofdp[c']. Propagate left-to-right, subtracting1at each step. - Build
right[c]= max over allc' >= cofdp[c']. Propagate right-to-left, subtracting1at each step. - For each column
c, setnextDp[c] = points[r][c] + max(left[c], right[c]).
- Build
- After processing all rows, return the maximum value in
dp.
class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
ROWS, COLS = len(points), len(points[0])
dp = points[0]
for r in range(1, ROWS):
left = [0] * COLS
left[0] = dp[0]
for c in range(1, COLS):
left[c] = max(dp[c], left[c - 1] - 1)
right = [0] * COLS
right[COLS - 1] = dp[COLS - 1]
for c in range(COLS - 2, -1, -1):
right[c] = max(dp[c], right[c + 1] - 1)
nextDp = points[r][:]
for c in range(COLS):
nextDp[c] += max(left[c], right[c])
dp = nextDp
return max(dp)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows, and is the number of columns.
4. Dynamic Programming (Space Optimized)
Intuition
We can reduce memory usage by combining the left and right sweeps into a single pass. Instead of storing separate left and right arrays, we build the left maximum in a cur array during the left-to-right pass. Then, during the right-to-left pass, we update cur[c] by taking the max of the current left value and the running right maximum, adding the cell value as we go.
This avoids allocating a separate right array, reducing the constant factor in space usage.
Algorithm
- Initialize
prevwith the first row's values. - For each subsequent row:
- Create a
curarray and build left maximums by iterating left-to-right:cur[c] = max(prev[c], cur[c-1] - 1). - Iterate right-to-left, tracking
rightMax. For each column, updatecur[c] = max(cur[c], rightMax) + points[r][c], then updaterightMax = max(prev[c], rightMax - 1). - Handle the last column separately since it was not processed in the right-to-left loop.
- Set
prev = curfor the next iteration.
- Create a
- Return the maximum value in
prev.
class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
ROWS, COLS = len(points), len(points[0])
prev = points[0]
for r in range(1, ROWS):
cur = [0] * COLS
cur[0] = prev[0]
for c in range(1, COLS):
cur[c] = max(prev[c], cur[c - 1] - 1)
rightMax = prev[COLS - 1]
for c in range(COLS - 2, -1 , -1):
rightMax = max(prev[c], rightMax - 1)
cur[c] = max(cur[c], rightMax) + points[r][c]
cur[COLS - 1] += points[r][COLS - 1]
prev = cur
return max(prev)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of rows, and is the number of columns.
Common Pitfalls
Integer Overflow
With grid dimensions up to 10^5 and cell values up to 10^5, the maximum total points can exceed the 32-bit integer limit. Using int instead of long (or equivalent 64-bit type) will cause overflow and incorrect results.
Missing One Direction in Prefix Maximum
The optimized solution requires computing both left-to-right and right-to-left prefix maximums. Forgetting one direction or incorrectly combining them will miss optimal transitions from certain columns, leading to suboptimal answers.
Off-by-One in Penalty Propagation
When propagating the prefix maximum, the penalty decreases by 1 for each column of distance. A common error is applying the penalty incorrectly, such as subtracting the penalty before comparing or not decrementing for each step. The update should be left[c] = max(dp[c], left[c-1] - 1), not left[c] = max(dp[c] - 1, left[c-1]).
Sign in to join the discussion