🗺️

二维动态规划(DP)入门

二维 DP = 填一张格子表:每个格子的答案 = 上面格子 + 左边格子(只能向右、向下走时)。记住「转移方程」,DP 就是「按行机械填表」——填到最后一个格子,答案就出来了。

💡 打个比方

想象你站在格子地图的左上角,只能向右或向下走,想知道到右下角一共有多少条不同的路线。笨办法是把每条路线都画出来——4×4 的地图就已经有 20 条,地图再大就画不动了。DP 的办法是填一张小表:到达任何一个格子的路线数 = 到达它上面格子的路线数 + 到达它左边格子的路线数(因为只能从这两个方向走进来)。于是第一行、第一列都只能走一条路,填 1;然后一行一行、从左到右往下加——填到右下角,答案就在那里。这种「用已经算好的格子,推出当前格子」的公式叫转移方程:dp[i][j] = dp[i-1][j] + dp[i][j-1]。这里的 dp 是一张二维表(行、列对应地图的行、列),所以叫「二维 DP」。以后学 LIS、LCS、背包,全都是「换个地图,同一个填表动作」。

术语:dp 数组(存答案的表,dp[i][j] = 「规模 (i,j) 的子问题」的答案);转移方程(用已知格子推当前格子的公式);边界(不需要转移、直接填的起始格子,如第一行 = 第一列 = 1);障碍(不能走的格子,dp 值强制填 0);最小花费变体(转移改成 dp = min(上, 左) + 本格花费)。

🖼️ 大图看明白

格子地图填表:每个格子 = 上面 + 左边(只能向右、向下走) 起点→ 第1列 第2列 第3列 第4列 第1行 第2行 第3行 第4行 1 1 1 1 1 2 3 4 1 3 6 10 1 4 10 20 10 10 20 = 10 + 10 填表规则 ① 边界:第一行 = 第一列 = 1 从起点到第一行只能一直向右、到第一列只能一直向下,各只有 1 条路 ② 转移方程(内部每个格子): dp[i][j] = dp[i-1][j] + dp[i][j-1] (上面) (左边) ③ 按行、从左到右填; 答案在右下角: dp[4][4] = 20 条 为什么 = 上面 + 左边? 走到 (i,j) 的最后一步,只能从上面或左边 跨进来——所以路线数 = 两条来源的路线数之和 地图规则 左上角出发 只能向右 / 向下 目标:右下角 填表 = DP

🧩 子任务 1:格子路径计数——推导转移方程

🎯 小目标:看懂「每个格子 = 上面 + 左边」为什么成立,会写转移方程 dp[i][j] = dp[i-1][j] + dp[i][j-1],会手填 3×3 的表。
// 为什么是「上面 + 左边」?
你只能向右或向下走。要走进格子 (i,j),
最后一步【只能】从两个地方跨进来:
  · 上面的格子 (i-1,j)  —— 向下跨一步
  · 左边的格子 (i,j-1)  —— 向右跨一步
所以:到 (i,j) 的路线数
      = 到 (i-1,j) 的路线数 + 到 (i,j-1) 的路线数

// 转移方程
dp[i][j] = dp[i-1][j] + dp[i][j-1]

// 边界:第一行、第一列 = 1
到第一行任意格子:只能一直向右 → 恰好 1 条路
到第一列任意格子:只能一直向下 → 恰好 1 条路

// 手填 3×3(按行、从左到右)
        第1列  第2列  第3列
第1行:    1      1      1
第2行:    1      2      3      dp[2][3] = dp[1][3] + dp[2][2] = 1 + 2 = 3
第3行:    1      3      6      dp[3][3] = dp[2][3] + dp[3][2] = 3 + 3 = 6

// 答案:3×3 格子,左上到右下 = 6 条路
✅ 小检查:① dp[2][3] = ?(写出加法)② 填表时,为什么算 dp[3][2] 不能用 dp[3][3]?③ 2×2 格子有几条路?
看看答案 ① dp[2][3] = dp[1][3] + dp[2][2] = 1 + 2 = 3。② 填表按行从左到右,dp[3][3] 在 dp[3][2] 右边,还没填出来;dp[3][2] = dp[2][2] + dp[3][1] = 2 + 1 = 3。③ dp[2][2] = dp[1][2] + dp[2][1] = 1 + 1 = 2(先右后下、先下后右,正好两条)。
易错:边界格子(第一行、第一列)必须填 1 而不是 0——它们有且只有一条路。如果填 0,整个表加出来全是 0。

🧩 子任务 2:手填 4×4 全表 & 组合数验证

🎯 小目标:完整手填 4×4 表(答案 20),会用「6 步里选 3 步向下」的组合数 C(6,3) 交叉验证。
// 4×4 全表(边界 = 1,内部 = 上面 + 左边)
        第1列  第2列  第3列  第4列
第1行:    1      1      1      1
第2行:    1      2      3      4
第3行:    1      3      6      10
第4行:    1      4      10     20  ← 答案 dp[4][4]

// 抽一格验算:dp[4][4] = dp[3][4] + dp[4][3] = 10 + 10 = 20 ✓

// 组合数交叉验证(另一条思路)
从 (1,1) 到 (4,4):要向下 (4-1) = 3 步,向右 (4-1) = 3 步,共 6 步。
每一步的「顺序」唯一决定一条路线,所以
路线数 = 在 6 步里选哪 3 步向下 = C(6,3)
      = 6! / (3! · 3!)
      = (6×5×4) / (3×2×1)
      = 20  ✓ 与填表结果一致
✅ 小检查:① dp[4][3] = ?② 5×5 格子有几条路?(提示:C(8,4))③ 表里为什么每一行从左到右严格变大?
看看答案 ① 10(dp[3][3] + dp[4][2] = 6 + 4)。② 4 下 + 4 右 = 8 步,C(8,4) = (8×7×6×5)/(4×3×2×1) = 70。③ 每个格子 = 上面 + 左边,两项都是正数,且向右走一步「可选的组合」只会更多——所以单调变大。
易错:组合数验证里,「向下的步数」= 行数 − 1,「向右的步数」= 列数 − 1——不是行数和列数本身。4×4 是 C(6,3),不是 C(8,4)(C(8,4) 是 5×5 的)。

🧩 子任务 3:障碍格——填 0 继续

🎯 小目标:某个格子不能走时,它的 dp 值填 0,其余格子照常「上面 + 左边」——会手填带障碍的 4×4 表。
// 障碍在 (2,2):路线不能踩这个格子
方法:填表遇到障碍格,直接写 0,后面照常算。

        第1列  第2列  第3列  第4列
第1行:    1      1      1      1
第2行:    1      0*     1      2    ← (2,2) 是障碍,填 0
第3行:    1      1      2      4
第4行:    1      2      4      8    ← 答案 dp[4][4] = 8

// 关键格验算
dp[2][3] = dp[1][3] + dp[2][2] = 1 + 0 = 1  // 上面的路被障碍「掐断」,只剩左边
dp[3][2] = dp[2][2] + dp[3][1] = 0 + 1 = 1
dp[3][3] = dp[2][3] + dp[3][2] = 1 + 1 = 2
dp[4][4] = dp[3][4] + dp[4][3] = 4 + 4 = 8  ✓

// 对比:没有障碍时 dp[4][4] = 20
// 一个障碍,路线从 20 条掉到 8 条——堵在路口正中,影响巨大
✅ 小检查:① 障碍在 (2,2) 时,dp[4][3] = ?② 如果障碍在 (1,2)(第一行),第一行会变成什么样?第 2 行还能绕过去吗?③ 起点 (1,1) 是障碍,有几条路?
看看答案 ① dp[4][3] = dp[3][3] + dp[4][2] = 2 + 2 = 4。② 第一行变成 1 0 0 0——从 (1,1) 走不出去,第一行第 2 列起全是 0;但第 2 行可以先向下再向右绕过去,dp[2][3] = dp[1][3] + dp[2][2] = 0 + 2 = 2,照样有路。③ 0 条——起点都站不住,一条路也走不成(dp[1][1] = 0,整张表加出来全是 0)。
易错:障碍格的 0 是直接填的,不是算出来的——写代码时「判断障碍」必须在「上面 + 左边」之前。先加后判,障碍格就变成非 0 了。

🧩 子任务 4:最小花费——min(上, 左) + 本格

🎯 小目标:把「数路」换成「找最便宜的路」:转移方程改成 dp[i][j] = min(上面, 左边) + cost[i][j],会手填 4×4 花费表(答案 10)。
// 花费地图(踩每个格子要付的费):
        第1列  第2列  第3列  第4列
第1行:    1      3      1      2
第2行:    2      1      4      1
第3行:    1      2      1      3
第4行:    3      1      2      1

// 转移方程变了:不再「相加」,而是「选便宜的一边」
dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + cost[i][j]
边界:dp[1][1] = cost[1][1] = 1(起点的花费也要付)

// 手填花费 DP 表(每格 = min(上, 左) + 本格花费):
        第1列  第2列  第3列  第4列
第1行:    1      4      5      7    只能从左边来:1+3, 4+1, 5+2
第2行:    3      4      8      8    dp[2][2] = min(4,3)+1 = 4
第3行:    4      6      7      10   dp[3][3] = min(8,6)+1 = 7
第4行:    7      7      9      10   ← 最小花费 = 10

// 验算 dp[4][4] = min(dp[3][4], dp[4][3]) + cost[4][4]
//          = min(10, 9) + 1 = 9 + 1 = 10 ✓
✅ 小检查:① dp[2][2] = ?(写出 min 的取舍)② 为什么这里用 min 而不是「+」?③ 哪条路径的花费是 10?试着从 (4,4) 沿「选 min 的那一边」倒推回去。
看看答案 ① dp[2][2] = min(dp[1][2], dp[2][1]) + cost[2][2] = min(4, 3) + 1 = 4(从左边来更便宜)。② 「数路」要的是所有路线,两条来源都要数,所以相加;「最小花费」只要一条最便宜的,所以取 min。③ 从 (4,4) 倒推:min(上 10, 左 9) → 选左 (4,3);min(上 7, 左 7) → 任选,走上 (3,3);min(上 8, 左 6) → 选左 (3,2);min(上 4, 左 4) → 走上 (2,2);min(上 4, 左 3) → 选左 (2,1);再上 (1,1)。路径:(1,1)→(2,1)→(2,2)→(3,2)→(3,3)→(4,3)→(4,4),花费 1+2+1+2+1+2+1 = 10 ✓。
易错:起点 dp[1][1] 要填 cost[1][1](本例是 1),不是 0——起点的花费也要付。第一行/第一列同理:只能从左边/上面来,dp = 前一个 + 本格。

💻 完整代码:最小花费格子(4×4)

// 题目:4×4 花费地图,从 (1,1) 走到 (4,4),只能向右/向下,
//       求最小花费(每个格子踩上去都要付 cost)。
// 方法:dp[i][j] = min(上面, 左边) + cost[i][j],逐行填表。
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 4;
int cost[N+1][N+1] = {
    {0,  0,   0,   0,   0  },
    {0,  1,   3,   1,   2  },
    {0,  2,   1,   4,   1  },
    {0,  1,   2,   1,   3  },
    {0,  3,   1,   2,   1  }
};

int main() {
    int dp[N+1][N+1] = {};
    dp[1][1] = cost[1][1];                 // 边界:起点的花费也要付
    for (int i = 1; i <= N; i++) {
        for (int j = 1; j <= N; j++) {
            if (i == 1 && j == 1) continue;
            int fromTop  = (i > 1) ? dp[i-1][j]   : 1e9;  // 第 1 行没有「上面」
            int fromLeft = (j > 1) ? dp[i][j-1]   : 1e9;  // 第 1 列没有「左边」
            dp[i][j] = min(fromTop, fromLeft) + cost[i][j];
        }
    }
    // 打印 dp 表
    for (int i = 1; i <= N; i++) {
        for (int j = 1; j <= N; j++) cout << dp[i][j] << " ";
        cout << endl;
    }
    cout << "最小花费 = " << dp[N][N] << endl;
    return 0;
}
// 运行结果:
// 1 4 5 7
// 3 4 8 8
// 4 6 7 10
// 7 7 9 10
// 最小花费 = 10
// 改成「数路」只需两步:dp[1][1] = 1;dp[i][j] = fromTop + fromLeft(相加)。

✋ 你来试试

第 1 题:3 行 4 列的格子(无障碍),从左上到右下有几条路? 2 下 + 3 右 = 5 步,C(5,2) = 10。也可以填表验证:第一行全 1;dp[2][4] = 4,dp[3][4] = dp[2][4] + dp[3][3] = 4 + 6 = 10。
第 2 题:4×4 格子,障碍在 (2,3)。手填整张表,dp[4][4] = ? 第1行: 1 1 1 1;第2行: 1 2 0 1;第3行: 1 3 3 4;第4行: 1 4 7 11。dp[4][4] = dp[3][4] + dp[4][3] = 4 + 7 = 11。
第 3 题:子任务 4 的花费地图里,把 cost[1][2] 从 3 改成 100,最小花费是多少?为什么? 还是 10。最优路径根本不踩 (1,2)——它绕道 (2,1)→(2,2)(花费 3+1)比走 (1,2)(花费 100)便宜得多。DP 表里 dp[2][2] = min(dp[1][2], dp[2][1]) + 1,自动选了更便宜的那边。「绕开贵格子」正是 min 的威力。
第 4 题(加餐):3×3 格子有几条路?用组合数验证。 2 下 + 2 右 = 4 步,C(4,2) = 6。填表结果也是 6(子任务 1 的表)。
🎯 一句话记住:二维 DP = 填格子表——边界先填好(数路:第一行第一列 = 1;最小花费:起点 = cost),转移方程跟着问题走(数路 = 上 + 左,最小花费 = min(上, 左) + 本格),按行从左到右填到最后一个格子,答案就在右下角。