← 返回文档列表

最小路径和 - 从零开始理解二维DP

二维数组让你蒙了?没关系!我们从最简单的例子开始,一步步理解二维动态规划的思路。


🎯 问题描述

给你一个二维网格,每个格子里有一个数字,你要从左上角走到右下角,每次只能向右向下移动一步,求路径上数字和的最小值。

📝 举个简单例子

网格:
[1, 3, 1]
[1, 5, 1] 
[4, 2, 1]

目标:从 (0,0) 走到 (2,2),找最小路径和

🤔 先用人脑思考

第一步:画出所有可能的路径

起点(0,0) → 终点(2,2)

路径1: 1 → 3 → 1 → 1 → 1 = 7
       ↓   ↓   →   →   
路径2: 1 → 3 → 5 → 1 → 1 = 11  
       ↓   →   ↓   →
路径3: 1 → 1 → 5 → 1 → 1 = 9
       →   ↓   ↓   →
路径4: 1 → 1 → 4 → 2 → 1 = 9
       →   →   ↓   ↓
路径5: 1 → 1 → 1 → 2 → 1 = 6  ← 最小!
       →   →   →   ↓
路径6: 1 → 1 → 1 → 5 → 1 = 9
       →   →   ↓   ↓

答案是6,但这样枚举太累了!能不能更聪明一点?


💡 动态规划的核心思想

关键洞察:

到达任意一个格子的最小路径和 = 该格子的值 + 到达该格子前一步的最小路径和

因为只能向右或向下移动,所以到达格子(i,j)只有两种可能:

  • 从上面(i-1,j)向下走来
  • 从左边(i,j-1)向右走来

选择这两种方式中路径和更小的那个!


🔍 一步步分析过程

第一步:理解状态定义

// dp[i][j] 表示:从起点(0,0)到达位置(i,j)的最小路径和

第二步:手工计算每个位置

让我们用一个表格来记录每个位置的最小路径和:

原始网格:
[1, 3, 1]
[1, 5, 1]
[4, 2, 1]

DP表格(一步步填充):
[?, ?, ?]
[?, ?, ?]
[?, ?, ?]

🚀 开始填充:

1. 起点 (0,0)

dp[0][0] = 1  // 起点就是网格[0][0]的值

[1, ?, ?]
[?, ?, ?]
[?, ?, ?]

2. 第一行(只能向右走)

dp[0][1] = dp[0][0] + grid[0][1] = 1 + 3 = 4
dp[0][2] = dp[0][1] + grid[0][2] = 4 + 1 = 5

[1, 4, 5]
[?, ?, ?]
[?, ?, ?]

3. 第一列(只能向下走)

dp[1][0] = dp[0][0] + grid[1][0] = 1 + 1 = 2
dp[2][0] = dp[1][0] + grid[2][0] = 2 + 4 = 6

[1, 4, 5]
[2, ?, ?]
[6, ?, ?]

4. 其他位置(可以从上面或左边来)

位置 (1,1):

从上面来:dp[0][1] + grid[1][1] = 4 + 5 = 9
从左边来:dp[1][0] + grid[1][1] = 2 + 5 = 7
选择更小的:dp[1][1] = min(9, 7) = 7

[1, 4, 5]
[2, 7, ?]
[6, ?, ?]

位置 (1,2):

从上面来:dp[0][2] + grid[1][2] = 5 + 1 = 6
从左边来:dp[1][1] + grid[1][2] = 7 + 1 = 8
选择更小的:dp[1][2] = min(6, 8) = 6

[1, 4, 5]
[2, 7, 6]
[6, ?, ?]

位置 (2,1):

从上面来:dp[1][1] + grid[2][1] = 7 + 2 = 9
从左边来:dp[2][0] + grid[2][1] = 6 + 2 = 8
选择更小的:dp[2][1] = min(9, 8) = 8

[1, 4, 5]
[2, 7, 6]
[6, 8, ?]

位置 (2,2):

从上面来:dp[1][2] + grid[2][2] = 6 + 1 = 7
从左边来:dp[2][1] + grid[2][2] = 8 + 1 = 9
选择更小的:dp[2][2] = min(7, 9) = 7

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

最终答案:dp[2][2] = 7


💻 代码实现(超详细注释)

function minPathSum(grid) {
  const m = grid.length;    // 行数
  const n = grid[0].length; // 列数
  
  // 创建DP表格,dp[i][j]表示到达(i,j)的最小路径和
  const dp = Array(m).fill().map(() => Array(n).fill(0));
  
  // 第1步:设置起点
  dp[0][0] = grid[0][0];
  console.log('起点:', dp[0][0]);
  
  // 第2步:填充第一行(只能向右走)
  for (let j = 1; j < n; j++) {
    dp[0][j] = dp[0][j - 1] + grid[0][j];
    console.log(`第一行 (0,${j}):`, dp[0][j]);
  }
  
  // 第3步:填充第一列(只能向下走)
  for (let i = 1; i < m; i++) {
    dp[i][0] = dp[i - 1][0] + grid[i][0];
    console.log(`第一列 (${i},0):`, dp[i][0]);
  }
  
  // 第4步:填充其他位置(从上面或左边来,选择更小的)
  for (let i = 1; i < m; i++) {
    for (let j = 1; j < n; j++) {
      const fromTop = dp[i - 1][j] + grid[i][j];    // 从上面来
      const fromLeft = dp[i][j - 1] + grid[i][j];   // 从左边来
      
      dp[i][j] = Math.min(fromTop, fromLeft);       // 选择更小的
      
      console.log(`位置 (${i},${j}): 从上=${fromTop}, 从左=${fromLeft}, 选择=${dp[i][j]}`);
    }
  }
  
  // 打印最终的DP表格
  console.log('最终DP表格:');
  dp.forEach((row, i) => {
    console.log(`第${i}行:`, row);
  });
  
  return dp[m - 1][n - 1]; // 返回右下角的值
}

// 测试
const grid = [
  [1, 3, 1],
  [1, 5, 1],
  [4, 2, 1]
];

console.log('网格:');
grid.forEach((row, i) => {
  console.log(`第${i}行:`, row);
});

console.log('\n开始计算...\n');
const result = minPathSum(grid);
console.log('\n最小路径和:', result);

🎨 可视化理解

用箭头表示最优路径:

原始网格:        DP表格:         最优路径:
[1, 3, 1]       [1, 4, 5]       [→, →, ↓]
[1, 5, 1]   →   [2, 7, 6]   →   [?, ?, →]  
[4, 2, 1]       [6, 8, 7]       [?, ?, ✓]

最优路径:(0,0) → (0,1) → (0,2) → (1,2) → (2,2)
路径和:1 + 3 + 1 + 1 + 1 = 7

🧠 理解要点

1. 为什么用二维数组?

  • 因为问题本身就是二维的(网格)
  • 每个位置的最优解都需要保存
  • 后面的计算需要用到前面的结果

2. 状态转移方程

dp[i][j] = Math.min(
  dp[i-1][j] + grid[i][j],  // 从上面来
  dp[i][j-1] + grid[i][j]   // 从左边来
)

3. 边界条件

  • 第一行:只能从左边来
  • 第一列:只能从上面来
  • 起点:就是原始值

4. 计算顺序

  • 必须从左上角开始
  • 按行或按列依次计算
  • 保证计算当前位置时,依赖的位置已经算好了

🚀 空间优化版本

理解了基本思路后,我们可以优化空间复杂度:

function minPathSumOptimized(grid) {
  const m = grid.length;
  const n = grid[0].length;
  
  // 只需要一行的空间!
  const dp = [...grid[0]]; // 复制第一行
  
  console.log('初始化第一行:', dp);
  
  // 逐行更新
  for (let i = 1; i < m; i++) {
    // 更新第一列
    dp[0] += grid[i][0];
    console.log(`第${i}行开始,第一列更新为:`, dp[0]);
    
    // 更新其他列
    for (let j = 1; j < n; j++) {
      // dp[j] 是从上面来的值(上一行的结果)
      // dp[j-1] 是从左边来的值(当前行已更新的结果)
      dp[j] = Math.min(dp[j], dp[j - 1]) + grid[i][j];
      console.log(`  位置(${i},${j})更新为:`, dp[j]);
    }
    
    console.log(`第${i}行完成:`, [...dp]);
  }
  
  return dp[n - 1];
}

// 测试优化版本
console.log('\n=== 空间优化版本 ===');
const result2 = minPathSumOptimized(grid);
console.log('最小路径和:', result2);

🎯 练习建议

1. 手工计算

先用纸笔手工计算几个小例子,理解每一步的逻辑。

2. 修改例子

试试不同的网格,看看结果如何变化:

// 简单例子
const grid1 = [
  [1, 2],
  [3, 4]
];

// 复杂例子  
const grid2 = [
  [1, 4, 8, 6, 2],
  [2, 7, 3, 8, 4],
  [8, 4, 2, 6, 5]
];

3. 调试技巧

  • 打印每一步的计算过程
  • 画出DP表格的填充过程
  • 验证边界条件的处理

🔗 相关问题

掌握了最小路径和,你就能解决这些类似问题:

  1. 不同路径:计算有多少种走法
  2. 不同路径II:有障碍物的情况
  3. 最大路径和:求最大值而不是最小值
  4. 三角形最小路径和:三角形形状的网格

💡 总结

二维DP的核心思路:

  1. 状态定义dp[i][j] 表示到达位置(i,j)的最优解
  2. 状态转移:当前位置的最优解 = 当前值 + 前面位置的最优解
  3. 边界处理:第一行、第一列需要特殊处理
  4. 计算顺序:从左上角开始,逐行或逐列计算

记住这个模板,所有二维DP问题都是这个套路!


下次遇到二维DP,不要慌!先画个小例子,手工算一遍,理解了思路再写代码。 🎉