1937. Maximum Number of Points with Cost - Explanation

Problem Link

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:

  • x for x >= 0.
  • -x for x < 0.

Example 1:

Input: points = [
    [1,2,3],
    [1,5,1],
    [3,1,1]
]

Output: 9

Explanation: 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: 11

Explanation: 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.length
  • n == points[r].length
  • 1 <= m, n <= 100,000
  • 1 <= m * n <= 100,000
  • 0 <= points[r][c] <= 100,000


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



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

  1. Define a recursive function dfs(r, c) that returns the maximum points obtainable from row r+1 to the last row, given we are currently at column c in row r.
  2. Base case: if r is the last row, return 0 (no more rows to process).
  3. For each column col in the next row, compute points[r+1][col] - |col - c| + dfs(r+1, col).
  4. Return the maximum over all column choices.
  5. The answer is the maximum of points[0][c] + dfs(0, c) for all starting columns c.
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 ans

Time & Space Complexity

  • Time complexity: O(nm)O(n ^ m)
  • Space complexity: O(m)O(m) for recursion stack.

Where mm is the number of rows, and nn 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

  1. Create a memoization table memo to cache results for each (r, c) pair.
  2. Define dfs(r, c) as before, but first check if the result is already cached.
  3. If cached, return it immediately.
  4. Otherwise, compute the result by trying all columns in the next row, cache it, and return.
  5. 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 ans

Time & Space Complexity

  • Time complexity: O(mn2)O(m * n ^ 2)
  • Space complexity: O(n)O(n)

Where mm is the number of rows, and nn 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 is c - c', so we want max(dp[c'] + c') for all c' <= c.
  • If c' > c: the penalty is c' - c, so we want max(dp[c'] - c') for all c' > 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

  1. Initialize dp with the first row's values.
  2. For each subsequent row:
    • Build left[c] = max over all c' <= c of dp[c']. Propagate left-to-right, subtracting 1 at each step.
    • Build right[c] = max over all c' >= c of dp[c']. Propagate right-to-left, subtracting 1 at each step.
    • For each column c, set nextDp[c] = points[r][c] + max(left[c], right[c]).
  3. 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: O(mn)O(m * n)
  • Space complexity: O(n)O(n)

Where mm is the number of rows, and nn 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

  1. Initialize prev with the first row's values.
  2. For each subsequent row:
    • Create a cur array 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, update cur[c] = max(cur[c], rightMax) + points[r][c], then update rightMax = max(prev[c], rightMax - 1).
    • Handle the last column separately since it was not processed in the right-to-left loop.
    • Set prev = cur for the next iteration.
  3. 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: O(mn)O(m * n)
  • Space complexity: O(n)O(n)

Where mm is the number of rows, and nn 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]).