64. Minimum Path Sum - Explanation

Problem Link

Description

You are given a m x n grid filled with non-negative numbers, find a path from top left to bottom right, which minimizes the sum of all numbers along its path.

Note: You can only move either down or right at any point in time.

Example 1:

Input: grid = [
    [1,2,0],
    [5,4,2],
    [1,1,3]
]

Output: 8

Explanation: The path with minimum sum is 1 -> 2 -> 0 -> 2 -> 3.

Example 2:

Input: grid = [
    [2,2],
    [1,0]
]

Output: 3

Constraints:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 200


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Recursion - The brute force solution explores all paths using recursive calls
  • Dynamic Programming - Both top-down (memoization) and bottom-up (tabulation) approaches are used
  • 2D Grid Traversal - Understanding how to navigate a grid moving only right or down
  • Space Optimization - The optimal solution reduces 2D DP to 1D by reusing a single array

1. Recursion

Intuition

From any cell, we can only move right or down. To find the minimum path sum to the bottom-right corner, we consider both choices and take the minimum. At the destination cell, we simply return its value. This naturally leads to a recursive solution where we explore all possible paths by branching at each cell.

Algorithm

  1. Define a recursive function dfs(r, c) that returns the minimum path sum from cell (r, c) to the bottom-right.
  2. Base case: if we reach the bottom-right cell, return grid[r][c].
  3. If we go out of bounds (past the last row or column), return infinity to indicate an invalid path.
  4. For each cell, return grid[r][c] + min(dfs(r+1, c), dfs(r, c+1)).
  5. Start the recursion from dfs(0, 0).
class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        def dfs(r, c):
            if r == len(grid) - 1 and c == len(grid[0]) - 1:
                return grid[r][c]
            if r == len(grid) or c == len(grid[0]):
                return float('inf')
            return grid[r][c] + min(dfs(r + 1, c), dfs(r, c + 1))

        return dfs(0, 0)

Time & Space Complexity

  • Time complexity: O(2m+n)O(2 ^ {m + n})
  • Space complexity: O(m+n)O(m + n) for recursion stack.

Where mm is the number of rows and nn is the number of columns.


2. Dynamic Programming (Top-Down)

Intuition

The plain recursive solution recalculates the same cells many times. For instance, both dfs(0,1) and dfs(1,0) might call dfs(1,1). By caching (memoizing) results for each cell, we ensure each subproblem is solved only once. This transforms exponential time into polynomial time.

Algorithm

  1. Create a 2D memoization table dp initialized to -1 (indicating uncomputed).
  2. Define dfs(r, c) as before, but first check if dp[r][c] has been computed.
  3. If dp[r][c] != -1, return the cached value.
  4. Otherwise, compute dp[r][c] = grid[r][c] + min(dfs(r+1, c), dfs(r, c+1)) and return it.
  5. Return dfs(0, 0).
class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        dp = [[-1] * n for _ in range(m)]

        def dfs(r, c):
            if r == m - 1 and c == n - 1:
                return grid[r][c]
            if r == m or c == n:
                return float('inf')
            if dp[r][c] != -1:
                return dp[r][c]

            dp[r][c] = grid[r][c] + min(dfs(r + 1, c), dfs(r, c + 1))
            return dp[r][c]

        return dfs(0, 0)

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns.


3. Dynamic Programming (Bottom-Up)

Intuition

Instead of recursing from top-left to bottom-right, we can fill a DP table starting from the bottom-right corner. For each cell, we know the minimum path sum to reach the destination from the cell below and from the cell to the right. We take the minimum of these two and add the current cell value. This iterative approach avoids recursion overhead.

Algorithm

  1. Create a DP table of size (ROWS+1) x (COLS+1) initialized to infinity. The extra row and column handle boundary conditions.
  2. Set dp[ROWS-1][COLS] = 0 as the base case (one position past the destination, allowing the destination cell to be computed correctly).
  3. Iterate from bottom-right to top-left: for each cell (r, c), set dp[r][c] = grid[r][c] + min(dp[r+1][c], dp[r][c+1]).
  4. Return dp[0][0].
class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        ROWS, COLS = len(grid), len(grid[0])
        dp = [[float("inf")] * (COLS + 1) for _ in range(ROWS + 1)]
        dp[ROWS - 1][COLS] = 0

        for r in range(ROWS - 1, -1, -1):
            for c in range(COLS - 1, -1, -1):
                dp[r][c] = grid[r][c] + min(dp[r + 1][c], dp[r][c + 1])

        return dp[0][0]

Time & Space Complexity

  • Time complexity: O(mn)O(m * n)
  • Space complexity: O(mn)O(m * n)

Where mm is the number of rows and nn is the number of columns.


4. Dynamic Programming (Space Optimized)

Intuition

When filling the DP table row by row (from bottom to top), we only need the current row and the row below. In fact, since we process columns right to left, we can overwrite the same 1D array. The value at dp[c] represents the minimum path sum from (r+1, c), and dp[c+1] represents the path from (r, c+1). After updating, dp[c] will hold the result for (r, c).

Algorithm

  1. Create a 1D array dp of size COLS+1 initialized to infinity.
  2. Set dp[COLS-1] = 0 as the base case.
  3. For each row from bottom to top:
    • For each column from right to left: dp[c] = grid[r][c] + min(dp[c], dp[c+1]).
  4. Return dp[0].
class Solution:
    def minPathSum(self, grid: List[List[int]]) -> int:
        ROWS, COLS = len(grid), len(grid[0])
        dp = [float("inf")] * (COLS + 1)
        dp[COLS - 1] = 0

        for r in range(ROWS - 1, -1, -1):
            for c in range(COLS - 1, -1, -1):
                dp[c] = grid[r][c] + min(dp[c], dp[c + 1])

        return dp[0]

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

Incorrect Boundary Initialization

When using bottom-up DP, the extra row and column must be initialized to infinity (or a very large value), except for one cell that serves as the base case. Initializing boundaries to 0 instead of infinity causes the algorithm to prefer going out of bounds, producing incorrect minimum sums.

Integer Overflow with MAX_VALUE

When adding grid[r][c] to Integer.MAX_VALUE (or equivalent), the result overflows to a negative number, which then incorrectly becomes the minimum. Use a large but safe value like 1 << 30 instead, or add overflow checks before the addition.

Wrong Iteration Direction in Space-Optimized Solution

In the 1D DP optimization, the iteration direction matters. When processing from bottom-right to top-left, you must iterate columns right-to-left within each row. Iterating left-to-right overwrites values before they are used, corrupting the DP state and producing wrong answers.