动态规划DP 自我提问模板

在面对动态规划问题时,建立一个系统的思考框架能大大提升解题效率。本文以 LeetCode 64 题为例,展示如何用"自我提问模板"梳理 DP 思路,并在实战中验证其有效性。

什么是动态规划自我提问模板?

动态规划的本质是"记忆化递归 + 最优子结构",但很多时候我们会在定义状态、推导转移方程时感到困惑。为了更系统地分析 DP 问题,我总结了一套"自我提问模板",包含以下关键要素:

  1. dpij 定义:明确每个状态代表什么
  2. 默认值:初始化的边界条件
  3. 外部遍历:如何遍历整个状态空间
  4. choice:在每个状态下有哪些选择(如果适用)
  5. cost:每个选择的代价(如果适用)
  6. 转移条件:什么情况下进行状态转移
  7. 转移方程:如何从子问题推导到当前问题
  8. 返回值:最终答案在状态空间的哪个位置
bash 复制代码
我的思路
dp[i][j]定义:dp 和 grid 大小相同,dp[i][j]表示对应走到 grid[i][j] 的最小sum
默认值:dp[0][0] = grid[0][0]
外部遍历:遍历 grid,i 为行,j 为列,跳过 i = 0,j = 0
choice: N/A
cost: N/A
转移条件:N/A
转移方程:i = 0: dp[i][j] = dp[i][j-1] + grid[i][j]
         j = 0: dp[i][j] = dp[i-1][j] + grid[i][j]
        else dp[i][j] = min(dp[i][j-1] + grid[i][j], dp[i-1][j] + grid[i][j])
返回值:dp[gridSize][gridColSize[gridSize - 1]] 行的最小值

实战案例:LeetCode 64. Minimum Path Sum

题目描述

给定一个 m x n 的网格 grid,每个格子里是一个非负整数。从左上角 (0,0) 出发,只能向右或向下移动,走到右下角 (m-1, n-1)。要求找到一条路径,使路径上所有格子数字之和最小,并返回这个最小和。

使用模板分析

1. dpij 定义

dp 和 grid 大小相同,dpij 表示对应走到 gridij 的最小sum

这个定义很直观:我们需要知道"到达每个格子时的最小路径和",因此 dpij 就表示从起点 (0,0) 走到位置 (i,j) 的最小路径和。

2. 默认值

dp00 = grid00

起点是我们的初始状态,它的最小路径和就是起点格子本身的值。

3. 外部遍历

遍历 grid,i 为行,j 为列,跳过 i = 0,j = 0

我们需要填充整个 dp 数组,按行列顺序遍历。由于起点已经初始化,可以跳过 (0,0)。

4. choice & cost

N/A

在这道题中,我们不需要显式地考虑"选择"和"代价"的概念,因为路径是确定性的:只能向右或向下走,没有多个选择需要对比。

5. 转移条件

N/A

这道题的转移是无条件的,每个格子都需要根据其左边和上边的格子更新。

6. 转移方程
复制代码
i = 0: dp[i][j] = dp[i][j-1] + grid[i][j]    // 第一行只能从左边来
j = 0: dp[i][j] = dp[i-1][j] + grid[i][j]    // 第一列只能从上边来
else:  dp[i][j] = min(dp[i][j-1] + grid[i][j], dp[i-1][j] + grid[i][j])  // 其他位置取左边和上边的最小值

这个转移方程体现了 DP 的核心:

  • 对于第一行(i=0),只能从左边走过来
  • 对于第一列(j=0),只能从上边走过来
  • 对于其他位置,可以从左边或上边过来,我们选择路径和更小的那条
7. 返回值

注意:原始思路写的是"dpgridSizegridColSize\[gridSize - 1] 行的最小值",这是错误的!

正确的返回值应该是:dpgridSize-1gridColSize\[gridSize-1-1]

即右下角那个格子的值,因为题目要求必须走到右下角,而不是最后一行的任意位置。

C 语言代码实现

c 复制代码
#include <stdlib.h>

#define INF_MAX 0x7fffffff
#define MIN(a, b) ((a) < (b) ? (a) : (b))

int minPathSum(int **grid, int gridSize, int *gridColSize) {
    int i, j, result, col_size;
    int **dp;

    // 分配 dp 数组
    dp = (int **)malloc(gridSize * sizeof(int *));
    for (i = 0; i < gridSize; i++) {
        col_size = gridColSize[i];
        dp[i] = (int *)malloc(col_size * sizeof(int));
        for (j = 0; j < col_size; j++)
            dp[i][j] = INF_MAX;  // 初始化为最大值
    }

    // 设置起点
    dp[0][0] = grid[0][0];

    // 遍历整个 grid
    for (i = 0; i < gridSize; i++) {
        col_size = gridColSize[i];
        for (j = 0; j < col_size; j++) {
            if (i == 0 && j == 0)
                continue;  // 跳过起点
            
            if (i == 0)
                // 第一行:只能从左边来
                dp[i][j] = dp[i][j - 1] + grid[i][j];
            else if (j == 0)
                // 第一列:只能从上边来
                dp[i][j] = dp[i - 1][j] + grid[i][j];
            else
                // 其他位置:取左边和上边的最小值
                dp[i][j] = MIN(dp[i][j - 1] + grid[i][j],
                               dp[i - 1][j] + grid[i][j]);
        }
    }

    // 获取右下角的结果
    col_size = gridColSize[gridSize - 1];
    result = dp[gridSize - 1][col_size - 1];

    // 释放内存
    for (i = 0; i < gridSize; i++)
        free(dp[i]);
    free(dp);

    return result;
}

模板的价值

通过这个案例,我们可以看到自我提问模板的价值:

  1. 系统性:按照模板逐项分析,不容易遗漏关键要素
  2. 纠错性:在填写"返回值"时,发现了原始思路的错误
  3. 可复用:这个模板可以应用到其他 DP 问题上

当你面对新的 DP 问题时,不妨试试用这个模板梳理思路:

  • 先明确状态定义
  • 找出初始值和边界条件
  • 确定遍历顺序
  • 分析选择和代价(如果有)
  • 推导转移方程
  • 确认返回值

这样的思考流程能帮助你更快地理清思路,写出正确的代码。

总结

动态规划的难点不在于写代码,而在于理清状态和转移的逻辑。通过建立一套自我提问的模板,我们可以:

  • 将复杂问题拆解成可回答的小问题
  • 系统地分析 DP 的各个要素
  • 及时发现思路中的错误

希望这个模板能帮助你在 DP 的道路上走得更顺畅!

相关推荐
加宝的起点雷达1 小时前
40 条候选里 38 条来自同一个源,主卡整屏重样
人工智能·算法·创业
秋天的怀念_David1 小时前
算法一千零一夜 · 第二十六夜 协议的母语
java·spring boot·算法
闻缺陷则喜何志丹4 小时前
【C++特性】四种类型转
c++·算法·类型转换
喵本喵叁肆4 小时前
用 Boosting 把弱隐私算法炼成强隐私算法(英文原题:Boosting and Differential Privacy (FOCS 2010))
算法·集成学习·boosting
青梅味猪大肠5 小时前
【操作系统-60】临近适应算法
java·linux·开发语言·嵌入式硬件·算法·操作系统
其实防守也摸鱼19 小时前
个人学习 Pwn 思路:从基础到漏洞利用的进阶路径
大数据·前端·人工智能·chrome·算法·安全·网络安全
Nuanyt20 小时前
LeetCode Hot 100 链表专题(五)· 2 题(Java)
java·算法·leetcode·链表