面试知识库
困难

动态规划基础#

一句话答案#

DP 四步:定义状态→写转移方程→确定初始值→确定遍历顺序,核心条件:最优子结构+重叠子问题。

核心要点

经典模型:

模型例题转移
线性DP最长递增子序列dp[i]=max(dp[j]+1)
背包0-1背包dp[i][w]=max(不选,选)
区间DP戳气球dp[i][j]=max(dp[i][k]+dp[k][j])

空间优化: 二维→一维滚动数组

0-1 背包滚动数组为何逆序#

二维原型: dp[i][w] = max(dp[i-1][w], dp[i-1][w-c] + v),决策「第 i 个物品选不选」只依赖上一行(i-1) 的两个值。

降一维的关键: 滚一维 dp[w] 复用上一行。问题在于把外层 i 抹掉后,dp[w] 是「正在被本轮覆盖」的数组——遍历容量 w 的方向决定 dp[w-c] 取到的是上一行还是本行。

❌ 正序遍历(w 从小到大)会出错:

  • dp[w]dp[w-c](w-c < w)本轮已经被更新过,已经是「考虑了第 i 个物品」的值
  • dp[w] = dp[w-c] + v 相当于在「已含第 i 个物品」的基础上再加一次 → 同一物品被选了多次,退化成完全背包

✅ 逆序遍历(w 从大到小)才正确:

  • dp[w]dp[w-c](w-c < w)本轮还没被触碰,仍是上一行(只考虑前 i-1 个物品)的值
  • 严格对应 dp[i-1][w-c],保证每个物品只被选一次

一句话:逆序让 dp[w-c] 保持「旧值(上一个物品的状态)」,正序则会读到「本物品刚写入的新值」从而重复选。

对比|完全背包正是要正序:

遍历方向dp[w-c] 含义物品可选次数
0-1 背包容量逆序上一行(前 i-1 个)每个最多 1 次
完全背包容量正序本行(已含第 i 个)每个无限次

完全背包 dp[i][w]=max(dp[i-1][w], dp[i][w-c]+v) 决策依赖本行 dp[i][w-c],所以正序「读到刚更新的值」恰好就是它想要的——同一物品重复选正是目的。两者只差遍历方向一个字。

面试回答(2分钟版)

动态规划解题我遵循四步法:定义状态、写转移方程、确定初始值、确定遍历顺序。第一步是最关键的,dp[i]代表什么必须定义清楚,比如最长递增子序列中dp[i]表示以nums[i]结尾的最长递增子序列长度。第二步写转移方程描述dp[i]和之前状态的关系,比如dp[i]=max(dp[j]+1)其中j<i且nums[j]<nums[i]。第三步确定初始值也就是base case,比如dp[0]=1。第四步确定遍历顺序要保证计算dp[i]时它依赖的状态都已经算过了。能用DP的前提是两个条件:最优子结构即大问题的最优解包含子问题的最优解,重叠子问题即同一个子问题会被反复计算。和贪心的区别在于DP会回头看所有子问题取全局最优,贪心只看当前最优不回头。经典模型有线性DP、0-1背包、区间DP等。空间优化上如果dp[i]只依赖dp[i-1]可以用滚动数组把二维降到一维,但要注意遍历方向,0-1背包逆序遍历才能避免重复使用。

追问与易错

追问方向:

  • “DP 和贪心的区别?”→ 贪心每步选局部最优且不回头,DP 需要考虑所有子问题的组合求全局最优;贪心需要证明贪心选择性质,DP 需要最优子结构 + 重叠子问题
  • “怎么判断一个题能用 DP?”→ 看三个特征:①问最值/方案数/可行性 ②能拆分成规模更小的子问题 ③子问题有重叠(同一子问题被反复求解);如果无重叠子问题可能是分治
  • “空间优化的思路?”→ 观察状态转移方程的依赖关系:若 dp[i] 只依赖 dp[i-1],可用滚动数组降为一维;二维 DP 若只依赖上一行可压缩为两行或一行(注意遍历方向)
  • “0-1 背包一维数组为什么必须逆序遍历容量?”→ 一维 dp[w] 复用上一行;正序时 dp[w-c] 本轮已被更新(含本物品),相当于物品被重复选 → 退化成完全背包;逆序时 dp[w-c] 还是上一行的旧值(前 i-1 个物品),保证每个物品只选一次。完全背包反过来用正序,正是要重复选

易错点:

  • ❌ DP 能解决所有优化问题——需要最优子结构+重叠子问题
  • ❌ 二维 DP 都能优化为一维——取决于依赖关系