🧩 动态规划数组维度选择指南:什么时候用 m+1?什么时候用 m?
"数组多开一格,思路少堵三天" —— 来自某位 debug 到凌晨的程序员
🔍 灵魂拷问:为什么要有两种维度?
每次写动态规划都要纠结数组维度?别慌!让我们先看两个经典场景:
🎯 场景 1:编辑距离(LeetCode 72. 编辑距离)
int[][] dp = new int[m+1][n+1]; // ✅ 多开一格的秘密武器
这里需要处理空字符串的基准情况,比如:
dp[0][3]表示把空字符串变成 3 个字符需要 3 次插入操作dp[3][0]表示删除 3 个字符变成空字符串需要 3 次删除操作
🎯 场景 2:不同路径(LeetCode 62. 不同路径)
int[][] dp = new int[m][n]; // ✅ 精准匹配网格坐标
这里 dp[2][3]直接对应网格的第 3 行第 4 列,不需要处理额外的基准状态
🗺️ 选择维度的心智模型
🌟 选择 (m+1)*(n+1) 的三大标志
- 🚩 空状态处理:就像备用的"零钱袋",专门存放空字符串/空集合等特殊状态
- 🚩 防越界护盾:当状态转移出现
i-1时,i=0会访问非法索引,+1 就是安全气囊 - 🚩 物理意义对齐:让
dp[i][j]严格对应前 i 个元素的处理结果,像刻度尺般精准
🎯 选择 m*n 的典型场景
🛠️ 实战演示:两种维度的代码对比
案例 1:编辑距离(m+1 版)
int[][] dp = new int[m+1][n+1];
// 🌟 初始化魔法在这里发生
for(int j=0; j<=n; j++) dp[0][j] = j;
// 状态转移三剑客:
dp[i][j] = Math.min(
dp[i-1][j], // 删除操作 📉
dp[i][j-1], // 插入操作 📈
dp[i-1][j-1] // 替换操作 🔄
) + 1;
案例 2:不同路径(m 版)
int[][] dp = new int[m][n];
// 🚀 简单粗暴的初始化
dp[0][0] = 1;
// 状态转移直通车:
dp[i][j] = dp[i-1][j] + dp[i][j-1];
💡 记忆口诀与快速决策
🧠 三句口诀破解谜题
- +1 是防火墙:防数组越界的终极保镖
- 空位留白艺术:给"虚无状态"一个合法席位
- 网格即坐标:当问题本身就是网格时保持纯净
🌌 三维空间决策:当问题复杂度升级时
案例:股票买卖(LeetCode 123. 买卖股票的最佳时机 III)
int[][][] dp = new int[prices.length][3][2]; // [天数][交易次数][持有状态]
// 第一维度需要+1吗?不需要!因为天数从0开始直接映射
// 但交易次数维度需要k+1?需要!因为0次交易也是一种状态
🚀 高频问题答疑
❓ 为什么背包问题多用(m+1)?
就像超市购物车,
dp[0][j]表示用 0 件商品装满购物车的方式(当然是 0 种啦),这个"空购物车"状态必须存在!
❓ 什么时候能用滚动数组?
当状态转移像接力赛跑 🏃♂️,只需要传递相邻两行数据时(如编辑距离),空间复杂度可从 O(mn)降到 O(n)
❓ 常见翻车现场
- 忘记给"零钱袋"初始化(第一行/列没填)
- 循环范围手滑写错(该用
<=时写成<) - 把字符下标和 dp 维度搞混(字符从 0 开始,dp 维度从 1 开始)
🌈 总结:维度选择的艺术
| 维度选择 | 适用场景 | 经典例题 |
|---|---|---|
(m+1)*(n+1) | 空状态/防越界/严格对齐 | 编辑距离🔄 最长子序列📏 |
m*n | 网格问题/可保护边界/空间优化 | 不同路径🛤️ 最小路径和💰 |
✨ 小技巧:下次写 DP 前,先画个状态转移草图,再对照这个表格检查,保你维度选择不再纠结!🎯