langShiftlangShift

多维动态规划(题单顺序)

热题 100 - 多维动态规划分组(按题单顺序):62 / 64 / 5 / 1143 / 72

本页包含题单「多维动态规划」分组的全部题目,顺序与 list.json 保持一致。

多维 DP 统一“落地步骤”(点击展开)
  • 状态dp[i][j] 表示什么(一定要带上“含义 + 范围”)。
  • 转移dp[i][j] 从哪些更小状态来,为什么不会漏/重。
  • 初始化:边界(第一行/第一列/空串/长度为 0)怎么设,避免越界。
  • 遍历顺序:保证用到的状态已经算好(常见是从小到大)。
  • 优化:能否滚动数组(把二维压成一维),以及滚动时更新方向是否会污染数据。

62. 不同路径

力扣原题:62. 不同路径
Medium
数学
动态规划
组合数学

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

 

示例 1:

输入:m = 3, n = 7
输出:28

示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

输入:m = 7, n = 3
输出:28

示例 4:

输入:m = 3, n = 3
输出:6

 

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 109
最优解(二维 DP:dp[i][j]=dp[i-1][j]+dp[i][j-1],可滚动成一维)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 题意:你站在左上角,每次只能往右或往下走一步,问到右下角一共有多少种不同的走法?
  • 核心想法:走到某一个格子时,你只能从它的上面一格下来,或者从它的左边一格过来,没有别的路。所以「到达这个格子的走法数」=「到达上面那格的走法数」+「到达左边那格的走法数」。写成式子就是:dp[i][j] = dp[i-1][j] + dp[i][j-1]
  • 边界:第一行只能一直往右走,第一列只能一直往下走,所以第一行、第一列每个格子都只有 1 种走法,初始化为 1。
  • 空间优化:算每一行时只用得到「上一行」的结果,所以可以用一维数组「滚动」:dp[j] 表示当前行第 j 列有多少种走法,更新时用「上一行的 dp[j]」加上「当前行左边的 dp[j-1]」,即 dp[j] = dp[j] + dp[j-1]
类似题目(网格 DP:来自上/左)

思路相同:当前格子的值只依赖「上面」和「左边」,转移时用一维滚动即可,例如:

dp[j] = dp[j] + dp[j-1]

64. 最小路径和

力扣原题:64. 最小路径和
Medium
数组
动态规划
矩阵

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

 

示例 1:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

示例 2:

输入:grid = [[1,2,3],[4,5,6]]
输出:12

 

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 200
最优解(二维 DP:dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j])
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 和 62 一样,只是把“路径条数”换成“路径代价最小”。
  • grid[i][j] 的最小代价只能来自上或左的最小代价,再加上当前格子权重。
  • 一维滚动时:
    • dp[j] 表示从上方来的最小和
    • dp[j-1] 表示从左方来的最小和(因为已更新到当前行)
    • 所以 dp[j] = min(dp[j], dp[j-1]) + grid[i][j]
类似题目(网格最短路 DP:来自上/左取 min)
dp[j] = min(dp[j], dp[j-1]) + cost[i][j]

5. 最长回文子串

力扣原题:5. 最长回文子串
Medium
双指针
字符串
动态规划

给你一个字符串 s,找到 s 中最长的 回文 子串

 

示例 1:

输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:

输入:s = "cbbd"
输出:"bb"

 

提示:

  • 1 <= s.length <= 1000
  • s 仅由数字和英文字母组成
最优解(中心扩展:每个中心向两边扩,O(n^2) 时间 O(1) 空间)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)
  • 回文的定义:左右对称。
  • 关键推理:最长回文一定有一个“中心”
    • 奇数长度回文中心是一个字符(比如 aba 的中心是 b)
    • 偶数长度回文中心在两个字符之间(比如 abba 的中心在 bb 之间)
  • 所以我们枚举每个可能中心,然后向两边扩展,直到不再相等。
  • 为什么是 (O(n^2)):中心有 (O(n)) 个,每次扩展最坏 (O(n))。
  • 这题也可以用二维 DP 表 pal[i][j],同样是 (O(n^2)),中心扩展更省空间、更直观。
类似题目(中心扩展模板)
for center in all:
expand(center, center)
expand(center, center+1)

1143. 最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

 

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。

示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。

 

提示:

  • 1 <= text1.length, text2.length <= 1000
  • text1 和 text2 仅由小写英文字符组成。
最优解(二维 DP:dp[i][j] 表示前 i/前 j 的 LCS 长度)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)

题目在问什么?

想象有两排字母,顺序固定。你从第一排里挑几个、从第二排里也挑几个,要挑出一模一样的一串(顺序一致)。问:这串最长能有多长

例:"abcde""ace",能挑出 "ace",长度 3。


推理的起点:先解决“小一点”的问题

如果只让你看两串的前几格,你也会算“这两小段里最长公共串多长”。我们就把这个答案记在一张表里:dp[i][j] = text1 前 i 个 与 text2 前 j 个 的最长公共子序列长度

表从左到右、从上到下填。填到 (i, j) 时,左边、上边、左上的格子都早就填好了,我们只要想:多出来的这两个字符(text1 的第 i 个 和 text2 的第 j 个)该怎么用?


情况一:这两个字符刚好相同

那就太好了:它们可以一起配对,算进公共串里。
“前面的部分”我们早就算过了,就是左上角那一格 dp[i-1][j-1]。所以当前格 = 左上 + 1:

  • dp[i][j] = dp[i-1][j-1] + 1

(就像两个人最后一位对上号了,长度自然多 1。)


情况二:这两个字符不一样

公共子序列的意思是:从两串里挑出同一串,也就是两边的字符要能一一配对(相同的才能配)。现在最后这两个字符不同,配不成一对,所以没法把“这两个”同时都算进公共串里。

那我们只能选一种「放弃」方式:要么放弃 text1 的最后这个(相当于只考虑 text1 前 i-1 个 和 text2 前 j 个),答案在上面那格 dp[i-1][j];要么放弃 text2 的最后这个(只考虑 text1 前 i 个 和 text2 前 j-1 个),答案在左边那格 dp[i][j-1]。我们要的是最长公共串,所以取这两种里更大的:

  • dp[i][j] = max(上面, 左边) = max(dp[i-1][j], dp[i][j-1])

小结

  • 最后一位相同 → 左上 +1(配对成功)。
  • 最后一位不同 → 上面、左边取 max(只能丢掉其中一串的最后一位,选更优的那边)。

填完表,右下角 dp[m][n] 就是答案。

类似题目(字符串二维 DP:末尾相同走对角,不同取上/左 max)

套路一样:看两串「末尾」是否相同,相同就对角 +1,不同就从上/左取最大:

if (a[i - 1] === b[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1
else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1])

72. 编辑距离

力扣原题:72. 编辑距离
Medium
字符串
动态规划

给你两个单词 word1 和 word2请返回将 word1 转换成 word2 所使用的最少操作数  。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

 

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

 

提示:

  • 0 <= word1.length, word2.length <= 500
  • word1word2 由小写英文字母组成
最优解(二维 DP:插入/删除/替换 三选一)
正在加载编辑器...
最优解讲解(通俗版 + 推理过程)

这题是字符串 DP 里的另一座大山,但本质和 LCS 是亲兄弟。掌握它,你对「字符串怎么一点点变形」会理解得非常透。


题目在干嘛?

给你两个单词:word1 → 变成 → word2。每次只能做 3 种操作:

  1. 插入一个字符
  2. 删除一个字符
  3. 替换一个字符

问:最少要几步?

例如 horse → ros 的一种做法:

  • horse → rorse(替换 h→r)
  • rorse → rose(删除 r)
  • rose → ros(删除 e)
    答案 = 3。

核心思路(一句话)

把大问题拆成:前 i 个字符变成前 j 个字符要几步


DP 定义

dp[i][j] = 把 word1 前 i 个字符 变成 word2 前 j 个字符最少操作数

注意:是“前 i 个”,不是下标 i(所以 i、j 从 1 到 m、n,0 留给空串)。


初始化(边界)

情况含义
dp[0][j]空串 → word2 前 j 个需要插入 j 次 → j
dp[i][0]word1 前 i 个 → 空串需要删除 i 次 → i

状态转移(灵魂所在)

最后一个字符:word1[i-1] 和 word2[j-1]。

情况 1:两个字符相同

那最后这个字符不用管,等于没变:dp[i][j] = dp[i-1][j-1](前面怎么变,现在就怎么变)。

情况 2:字符不同

必须做一次操作(插入 / 删除 / 替换),取步数最少的一种:

操作含义转移
替换把 word1[i-1] 换成 word2[j-1]dp[i-1][j-1] + 1
删除删掉 word1[i-1],word1 少一个再去匹配 word2dp[i-1][j] + 1
插入在 word1 末尾插入 word2[j-1],相当于 word2 少一个先匹配dp[i][j-1] + 1

为什么对应这个格子?

  • dp[i-1][j] + 1(删除):当前是“word1 前 i 个”对“word2 前 j 个”。若删掉 word1 的最后一个,剩下的问题就是“word1 前 i-1 个 → word2 前 j 个”,答案已经算在 dp[i-1][j] 里,再加这一次删除就是 dp[i-1][j] + 1。
  • dp[i][j-1] + 1(插入):若在 word1 末尾插入 word2[j-1],插入后末尾已经对齐;等价于先解决“word1 前 i 个 → word2 前 j-1 个”(dp[i][j-1]),再用 1 步插入补上 word2 的最后一个。
  • dp[i-1][j-1] + 1(替换):若把 word1[i-1] 替换成 word2[j-1],两边末尾就一样了;等价于先解决“word1 前 i-1 个 → word2 前 j-1 个”(dp[i-1][j-1]),再加这 1 次替换。

取最小dp[i][j] = 1 + min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1])


过程直观理解

可以理解为:从左上角走到右下角,每一步都在做「最省操作的选择」。代码见上方最优解。

复杂度:时间 O(m×n),空间 O(m×n)(可优化到 O(n))。


和 LCS 的关系

LCS编辑距离
末尾相同配对,+1不操作,继承左上
末尾不同取 max(丢一边)取 min + 1(替换/删/插)
目标求最长“保留”求最少“修改”

LCS 是「尽量不改」,编辑距离是「最少改几步」,本质都是在比较两个字符串的对齐方式


最通俗总结

  • 字符一样 → 继承左上角。
  • 字符不一样 → 从「替换、删除、插入」里选最便宜的一步。

延伸:若只允许插入和删除(不允许替换),有结论:编辑距离 = len1 + len2 − 2×LCS,能把两道题直接打通。

类似题目(编辑类 DP:三操作取 min)
if same: dp[i][j]=dp[i-1][j-1]
else dp[i][j]=1+min(del, ins, rep)