Skip to content

14.3   Dynamic programming problem-solving approach

The last two sections introduced the main characteristics of dynamic programming problems. Next, let's explore two more practical issues together.

  1. How to determine whether a problem is a dynamic programming problem?
  2. What are the complete steps to solve a dynamic programming problem?

14.3.1   Problem determination

Generally speaking, if a problem contains overlapping subproblems, optimal substructure, and exhibits no aftereffects, it is usually suitable for dynamic programming solutions. However, it is often difficult to directly extract these characteristics from the problem description. Therefore, we usually relax the conditions and first observe whether the problem is suitable for resolution using backtracking (exhaustive search).

Problems suitable for backtracking usually fit the "decision tree model", which can be described using a tree structure, where each node represents a decision, and each path represents a sequence of decisions.

In other words, if the problem contains explicit decision concepts, and the solution is produced through a series of decisions, then it fits the decision tree model and can usually be solved using backtracking.

On this basis, there are some "bonus points" for determining dynamic programming problems.

  • The problem contains descriptions of maximization (minimization) or finding the most (least) optimal solution.
  • The problem's states can be represented using a list, multi-dimensional matrix, or tree, and a state has a recursive relationship with its surrounding states.

Correspondingly, there are also some "penalty points".

  • The goal of the problem is to find all possible solutions, not just the optimal solution.
  • The problem description has obvious characteristics of permutations and combinations, requiring the return of specific multiple solutions.

If a problem fits the decision tree model and has relatively obvious "bonus points", we can assume it is a dynamic programming problem and verify it during the solution process.

14.3.2   Problem-solving steps

The dynamic programming problem-solving process varies with the nature and difficulty of the problem but generally follows these steps: describe decisions, define states, establish a \(dp\) table, derive state transition equations, and determine boundary conditions, etc.

To illustrate the problem-solving steps more vividly, we use a classic problem, "Minimum Path Sum", as an example.

Question

Given an \(n \times m\) two-dimensional grid grid, each cell in the grid contains a non-negative integer representing the cost of that cell. The robot starts from the top-left cell and can only move down or right at each step until it reaches the bottom-right cell. Return the minimum path sum from the top-left to the bottom-right.

Figure 14-10 shows an example, where the given grid's minimum path sum is \(13\).

Minimum Path Sum Example Data

Figure 14-10   Minimum Path Sum Example Data

First step: Think about each round of decisions, define the state, and thereby obtain the \(dp\) table

Each round of decisions in this problem is to move one step down or right from the current cell. Suppose the row and column indices of the current cell are \([i, j]\), then after moving down or right, the indices become \([i+1, j]\) or \([i, j+1]\). Therefore, the state should include two variables: the row index and the column index, denoted as \([i, j]\).

The state \([i, j]\) corresponds to the subproblem: the minimum path sum from the starting point \([0, 0]\) to \([i, j]\), denoted as \(dp[i, j]\).

Thus, we obtain the two-dimensional \(dp\) matrix shown in Figure 14-11, whose size is the same as the input grid \(grid\).

State definition and DP table

Figure 14-11   State definition and DP table

Note

Dynamic programming and backtracking can be described as a sequence of decisions, while a state consists of all decision variables. It should include all variables that describe the progress of solving the problem, containing enough information to derive the next state.

Each state corresponds to a subproblem, and we define a \(dp\) table to store the solutions to all subproblems. Each independent variable of the state is a dimension of the \(dp\) table. Essentially, the \(dp\) table is a mapping between states and solutions to subproblems.

Second step: Identify the optimal substructure, then derive the state transition equation

For the state \([i, j]\), it can only be derived from the cell above \([i-1, j]\) or the cell to the left \([i, j-1]\). Therefore, the optimal substructure is: the minimum path sum to reach \([i, j]\) is determined by the smaller of the minimum path sums of \([i, j-1]\) and \([i-1, j]\).

Based on the above analysis, the state transition equation shown in Figure 14-12 can be derived:

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

Optimal substructure and state transition equation

Figure 14-12   Optimal substructure and state transition equation

Note

Based on the defined \(dp\) table, think about the relationship between the original problem and the subproblems, and find out how to construct the optimal solution to the original problem from the optimal solutions to the subproblems, i.e., the optimal substructure.

Once we have identified the optimal substructure, we can use it to build the state transition equation.

Third step: Determine boundary conditions and state transition order

In this problem, the states in the first row can only come from the states to their left, and the states in the first column can only come from the states above them, so the first row \(i = 0\) and the first column \(j = 0\) are the boundary conditions.

As shown in Figure 14-13, since each cell is derived from the cell to its left and the cell above it, we use loops to traverse the matrix, the outer loop iterating over the rows and the inner loop iterating over the columns.

Boundary conditions and state transition order

Figure 14-13   Boundary conditions and state transition order

Note

Boundary conditions are used in dynamic programming to initialize the \(dp\) table, and in search to prune.

The core of the state transition order is to ensure that when calculating the solution to the current problem, all the smaller subproblems it depends on have already been correctly calculated.

Based on the above analysis, we can directly write the dynamic programming code. However, the decomposition of subproblems is a top-down approach, so implementing it in the order of "brute-force search → memoized search → dynamic programming" is more in line with habitual thinking.

Start searching from the state \([i, j]\), constantly decomposing it into smaller states \([i-1, j]\) and \([i, j-1]\). The recursive function includes the following elements.

  • Recursive parameter: state \([i, j]\).
  • Return value: the minimum path sum from \([0, 0]\) to \([i, j]\) \(dp[i, j]\).
  • Termination condition: when \(i = 0\) and \(j = 0\), return the cost \(grid[0, 0]\).
  • Pruning: when \(i < 0\) or \(j < 0\) index out of bounds, return the cost \(+\infty\), representing infeasibility.

Implementation code as follows:

min_path_sum.py
def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int:
    """最小路径和:暴力搜索"""
    # 若为左上角单元格,则终止搜索
    if i == 0 and j == 0:
        return grid[0][0]
    # 若行列索引越界,则返回 +∞ 代价
    if i < 0 or j < 0:
        return inf
    # 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    up = min_path_sum_dfs(grid, i - 1, j)
    left = min_path_sum_dfs(grid, i, j - 1)
    # 返回从左上角到 (i, j) 的最小路径代价
    return min(left, up) + grid[i][j]
min_path_sum.cpp
/* 最小路径和:暴力搜索 */
int minPathSumDFS(vector<vector<int>> &grid, int i, int j) {
    // 若为左上角单元格,则终止搜索
    if (i == 0 && j == 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return INT_MAX;
    }
    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    int up = minPathSumDFS(grid, i - 1, j);
    int left = minPathSumDFS(grid, i, j - 1);
    // 返回从左上角到 (i, j) 的最小路径代价
    return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;
}
min_path_sum.java
/* 最小路径和:暴力搜索 */
int minPathSumDFS(int[][] grid, int i, int j) {
    // 若为左上角单元格,则终止搜索
    if (i == 0 && j == 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return Integer.MAX_VALUE;
    }
    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    int up = minPathSumDFS(grid, i - 1, j);
    int left = minPathSumDFS(grid, i, j - 1);
    // 返回从左上角到 (i, j) 的最小路径代价
    return Math.min(left, up) + grid[i][j];
}
min_path_sum.cs
/* 最小路径和:暴力搜索 */
int MinPathSumDFS(int[][] grid, int i, int j) {
    // 若为左上角单元格,则终止搜索
    if (i == 0 && j == 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return int.MaxValue;
    }
    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    int up = MinPathSumDFS(grid, i - 1, j);
    int left = MinPathSumDFS(grid, i, j - 1);
    // 返回从左上角到 (i, j) 的最小路径代价
    return Math.Min(left, up) + grid[i][j];
}
min_path_sum.go
/* 最小路径和:暴力搜索 */
func minPathSumDFS(grid [][]int, i, j int) int {
    // 若为左上角单元格,则终止搜索
    if i == 0 && j == 0 {
        return grid[0][0]
    }
    // 若行列索引越界,则返回 +∞ 代价
    if i < 0 || j < 0 {
        return math.MaxInt
    }
    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    up := minPathSumDFS(grid, i-1, j)
    left := minPathSumDFS(grid, i, j-1)
    // 返回从左上角到 (i, j) 的最小路径代价
    return int(math.Min(float64(left), float64(up))) + grid[i][j]
}
min_path_sum.swift
/* 最小路径和:暴力搜索 */
func minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int {
    // 若为左上角单元格,则终止搜索
    if i == 0, j == 0 {
        return grid[0][0]
    }
    // 若行列索引越界,则返回 +∞ 代价
    if i < 0 || j < 0 {
        return .max
    }
    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    let up = minPathSumDFS(grid: grid, i: i - 1, j: j)
    let left = minPathSumDFS(grid: grid, i: i, j: j - 1)
    // 返回从左上角到 (i, j) 的最小路径代价
    return min(left, up) + grid[i][j]
}
min_path_sum.js
/* 最小路径和:暴力搜索 */
function minPathSumDFS(grid, i, j) {
    // 若为左上角单元格,则终止搜索
    if (i === 0 && j === 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return Infinity;
    }
    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    const up = minPathSumDFS(grid, i - 1, j);
    const left = minPathSumDFS(grid, i, j - 1);
    // 返回从左上角到 (i, j) 的最小路径代价
    return Math.min(left, up) + grid[i][j];
}
min_path_sum.ts
/* 最小路径和:暴力搜索 */
function minPathSumDFS(
    grid: Array<Array<number>>,
    i: number,
    j: number
): number {
    // 若为左上角单元格,则终止搜索
    if (i === 0 && j == 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return Infinity;
    }
    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    const up = minPathSumDFS(grid, i - 1, j);
    const left = minPathSumDFS(grid, i, j - 1);
    // 返回从左上角到 (i, j) 的最小路径代价
    return Math.min(left, up) + grid[i][j];
}
min_path_sum.dart
/* 最小路径和:暴力搜索 */
int minPathSumDFS(List<List<int>> grid, int i, int j) {
  // 若为左上角单元格,则终止搜索
  if (i == 0 && j == 0) {
    return grid[0][0];
  }
  // 若行列索引越界,则返回 +∞ 代价
  if (i < 0 || j < 0) {
    // 在 Dart 中,int 类型是固定范围的整数,不存在表示“无穷大”的值
    return BigInt.from(2).pow(31).toInt();
  }
  // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
  int up = minPathSumDFS(grid, i - 1, j);
  int left = minPathSumDFS(grid, i, j - 1);
  // 返回从左上角到 (i, j) 的最小路径代价
  return min(left, up) + grid[i][j];
}
min_path_sum.rs
/* 最小路径和:暴力搜索 */
fn min_path_sum_dfs(grid: &Vec<Vec<i32>>, i: i32, j: i32) -> i32 {
    // 若为左上角单元格,则终止搜索
    if i == 0 && j == 0 {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if i < 0 || j < 0 {
        return i32::MAX;
    }
    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    let up = min_path_sum_dfs(grid, i - 1, j);
    let left = min_path_sum_dfs(grid, i, j - 1);
    // 返回从左上角到 (i, j) 的最小路径代价
    std::cmp::min(left, up) + grid[i as usize][j as usize]
}
min_path_sum.c
/* 最小路径和:暴力搜索 */
int minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) {
    // 若为左上角单元格,则终止搜索
    if (i == 0 && j == 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return INT_MAX;
    }
    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    int up = minPathSumDFS(grid, i - 1, j);
    int left = minPathSumDFS(grid, i, j - 1);
    // 返回从左上角到 (i, j) 的最小路径代价
    return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;
}
min_path_sum.kt
/* 最小路径和:暴力搜索 */
fun minPathSumDFS(grid: Array<IntArray>, i: Int, j: Int): Int {
    // 若为左上角单元格,则终止搜索
    if (i == 0 && j == 0) {
        return grid[0][0]
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return Int.MAX_VALUE
    }
    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    val up = minPathSumDFS(grid, i - 1, j)
    val left = minPathSumDFS(grid, i, j - 1)
    // 返回从左上角到 (i, j) 的最小路径代价
    return min(left, up) + grid[i][j]
}
min_path_sum.rb
[class]{}-[func]{min_path_sum_dfs}
min_path_sum.zig
// 最小路径和:暴力搜索
fn minPathSumDFS(grid: anytype, i: i32, j: i32) i32 {
    // 若为左上角单元格,则终止搜索
    if (i == 0 and j == 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 or j < 0) {
        return std.math.maxInt(i32);
    }
    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    var up = minPathSumDFS(grid, i - 1, j);
    var left = minPathSumDFS(grid, i, j - 1);
    // 返回从左上角到 (i, j) 的最小路径代价
    return @min(left, up) + grid[@as(usize, @intCast(i))][@as(usize, @intCast(j))];
}
Code Visualization

Figure 14-14 shows the recursive tree rooted at \(dp[2, 1]\), which includes some overlapping subproblems, the number of which increases sharply as the size of the grid grid increases.

Essentially, the reason for overlapping subproblems is: there are multiple paths to reach a certain cell from the top-left corner.

Brute-force search recursive tree

Figure 14-14   Brute-force search recursive tree

Each state has two choices, down and right, so the total number of steps from the top-left corner to the bottom-right corner is \(m + n - 2\), so the worst-case time complexity is \(O(2^{m + n})\). Please note that this calculation method does not consider the situation near the grid edge, where there is only one choice left when reaching the network edge, so the actual number of paths will be less.

We introduce a memo list mem of the same size as the grid grid, used to record the solutions to various subproblems, and prune overlapping subproblems:

min_path_sum.py
def min_path_sum_dfs_mem(
    grid: list[list[int]], mem: list[list[int]], i: int, j: int
) -> int:
    """最小路径和:记忆化搜索"""
    # 若为左上角单元格,则终止搜索
    if i == 0 and j == 0:
        return grid[0][0]
    # 若行列索引越界,则返回 +∞ 代价
    if i < 0 or j < 0:
        return inf
    # 若已有记录,则直接返回
    if mem[i][j] != -1:
        return mem[i][j]
    # 左边和上边单元格的最小路径代价
    up = min_path_sum_dfs_mem(grid, mem, i - 1, j)
    left = min_path_sum_dfs_mem(grid, mem, i, j - 1)
    # 记录并返回左上角到 (i, j) 的最小路径代价
    mem[i][j] = min(left, up) + grid[i][j]
    return mem[i][j]
min_path_sum.cpp
/* 最小路径和:记忆化搜索 */
int minPathSumDFSMem(vector<vector<int>> &grid, vector<vector<int>> &mem, int i, int j) {
    // 若为左上角单元格,则终止搜索
    if (i == 0 && j == 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return INT_MAX;
    }
    // 若已有记录,则直接返回
    if (mem[i][j] != -1) {
        return mem[i][j];
    }
    // 左边和上边单元格的最小路径代价
    int up = minPathSumDFSMem(grid, mem, i - 1, j);
    int left = minPathSumDFSMem(grid, mem, i, j - 1);
    // 记录并返回左上角到 (i, j) 的最小路径代价
    mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX;
    return mem[i][j];
}
min_path_sum.java
/* 最小路径和:记忆化搜索 */
int minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {
    // 若为左上角单元格,则终止搜索
    if (i == 0 && j == 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return Integer.MAX_VALUE;
    }
    // 若已有记录,则直接返回
    if (mem[i][j] != -1) {
        return mem[i][j];
    }
    // 左边和上边单元格的最小路径代价
    int up = minPathSumDFSMem(grid, mem, i - 1, j);
    int left = minPathSumDFSMem(grid, mem, i, j - 1);
    // 记录并返回左上角到 (i, j) 的最小路径代价
    mem[i][j] = Math.min(left, up) + grid[i][j];
    return mem[i][j];
}
min_path_sum.cs
/* 最小路径和:记忆化搜索 */
int MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) {
    // 若为左上角单元格,则终止搜索
    if (i == 0 && j == 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return int.MaxValue;
    }
    // 若已有记录,则直接返回
    if (mem[i][j] != -1) {
        return mem[i][j];
    }
    // 左边和上边单元格的最小路径代价
    int up = MinPathSumDFSMem(grid, mem, i - 1, j);
    int left = MinPathSumDFSMem(grid, mem, i, j - 1);
    // 记录并返回左上角到 (i, j) 的最小路径代价
    mem[i][j] = Math.Min(left, up) + grid[i][j];
    return mem[i][j];
}
min_path_sum.go
/* 最小路径和:记忆化搜索 */
func minPathSumDFSMem(grid, mem [][]int, i, j int) int {
    // 若为左上角单元格,则终止搜索
    if i == 0 && j == 0 {
        return grid[0][0]
    }
    // 若行列索引越界,则返回 +∞ 代价
    if i < 0 || j < 0 {
        return math.MaxInt
    }
    // 若已有记录,则直接返回
    if mem[i][j] != -1 {
        return mem[i][j]
    }
    // 左边和上边单元格的最小路径代价
    up := minPathSumDFSMem(grid, mem, i-1, j)
    left := minPathSumDFSMem(grid, mem, i, j-1)
    // 记录并返回左上角到 (i, j) 的最小路径代价
    mem[i][j] = int(math.Min(float64(left), float64(up))) + grid[i][j]
    return mem[i][j]
}
min_path_sum.swift
/* 最小路径和:记忆化搜索 */
func minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int {
    // 若为左上角单元格,则终止搜索
    if i == 0, j == 0 {
        return grid[0][0]
    }
    // 若行列索引越界,则返回 +∞ 代价
    if i < 0 || j < 0 {
        return .max
    }
    // 若已有记录,则直接返回
    if mem[i][j] != -1 {
        return mem[i][j]
    }
    // 左边和上边单元格的最小路径代价
    let up = minPathSumDFSMem(grid: grid, mem: &mem, i: i - 1, j: j)
    let left = minPathSumDFSMem(grid: grid, mem: &mem, i: i, j: j - 1)
    // 记录并返回左上角到 (i, j) 的最小路径代价
    mem[i][j] = min(left, up) + grid[i][j]
    return mem[i][j]
}
min_path_sum.js
/* 最小路径和:记忆化搜索 */
function minPathSumDFSMem(grid, mem, i, j) {
    // 若为左上角单元格,则终止搜索
    if (i === 0 && j === 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return Infinity;
    }
    // 若已有记录,则直接返回
    if (mem[i][j] !== -1) {
        return mem[i][j];
    }
    // 左边和上边单元格的最小路径代价
    const up = minPathSumDFSMem(grid, mem, i - 1, j);
    const left = minPathSumDFSMem(grid, mem, i, j - 1);
    // 记录并返回左上角到 (i, j) 的最小路径代价
    mem[i][j] = Math.min(left, up) + grid[i][j];
    return mem[i][j];
}
min_path_sum.ts
/* 最小路径和:记忆化搜索 */
function minPathSumDFSMem(
    grid: Array<Array<number>>,
    mem: Array<Array<number>>,
    i: number,
    j: number
): number {
    // 若为左上角单元格,则终止搜索
    if (i === 0 && j === 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return Infinity;
    }
    // 若已有记录,则直接返回
    if (mem[i][j] != -1) {
        return mem[i][j];
    }
    // 左边和上边单元格的最小路径代价
    const up = minPathSumDFSMem(grid, mem, i - 1, j);
    const left = minPathSumDFSMem(grid, mem, i, j - 1);
    // 记录并返回左上角到 (i, j) 的最小路径代价
    mem[i][j] = Math.min(left, up) + grid[i][j];
    return mem[i][j];
}
min_path_sum.dart
/* 最小路径和:记忆化搜索 */
int minPathSumDFSMem(List<List<int>> grid, List<List<int>> mem, int i, int j) {
  // 若为左上角单元格,则终止搜索
  if (i == 0 && j == 0) {
    return grid[0][0];
  }
  // 若行列索引越界,则返回 +∞ 代价
  if (i < 0 || j < 0) {
    // 在 Dart 中,int 类型是固定范围的整数,不存在表示“无穷大”的值
    return BigInt.from(2).pow(31).toInt();
  }
  // 若已有记录,则直接返回
  if (mem[i][j] != -1) {
    return mem[i][j];
  }
  // 左边和上边单元格的最小路径代价
  int up = minPathSumDFSMem(grid, mem, i - 1, j);
  int left = minPathSumDFSMem(grid, mem, i, j - 1);
  // 记录并返回左上角到 (i, j) 的最小路径代价
  mem[i][j] = min(left, up) + grid[i][j];
  return mem[i][j];
}
min_path_sum.rs
/* 最小路径和:记忆化搜索 */
fn min_path_sum_dfs_mem(grid: &Vec<Vec<i32>>, mem: &mut Vec<Vec<i32>>, i: i32, j: i32) -> i32 {
    // 若为左上角单元格,则终止搜索
    if i == 0 && j == 0 {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if i < 0 || j < 0 {
        return i32::MAX;
    }
    // 若已有记录,则直接返回
    if mem[i as usize][j as usize] != -1 {
        return mem[i as usize][j as usize];
    }
    // 左边和上边单元格的最小路径代价
    let up = min_path_sum_dfs_mem(grid, mem, i - 1, j);
    let left = min_path_sum_dfs_mem(grid, mem, i, j - 1);
    // 记录并返回左上角到 (i, j) 的最小路径代价
    mem[i as usize][j as usize] = std::cmp::min(left, up) + grid[i as usize][j as usize];
    mem[i as usize][j as usize]
}
min_path_sum.c
/* 最小路径和:记忆化搜索 */
int minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) {
    // 若为左上角单元格,则终止搜索
    if (i == 0 && j == 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return INT_MAX;
    }
    // 若已有记录,则直接返回
    if (mem[i][j] != -1) {
        return mem[i][j];
    }
    // 左边和上边单元格的最小路径代价
    int up = minPathSumDFSMem(grid, mem, i - 1, j);
    int left = minPathSumDFSMem(grid, mem, i, j - 1);
    // 记录并返回左上角到 (i, j) 的最小路径代价
    mem[i][j] = myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX;
    return mem[i][j];
}
min_path_sum.kt
/* 最小路径和:记忆化搜索 */
fun minPathSumDFSMem(
    grid: Array<IntArray>,
    mem: Array<IntArray>,
    i: Int,
    j: Int
): Int {
    // 若为左上角单元格,则终止搜索
    if (i == 0 && j == 0) {
        return grid[0][0]
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 || j < 0) {
        return Int.MAX_VALUE
    }
    // 若已有记录,则直接返回
    if (mem[i][j] != -1) {
        return mem[i][j]
    }
    // 左边和上边单元格的最小路径代价
    val up = minPathSumDFSMem(grid, mem, i - 1, j)
    val left = minPathSumDFSMem(grid, mem, i, j - 1)
    // 记录并返回左上角到 (i, j) 的最小路径代价
    mem[i][j] = min(left, up) + grid[i][j]
    return mem[i][j]
}
min_path_sum.rb
[class]{}-[func]{min_path_sum_dfs_mem}
min_path_sum.zig
// 最小路径和:记忆化搜索
fn minPathSumDFSMem(grid: anytype, mem: anytype, i: i32, j: i32) i32 {
    // 若为左上角单元格,则终止搜索
    if (i == 0 and j == 0) {
        return grid[0][0];
    }
    // 若行列索引越界,则返回 +∞ 代价
    if (i < 0 or j < 0) {
        return std.math.maxInt(i32);
    }
    // 若已有记录,则直接返回
    if (mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))] != -1) {
        return mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))];
    }
    // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价
    var up = minPathSumDFSMem(grid, mem, i - 1, j);
    var left = minPathSumDFSMem(grid, mem, i, j - 1);
    // 返回从左上角到 (i, j) 的最小路径代价
    // 记录并返回左上角到 (i, j) 的最小路径代价
    mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))] = @min(left, up) + grid[@as(usize, @intCast(i))][@as(usize, @intCast(j))];
    return mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))];
}
Code Visualization

As shown in Figure 14-15, after introducing memoization, all subproblem solutions only need to be calculated once, so the time complexity depends on the total number of states, i.e., the grid size \(O(nm)\).

Memoized search recursive tree

Figure 14-15   Memoized search recursive tree

3.   Method 3: Dynamic programming

Implement the dynamic programming solution iteratively, code as shown below:

min_path_sum.py
def min_path_sum_dp(grid: list[list[int]]) -> int:
    """最小路径和:动态规划"""
    n, m = len(grid), len(grid[0])
    # 初始化 dp 表
    dp = [[0] * m for _ in range(n)]
    dp[0][0] = grid[0][0]
    # 状态转移:首行
    for j in range(1, m):
        dp[0][j] = dp[0][j - 1] + grid[0][j]
    # 状态转移:首列
    for i in range(1, n):
        dp[i][0] = dp[i - 1][0] + grid[i][0]
    # 状态转移:其余行和列
    for i in range(1, n):
        for j in range(1, m):
            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]
    return dp[n - 1][m - 1]
min_path_sum.cpp
/* 最小路径和:动态规划 */
int minPathSumDP(vector<vector<int>> &grid) {
    int n = grid.size(), m = grid[0].size();
    // 初始化 dp 表
    vector<vector<int>> dp(n, vector<int>(m));
    dp[0][0] = grid[0][0];
    // 状态转移:首行
    for (int j = 1; j < m; j++) {
        dp[0][j] = dp[0][j - 1] + grid[0][j];
    }
    // 状态转移:首列
    for (int i = 1; i < n; i++) {
        dp[i][0] = dp[i - 1][0] + grid[i][0];
    }
    // 状态转移:其余行和列
    for (int i = 1; i < n; i++) {
        for (int j = 1; j < m; j++) {
            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
        }
    }
    return dp[n - 1][m - 1];
}
min_path_sum.java
/* 最小路径和:动态规划 */
int minPathSumDP(int[][] grid) {
    int n = grid.length, m = grid[0].length;
    // 初始化 dp 表
    int[][] dp = new int[n][m];
    dp[0][0] = grid[0][0];
    // 状态转移:首行
    for (int j = 1; j < m; j++) {
        dp[0][j] = dp[0][j - 1] + grid[0][j];
    }
    // 状态转移:首列
    for (int i = 1; i < n; i++) {
        dp[i][0] = dp[i - 1][0] + grid[i][0];
    }
    // 状态转移:其余行和列
    for (int i = 1; i < n; i++) {
        for (int j = 1; j < m; j++) {
            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
        }
    }
    return dp[n - 1][m - 1];
}
min_path_sum.cs
/* 最小路径和:动态规划 */
int MinPathSumDP(int[][] grid) {
    int n = grid.Length, m = grid[0].Length;
    // 初始化 dp 表
    int[,] dp = new int[n, m];
    dp[0, 0] = grid[0][0];
    // 状态转移:首行
    for (int j = 1; j < m; j++) {
        dp[0, j] = dp[0, j - 1] + grid[0][j];
    }
    // 状态转移:首列
    for (int i = 1; i < n; i++) {
        dp[i, 0] = dp[i - 1, 0] + grid[i][0];
    }
    // 状态转移:其余行和列
    for (int i = 1; i < n; i++) {
        for (int j = 1; j < m; j++) {
            dp[i, j] = Math.Min(dp[i, j - 1], dp[i - 1, j]) + grid[i][j];
        }
    }
    return dp[n - 1, m - 1];
}
min_path_sum.go
/* 最小路径和:动态规划 */
func minPathSumDP(grid [][]int) int {
    n, m := len(grid), len(grid[0])
    // 初始化 dp 表
    dp := make([][]int, n)
    for i := 0; i < n; i++ {
        dp[i] = make([]int, m)
    }
    dp[0][0] = grid[0][0]
    // 状态转移:首行
    for j := 1; j < m; j++ {
        dp[0][j] = dp[0][j-1] + grid[0][j]
    }
    // 状态转移:首列
    for i := 1; i < n; i++ {
        dp[i][0] = dp[i-1][0] + grid[i][0]
    }
    // 状态转移:其余行和列
    for i := 1; i < n; i++ {
        for j := 1; j < m; j++ {
            dp[i][j] = int(math.Min(float64(dp[i][j-1]), float64(dp[i-1][j]))) + grid[i][j]
        }
    }
    return dp[n-1][m-1]
}
min_path_sum.swift
/* 最小路径和:动态规划 */
func minPathSumDP(grid: [[Int]]) -> Int {
    let n = grid.count
    let m = grid[0].count
    // 初始化 dp 表
    var dp = Array(repeating: Array(repeating: 0, count: m), count: n)
    dp[0][0] = grid[0][0]
    // 状态转移:首行
    for j in 1 ..< m {
        dp[0][j] = dp[0][j - 1] + grid[0][j]
    }
    // 状态转移:首列
    for i in 1 ..< n {
        dp[i][0] = dp[i - 1][0] + grid[i][0]
    }
    // 状态转移:其余行和列
    for i in 1 ..< n {
        for j in 1 ..< m {
            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]
        }
    }
    return dp[n - 1][m - 1]
}
min_path_sum.js
/* 最小路径和:动态规划 */
function minPathSumDP(grid) {
    const n = grid.length,
        m = grid[0].length;
    // 初始化 dp 表
    const dp = Array.from({ length: n }, () =>
        Array.from({ length: m }, () => 0)
    );
    dp[0][0] = grid[0][0];
    // 状态转移:首行
    for (let j = 1; j < m; j++) {
        dp[0][j] = dp[0][j - 1] + grid[0][j];
    }
    // 状态转移:首列
    for (let i = 1; i < n; i++) {
        dp[i][0] = dp[i - 1][0] + grid[i][0];
    }
    // 状态转移:其余行和列
    for (let i = 1; i < n; i++) {
        for (let j = 1; j < m; j++) {
            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
        }
    }
    return dp[n - 1][m - 1];
}
min_path_sum.ts
/* 最小路径和:动态规划 */
function minPathSumDP(grid: Array<Array<number>>): number {
    const n = grid.length,
        m = grid[0].length;
    // 初始化 dp 表
    const dp = Array.from({ length: n }, () =>
        Array.from({ length: m }, () => 0)
    );
    dp[0][0] = grid[0][0];
    // 状态转移:首行
    for (let j = 1; j < m; j++) {
        dp[0][j] = dp[0][j - 1] + grid[0][j];
    }
    // 状态转移:首列
    for (let i = 1; i < n; i++) {
        dp[i][0] = dp[i - 1][0] + grid[i][0];
    }
    // 状态转移:其余行和列
    for (let i = 1; i < n; i++) {
        for (let j: number = 1; j < m; j++) {
            dp[i][j] = Math.min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
        }
    }
    return dp[n - 1][m - 1];
}
min_path_sum.dart
/* 最小路径和:动态规划 */
int minPathSumDP(List<List<int>> grid) {
  int n = grid.length, m = grid[0].length;
  // 初始化 dp 表
  List<List<int>> dp = List.generate(n, (i) => List.filled(m, 0));
  dp[0][0] = grid[0][0];
  // 状态转移:首行
  for (int j = 1; j < m; j++) {
    dp[0][j] = dp[0][j - 1] + grid[0][j];
  }
  // 状态转移:首列
  for (int i = 1; i < n; i++) {
    dp[i][0] = dp[i - 1][0] + grid[i][0];
  }
  // 状态转移:其余行和列
  for (int i = 1; i < n; i++) {
    for (int j = 1; j < m; j++) {
      dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
    }
  }
  return dp[n - 1][m - 1];
}
min_path_sum.rs
/* 最小路径和:动态规划 */
fn min_path_sum_dp(grid: &Vec<Vec<i32>>) -> i32 {
    let (n, m) = (grid.len(), grid[0].len());
    // 初始化 dp 表
    let mut dp = vec![vec![0; m]; n];
    dp[0][0] = grid[0][0];
    // 状态转移:首行
    for j in 1..m {
        dp[0][j] = dp[0][j - 1] + grid[0][j];
    }
    // 状态转移:首列
    for i in 1..n {
        dp[i][0] = dp[i - 1][0] + grid[i][0];
    }
    // 状态转移:其余行和列
    for i in 1..n {
        for j in 1..m {
            dp[i][j] = std::cmp::min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
        }
    }
    dp[n - 1][m - 1]
}
min_path_sum.c
/* 最小路径和:动态规划 */
int minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {
    // 初始化 dp 表
    int **dp = malloc(n * sizeof(int *));
    for (int i = 0; i < n; i++) {
        dp[i] = calloc(m, sizeof(int));
    }
    dp[0][0] = grid[0][0];
    // 状态转移:首行
    for (int j = 1; j < m; j++) {
        dp[0][j] = dp[0][j - 1] + grid[0][j];
    }
    // 状态转移:首列
    for (int i = 1; i < n; i++) {
        dp[i][0] = dp[i - 1][0] + grid[i][0];
    }
    // 状态转移:其余行和列
    for (int i = 1; i < n; i++) {
        for (int j = 1; j < m; j++) {
            dp[i][j] = myMin(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
        }
    }
    int res = dp[n - 1][m - 1];
    // 释放内存
    for (int i = 0; i < n; i++) {
        free(dp[i]);
    }
    return res;
}
min_path_sum.kt
/* 最小路径和:动态规划 */
fun minPathSumDP(grid: Array<IntArray>): Int {
    val n = grid.size
    val m = grid[0].size
    // 初始化 dp 表
    val dp = Array(n) { IntArray(m) }
    dp[0][0] = grid[0][0]
    // 状态转移:首行
    for (j in 1..<m) {
        dp[0][j] = dp[0][j - 1] + grid[0][j]
    }
    // 状态转移:首列
    for (i in 1..<n) {
        dp[i][0] = dp[i - 1][0] + grid[i][0]
    }
    // 状态转移:其余行和列
    for (i in 1..<n) {
        for (j in 1..<m) {
            dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]
        }
    }
    return dp[n - 1][m - 1]
}
min_path_sum.rb
[class]{}-[func]{min_path_sum_dp}
min_path_sum.zig
// 最小路径和:动态规划
fn minPathSumDP(comptime grid: anytype) i32 {
    comptime var n = grid.len;
    comptime var m = grid[0].len;
    // 初始化 dp 表
    var dp = [_][m]i32{[_]i32{0} ** m} ** n;
    dp[0][0] = grid[0][0];
    // 状态转移:首行
    for (1..m) |j| {
        dp[0][j] = dp[0][j - 1] + grid[0][j];
    }
    // 状态转移:首列
    for (1..n) |i| {
        dp[i][0] = dp[i - 1][0] + grid[i][0];
    }
    // 状态转移:其余行和列
    for (1..n) |i| {
        for (1..m) |j| {
            dp[i][j] = @min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j];
        }
    }
    return dp[n - 1][m - 1];
}
Code Visualization

Figure 14-16 show the state transition process of the minimum path sum, traversing the entire grid, thus the time complexity is \(O(nm)\).

The array dp is of size \(n \times m\), therefore the space complexity is \(O(nm)\).

Dynamic programming process of minimum path sum

min_path_sum_dp_step2

min_path_sum_dp_step3

min_path_sum_dp_step4

min_path_sum_dp_step5

min_path_sum_dp_step6

min_path_sum_dp_step7

min_path_sum_dp_step8

min_path_sum_dp_step9

min_path_sum_dp_step10

min_path_sum_dp_step11

min_path_sum_dp_step12

Figure 14-16   Dynamic programming process of minimum path sum

4.   Space optimization

Since each cell is only related to the cell to its left and above, we can use a single-row array to implement the \(dp\) table.

Please note, since the array dp can only represent the state of one row, we cannot initialize the first column state in advance, but update it as we traverse each row:

min_path_sum.py
def min_path_sum_dp_comp(grid: list[list[int]]) -> int:
    """最小路径和:空间优化后的动态规划"""
    n, m = len(grid), len(grid[0])
    # 初始化 dp 表
    dp = [0] * m
    # 状态转移:首行
    dp[0] = grid[0][0]
    for j in range(1, m):
        dp[j] = dp[j - 1] + grid[0][j]
    # 状态转移:其余行
    for i in range(1, n):
        # 状态转移:首列
        dp[0] = dp[0] + grid[i][0]
        # 状态转移:其余列
        for j in range(1, m):
            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]
    return dp[m - 1]
min_path_sum.cpp
/* 最小路径和:空间优化后的动态规划 */
int minPathSumDPComp(vector<vector<int>> &grid) {
    int n = grid.size(), m = grid[0].size();
    // 初始化 dp 表
    vector<int> dp(m);
    // 状态转移:首行
    dp[0] = grid[0][0];
    for (int j = 1; j < m; j++) {
        dp[j] = dp[j - 1] + grid[0][j];
    }
    // 状态转移:其余行
    for (int i = 1; i < n; i++) {
        // 状态转移:首列
        dp[0] = dp[0] + grid[i][0];
        // 状态转移:其余列
        for (int j = 1; j < m; j++) {
            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];
        }
    }
    return dp[m - 1];
}
min_path_sum.java
/* 最小路径和:空间优化后的动态规划 */
int minPathSumDPComp(int[][] grid) {
    int n = grid.length, m = grid[0].length;
    // 初始化 dp 表
    int[] dp = new int[m];
    // 状态转移:首行
    dp[0] = grid[0][0];
    for (int j = 1; j < m; j++) {
        dp[j] = dp[j - 1] + grid[0][j];
    }
    // 状态转移:其余行
    for (int i = 1; i < n; i++) {
        // 状态转移:首列
        dp[0] = dp[0] + grid[i][0];
        // 状态转移:其余列
        for (int j = 1; j < m; j++) {
            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];
        }
    }
    return dp[m - 1];
}
min_path_sum.cs
/* 最小路径和:空间优化后的动态规划 */
int MinPathSumDPComp(int[][] grid) {
    int n = grid.Length, m = grid[0].Length;
    // 初始化 dp 表
    int[] dp = new int[m];
    dp[0] = grid[0][0];
    // 状态转移:首行
    for (int j = 1; j < m; j++) {
        dp[j] = dp[j - 1] + grid[0][j];
    }
    // 状态转移:其余行
    for (int i = 1; i < n; i++) {
        // 状态转移:首列
        dp[0] = dp[0] + grid[i][0];
        // 状态转移:其余列
        for (int j = 1; j < m; j++) {
            dp[j] = Math.Min(dp[j - 1], dp[j]) + grid[i][j];
        }
    }
    return dp[m - 1];
}
min_path_sum.go
/* 最小路径和:空间优化后的动态规划 */
func minPathSumDPComp(grid [][]int) int {
    n, m := len(grid), len(grid[0])
    // 初始化 dp 表
    dp := make([]int, m)
    // 状态转移:首行
    dp[0] = grid[0][0]
    for j := 1; j < m; j++ {
        dp[j] = dp[j-1] + grid[0][j]
    }
    // 状态转移:其余行和列
    for i := 1; i < n; i++ {
        // 状态转移:首列
        dp[0] = dp[0] + grid[i][0]
        // 状态转移:其余列
        for j := 1; j < m; j++ {
            dp[j] = int(math.Min(float64(dp[j-1]), float64(dp[j]))) + grid[i][j]
        }
    }
    return dp[m-1]
}
min_path_sum.swift
/* 最小路径和:空间优化后的动态规划 */
func minPathSumDPComp(grid: [[Int]]) -> Int {
    let n = grid.count
    let m = grid[0].count
    // 初始化 dp 表
    var dp = Array(repeating: 0, count: m)
    // 状态转移:首行
    dp[0] = grid[0][0]
    for j in 1 ..< m {
        dp[j] = dp[j - 1] + grid[0][j]
    }
    // 状态转移:其余行
    for i in 1 ..< n {
        // 状态转移:首列
        dp[0] = dp[0] + grid[i][0]
        // 状态转移:其余列
        for j in 1 ..< m {
            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]
        }
    }
    return dp[m - 1]
}
min_path_sum.js
/* 最小路径和:状态压缩后的动态规划 */
function minPathSumDPComp(grid) {
    const n = grid.length,
        m = grid[0].length;
    // 初始化 dp 表
    const dp = new Array(m);
    // 状态转移:首行
    dp[0] = grid[0][0];
    for (let j = 1; j < m; j++) {
        dp[j] = dp[j - 1] + grid[0][j];
    }
    // 状态转移:其余行
    for (let i = 1; i < n; i++) {
        // 状态转移:首列
        dp[0] = dp[0] + grid[i][0];
        // 状态转移:其余列
        for (let j = 1; j < m; j++) {
            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];
        }
    }
    return dp[m - 1];
}
min_path_sum.ts
/* 最小路径和:状态压缩后的动态规划 */
function minPathSumDPComp(grid: Array<Array<number>>): number {
    const n = grid.length,
        m = grid[0].length;
    // 初始化 dp 表
    const dp = new Array(m);
    // 状态转移:首行
    dp[0] = grid[0][0];
    for (let j = 1; j < m; j++) {
        dp[j] = dp[j - 1] + grid[0][j];
    }
    // 状态转移:其余行
    for (let i = 1; i < n; i++) {
        // 状态转移:首列
        dp[0] = dp[0] + grid[i][0];
        // 状态转移:其余列
        for (let j = 1; j < m; j++) {
            dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];
        }
    }
    return dp[m - 1];
}
min_path_sum.dart
/* 最小路径和:空间优化后的动态规划 */
int minPathSumDPComp(List<List<int>> grid) {
  int n = grid.length, m = grid[0].length;
  // 初始化 dp 表
  List<int> dp = List.filled(m, 0);
  dp[0] = grid[0][0];
  for (int j = 1; j < m; j++) {
    dp[j] = dp[j - 1] + grid[0][j];
  }
  // 状态转移:其余行
  for (int i = 1; i < n; i++) {
    // 状态转移:首列
    dp[0] = dp[0] + grid[i][0];
    // 状态转移:其余列
    for (int j = 1; j < m; j++) {
      dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];
    }
  }
  return dp[m - 1];
}
min_path_sum.rs
/* 最小路径和:空间优化后的动态规划 */
fn min_path_sum_dp_comp(grid: &Vec<Vec<i32>>) -> i32 {
    let (n, m) = (grid.len(), grid[0].len());
    // 初始化 dp 表
    let mut dp = vec![0; m];
    // 状态转移:首行
    dp[0] = grid[0][0];
    for j in 1..m {
        dp[j] = dp[j - 1] + grid[0][j];
    }
    // 状态转移:其余行
    for i in 1..n {
        // 状态转移:首列
        dp[0] = dp[0] + grid[i][0];
        // 状态转移:其余列
        for j in 1..m {
            dp[j] = std::cmp::min(dp[j - 1], dp[j]) + grid[i][j];
        }
    }
    dp[m - 1]
}
min_path_sum.c
/* 最小路径和:空间优化后的动态规划 */
int minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) {
    // 初始化 dp 表
    int *dp = calloc(m, sizeof(int));
    // 状态转移:首行
    dp[0] = grid[0][0];
    for (int j = 1; j < m; j++) {
        dp[j] = dp[j - 1] + grid[0][j];
    }
    // 状态转移:其余行
    for (int i = 1; i < n; i++) {
        // 状态转移:首列
        dp[0] = dp[0] + grid[i][0];
        // 状态转移:其余列
        for (int j = 1; j < m; j++) {
            dp[j] = myMin(dp[j - 1], dp[j]) + grid[i][j];
        }
    }
    int res = dp[m - 1];
    // 释放内存
    free(dp);
    return res;
}
min_path_sum.kt
/* 最小路径和:空间优化后的动态规划 */
fun minPathSumDPComp(grid: Array<IntArray>): Int {
    val n = grid.size
    val m = grid[0].size
    // 初始化 dp 表
    val dp = IntArray(m)
    // 状态转移:首行
    dp[0] = grid[0][0]
    for (j in 1..<m) {
        dp[j] = dp[j - 1] + grid[0][j]
    }
    // 状态转移:其余行
    for (i in 1..<n) {
        // 状态转移:首列
        dp[0] = dp[0] + grid[i][0]
        // 状态转移:其余列
        for (j in 1..<m) {
            dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]
        }
    }
    return dp[m - 1]
}
min_path_sum.rb
[class]{}-[func]{min_path_sum_dp_comp}
min_path_sum.zig
// 最小路径和:空间优化后的动态规划
fn minPathSumDPComp(comptime grid: anytype) i32 {
    comptime var n = grid.len;
    comptime var m = grid[0].len;
    // 初始化 dp 表
    var dp = [_]i32{0} ** m;
    // 状态转移:首行
    dp[0] = grid[0][0];
    for (1..m) |j| {
        dp[j] = dp[j - 1] + grid[0][j];
    }
    // 状态转移:其余行
    for (1..n) |i| {
        // 状态转移:首列
        dp[0] = dp[0] + grid[i][0];
        for (1..m) |j| {
            dp[j] = @min(dp[j - 1], dp[j]) + grid[i][j];
        }
    }
    return dp[m - 1];
}
Code Visualization

Feel free to drop your insights, questions or suggestions