Taro Logo

Minimum Falling Path Sum

Medium
2 months ago

Given an n x n array of integers matrix, return the minimum sum of any falling path through matrix.

A falling path starts at any element in the first row and chooses the element in the next row that is either directly below or diagonally left/right. Specifically, the next element from position (row, col) will be (row + 1, col - 1), (row + 1, col), or (row + 1, col + 1).

For example:

Consider the following matrix:

[[2, 1, 3],
 [6, 5, 4],
 [7, 8, 9]]

There are two falling paths with a minimum sum of 13.

As another example, consider:

[[-19, 57],
 [-40, -5]]

The falling path with the minimum sum is -59.

Write a function to efficiently compute the minimum falling path sum for a given square matrix of integers. What is the time and space complexity of your solution? How does your solution handle edge cases, such as empty matrices or matrices with a single element?

Sample Answer
# Minimum Falling Path Sum

## Problem Description

Given an `n x n` array of integers `matrix`, the goal is to find the minimum sum of any falling path through the `matrix`. A falling path starts at any element in the first row and chooses the element in the next row that is either directly below or diagonally left/right. In other words, from position `(row, col)`, the next element will be `(row + 1, col - 1)`, `(row + 1, col)`, or `(row + 1, col + 1)`.

For example:

matrix = [[2,1,3],[6,5,4],[7,8,9]]


The minimum falling path sum is 13 (1 -> 5 -> 7 or 1 -> 5 -> 8).

## Brute Force Solution

A brute-force approach would involve exploring every possible falling path and computing their sums. We can use recursion to achieve this. Start from each cell in the first row and recursively explore the possible paths downwards.  The minimum sum among all paths is the result.

```python
def min_falling_path_sum_brute_force(matrix):
    n = len(matrix)
    
    def find_path(row, col):
        if row == n:
            return 0
            
        if col < 0 or col >= n:
            return float('inf')
        
        return matrix[row][col] + min(
            find_path(row + 1, col - 1),
            find_path(row + 1, col),
            find_path(row + 1, col + 1)
        )
    
    min_sum = float('inf')
    for start_col in range(n):
        min_sum = min(min_sum, find_path(0, start_col))
    
    return min_sum

Optimal Solution (Dynamic Programming)

We can use dynamic programming to optimize this process. We will build a DP table of the same size as the input matrix, where dp[i][j] stores the minimum falling path sum ending at matrix[i][j]. The transitions are as follows:

dp[i][j] = matrix[i][j] + min(dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1])

Handle boundary conditions carefully. The minimum falling path sum will be the minimum value in the last row of the DP table.

def min_falling_path_sum_dp(matrix):
    n = len(matrix)
    dp = [[0] * n for _ in range(n)]

    # Initialize the first row of DP table with the first row of the matrix
    for j in range(n):
        dp[0][j] = matrix[0][j]

    # Fill the DP table
    for i in range(1, n):
        for j in range(n):
            # Calculate the minimum path sum from the previous row
            if j == 0:
                dp[i][j] = matrix[i][j] + min(dp[i-1][j], dp[i-1][j+1])
            elif j == n - 1:
                dp[i][j] = matrix[i][j] + min(dp[i-1][j-1], dp[i-1][j])
            else:
                dp[i][j] = matrix[i][j] + min(dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1])

    # The result is the minimum value in the last row
    return min(dp[n-1])

Big(O) Run-time Analysis

  • Brute Force: O(3^n * n), where n is the number of rows (and columns). For each cell, we potentially explore three paths. The 'n' factor accounts for the number of starting columns.
  • Dynamic Programming: O(n^2), where n is the number of rows (and columns). We iterate through each cell in the n x n matrix once.

Big(O) Space Usage Analysis

  • Brute Force: O(n) due to recursion depth.
  • Dynamic Programming: O(n^2) because we use a DP table of size n x n to store intermediate results.

Edge Cases

  1. Empty Matrix: If the matrix is empty (n=0), return 0. Although, based on the constraints, this is impossible, it's good practice to consider. If the constraint were n >= 0 instead of n >= 1, we would need to account for this.
  2. Single Element Matrix: If the matrix has only one element (n=1), the minimum falling path sum is the value of that element.
  3. Negative Values: The matrix can contain negative values, so the minimum sum can be negative.
  4. Large Matrix: For very large matrices, memory usage with the DP approach can be a concern. Space optimization techniques (e.g., using only two rows of the DP table at a time) could be considered, reducing space complexity to O(n).
def min_falling_path_sum_dp_optimized_space(matrix):
    n = len(matrix)
    if n == 0:
        return 0
    if n == 1:
        return matrix[0][0]

    dp = [row[:] for row in matrix]  # Using the original matrix for DP

    for i in range(1, n):
        for j in range(n):
            if j == 0:
                dp[i][j] += min(dp[i-1][j], dp[i-1][j+1])
            elif j == n - 1:
                dp[i][j] += min(dp[i-1][j-1], dp[i-1][j])
            else:
                dp[i][j] += min(dp[i-1][j-1], dp[i-1][j], dp[i-1][j+1])

    return min(dp[n-1])