931. Minimum Falling Path Sum - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Dynamic Programming Fundamentals - Understanding how to break problems into overlapping subproblems and use memoization or tabulation
  • 2D Grid/Matrix Traversal - Ability to navigate and process elements in a 2D array with boundary checks
  • Recursion with Memoization - Converting recursive solutions to top-down DP by caching computed results

1. Recursion

Intuition

A falling path starts at any cell in the first row and moves to an adjacent cell in the next row (directly below, diagonally left, or diagonally right). We want to find the path with the minimum sum. For each starting column in the first row, we recursively explore all valid paths and track the minimum total. This brute force approach considers all possible paths, leading to exponential time complexity.

Algorithm

  1. Define dfs(r, c) that returns the minimum path sum starting from cell (r, c) to the bottom row.
  2. Base cases:
    • If r == n, we've gone past the last row, return 0.
    • If c is out of bounds, return Infinity (invalid path).
  3. Return matrix[r][c] plus the minimum of dfs(r+1, c-1), dfs(r+1, c), and dfs(r+1, c+1).
  4. Try starting from each column in row 0 and return the minimum result.
class Solution:
    def minFallingPathSum(self, matrix: List[List[int]]) -> int:
        N = len(matrix)

        def dfs(r, c):
            if r == N:
                return 0
            if c < 0 or c >= N:
                return float("inf")
            return matrix[r][c] + min(
                dfs(r + 1, c - 1),
                dfs(r + 1, c),
                dfs(r + 1, c + 1)
            )

        res = float("inf")
        for c in range(N):
            res = min(res, dfs(0, c))
        return res

Time & Space Complexity

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

2. Dynamic Programming (Top-Down)

Intuition

The recursive solution has overlapping subproblems: the same (r, c) state is computed multiple times. By caching results in a memoization table, we avoid redundant calculations. Each unique (r, c) pair is computed only once, reducing time complexity from exponential to polynomial.

Algorithm

  1. Create a cache (dictionary or 2D array) to store computed results.
  2. Define dfs(r, c) that returns the minimum path sum from (r, c) to the bottom.
  3. Before computing, check if (r, c) is already in the cache; if so, return the cached value.
  4. Compute the result as matrix[r][c] plus the minimum of the three possible moves.
  5. Store the result in the cache and return it.
  6. Return the minimum among all starting columns in row 0.
class Solution:
    def minFallingPathSum(self, matrix: List[List[int]]) -> int:
        N = len(matrix)
        cache = {}

        def dfs(r, c):
            if r == N:
                return 0
            if c < 0 or c >= N:
                return float("inf")
            if (r, c) in cache:
                return cache[(r, c)]
            cache[(r, c)] = matrix[r][c] + min(
                dfs(r + 1, c - 1),
                dfs(r + 1, c),
                dfs(r + 1, c + 1)
            )
            return cache[(r, c)]

        res = float("inf")
        for c in range(N):
            res = min(res, dfs(0, c))
        return res

Time & Space Complexity

  • Time complexity: O(nn)O(n * n)
  • Space complexity: O(nn)O(n * n)

3. Dynamic Programming (Bottom-Up)

Intuition

We can solve this iteratively by building up solutions row by row. For each cell, the minimum path sum to reach it equals its value plus the minimum of the three cells above it that could lead here. By processing rows from top to bottom and only keeping the previous row's values, we achieve O(n) space complexity.

Algorithm

  1. Initialize a 1D dp array with the first row of the matrix.
  2. For each subsequent row r:
    • Track leftUp (the previous row's value to the left) to avoid overwriting issues.
    • For each column c:
      • midUp = dp[c] (directly above).
      • rightUp = dp[c+1] if within bounds, else infinity.
      • Update dp[c] = matrix[r][c] + min(leftUp, midUp, rightUp).
      • Set leftUp = midUp for the next iteration.
  3. Return the minimum value in the final dp array.
class Solution:
    def minFallingPathSum(self, matrix: List[List[int]]) -> int:
        N = len(matrix)
        dp = matrix[0][:]

        for r in range(1, N):
            leftUp = float('inf')
            for c in range(N):
                midUp = dp[c]
                rightUp = dp[c + 1] if c < N - 1 else float('inf')
                dp[c] = matrix[r][c] + min(midUp, leftUp, rightUp)
                leftUp = midUp

        return min(dp)

Time & Space Complexity

  • Time complexity: O(n2)O(n ^ 2)
  • Space complexity: O(n)O(n)

4. Dynamic Programming (In-Place)

Intuition

If we are allowed to modify the input matrix, we can avoid using any extra space for DP. We update each cell in place to store the minimum path sum to reach that cell. This is the most space-efficient approach, using only O(1) extra space beyond the input.

Algorithm

  1. Start from the second row (index 1).
  2. For each cell (r, c):
    • Compute the minimum of the three cells above: (r-1, c-1), (r-1, c), (r-1, c+1) (treating out-of-bounds as infinity).
    • Add this minimum to matrix[r][c].
  3. After processing all rows, return the minimum value in the last row.
class Solution:
    def minFallingPathSum(self, matrix: List[List[int]]) -> int:
        N = len(matrix)

        for r in range(1, N):
            for c in range(N):
                mid = matrix[r - 1][c]
                left = matrix[r - 1][c - 1] if c > 0 else float("inf")
                right = matrix[r - 1][c + 1] if c < N - 1 else float("inf")
                matrix[r][c] = matrix[r][c] + min(mid, left, right)

        return min(matrix[-1])

Time & Space Complexity

  • Time complexity: O(n2)O(n ^ 2)
  • Space complexity: O(1)O(1) extra space.

Common Pitfalls

Out-of-Bounds Column Access

When computing the minimum from the three cells above (left-diagonal, directly above, right-diagonal), forgetting to check column boundaries causes index errors. The leftmost column has no left-diagonal parent, and the rightmost column has no right-diagonal parent. Always handle these edge cases by treating out-of-bounds values as infinity.

Modifying Input While Reading

In the in-place DP approach, you update matrix[r][c] using values from the previous row. Since you read from the same row you just wrote to, there is no conflict. However, if you mistakenly read from cells you have already updated in the current row, you will get incorrect results. Process each row independently from the previous row.

Forgetting to Check All Starting Columns

The path can start from any column in the first row. A common mistake is starting only from column 0 or assuming the minimum value in the first row is the optimal start. You must either try all starting columns explicitly or let the DP naturally propagate all possibilities.