Skip to content

参考:DP 基础 API、模型与设计速查

基于通用算法概念 · 核于 2026-07

速查

  • DP 三要素:①状态定义dp[...] 代表什么);②状态转移方程(从「最后一步」倒推);③边界条件(最小初始值)。
  • 两个核心特征最优子结构(子问题最优能推出全局最优)+ 重叠子问题(子问题反复出现,需记忆化)。
  • 两种实现:自顶向下记忆化递归(贴近直觉、只算用到的子问题);自底向上递推填表(常数小、易空间优化)。
  • 斐波那契dp[i]=dp[i-1]+dp[i-2],边界 dp[1]=dp[2]=1,O(n) 时间 O(1) 空间。
  • 爬楼梯dp[i]=dp[i-1]+dp[i-2],边界 dp[1]=1, dp[2]=2,O(n) 时间 O(1) 空间。
  • 打家劫舍dp[i]=max(dp[i-1], dp[i-2]+nums[i]),O(n) 时间 O(1) 空间。
  • Kadanedp[i]=max(nums[i], dp[i-1]+nums[i]),答案取 max(dp[i]),O(n) 时间 O(1) 空间。
  • 0-1 背包dp[j]=max(dp[j], dp[j-w]+v),容量倒序,O(n·W) 时间 O(W) 空间。
  • 完全背包dp[j]=max(dp[j], dp[j-w]+v),容量正序,O(n·W) 时间 O(W) 空间。
  • 零钱兑换 II(组合数)dp[j]+=dp[j-coin],外层物品内层容量正序,dp[0]=1
  • 零钱兑换 I(最少硬币)dp[j]=min(dp[j], dp[j-coin]+1)dp[0]=0 其余 Infinity
  • 组合 vs 排列:外层物品内层容量 → 组合;外层容量内层物品 → 排列。
  • 交互演示0-1 背包完全背包零钱兑换

一、DP 三要素与两个前提

要素含义示例(爬楼梯)
状态定义dp[...] 代表什么dp[i] = 爬到第 i 阶的方案数
转移方程dp[i] 怎么由更小子问题推出dp[i] = dp[i-1] + dp[i-2]
边界条件不依赖转移的初始值dp[1]=1, dp[2]=2

两个隐性前提:最优子结构(大问题最优解 = 子问题最优解的组合)+ 无后效性(未来只依赖当前状态,不依赖到达路径)。

二、常见基础模型表

模型状态定义转移方程边界时间空间
斐波那契dp[i]=第 i 个斐波那契数dp[i]=dp[i-1]+dp[i-2]dp[1]=dp[2]=1O(n)O(1)
爬楼梯dp[i]=到 i 阶方案数dp[i]=dp[i-1]+dp[i-2]dp[1]=1, dp[2]=2O(n)O(1)
打家劫舍dp[i]=前 i 家最大金额dp[i]=max(dp[i-1], dp[i-2]+nums[i])dp[0]=0, dp[1]=nums[0]O(n)O(1)
Kadanedp[i]=以 i 结尾最大和dp[i]=max(nums[i], dp[i-1]+nums[i])dp[0]=nums[0]O(n)O(1)
0-1 背包dp[j]=容量 j 最大价值dp[j]=max(dp[j], dp[j-w]+v)dp[0..W]=0O(n·W)O(W)
完全背包dp[j]=容量 j 最大价值dp[j]=max(dp[j], dp[j-w]+v)dp[0..W]=0O(n·W)O(W)
零钱兑换 IIdp[j]=凑 j 元组合数dp[j]+=dp[j-coin]dp[0]=1O(n·W)O(W)
零钱兑换 Idp[j]=凑 j 元最少硬币dp[j]=min(dp[j], dp[j-coin]+1)dp[0]=0, 其余 ∞O(n·W)O(W)

三、背包代码模板

0-1 背包(一维倒序)

js
// 每件最多放一次,求最大价值
function knapsack01(w, v, W) {
  const dp = new Array(W + 1).fill(0);
  for (let i = 0; i < w.length; i++) {
    for (let j = W; j >= w[i]; j--) {   // 倒序——灵魂
      dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
    }
  }
  return dp[W];
}

完全背包(一维正序)

js
// 每件无限件,求最大价值
function knapsackComplete(w, v, W) {
  const dp = new Array(W + 1).fill(0);
  for (let i = 0; i < w.length; i++) {
    for (let j = w[i]; j <= W; j++) {   // 正序——灵魂
      dp[j] = Math.max(dp[j], dp[j - w[i]] + v[i]);
    }
  }
  return dp[W];
}

零钱兑换 II(组合数)

js
// 凑 amount 元的组合数(外层物品内层容量=组合)
function change(amount, coins) {
  const dp = new Array(amount + 1).fill(0);
  dp[0] = 1;
  for (const coin of coins) {
    for (let j = coin; j <= amount; j++) dp[j] += dp[j - coin];
  }
  return dp[amount];
}

四、DP 设计五步

  1. 定义状态dp[...] 代表什么(满足无后效性,包含所有影响未来的信息)。
  2. 找转移方程:从「最后一步」倒推,枚举所有可能的最后一步取最优。
  3. 定边界条件:最小的初始值(dp[0]dp[1] 等)。
  4. 确定计算顺序:保证算 dp[i] 时依赖项已就绪(一维从小到大;背包外层物品内层容量)。
  5. 优化空间:滚动数组 / 一维化(0-1 背包倒序、完全背包正序)。

五、记忆化 vs 递推速查

维度记忆化递归(自顶向下)递推填表(自底向上)
方向大问题 → 边界边界 → 大问题
形态递归 + memofor 循环 + dp 数组
子问题只算用到的全部算一遍
栈 / 常数有递归栈,可能溢出无栈,常数小
空间优化易(滚动 / 一维化)
选型状态稀疏、思路梳理要常数优势、要空间优化

六、易错点清单

  • 状态定义错:状态没包含所有影响未来的信息 → 后效性破坏 → 转移不正确(需扩展状态维度)。
  • 边界漏写 / 写错dp[0]dp[1] 必须显式给出;组合数问题 dp[0]=1(「什么都不选」算 1 种),漏了全 0。
  • 0-1 背包忘倒序:一维化后容量正序遍历会退化成完全背包(一件物品被放多次)——最高频坑。
  • 完全背包忘正序:误用倒序会导致「每件只用一次」,丢失无限件的语义。
  • 组合 vs 排列搞反:求组合数(不区分顺序)必须外层物品内层容量;求排列数(区分顺序)反过来。零钱兑换 II 用前者,爬楼梯用后者。
  • Kadane 答案取错:状态是「以 i 结尾」,答案是 max(dp[i]) 不是 dp[n-1]
  • 初始化值用错:求最大值初始化 0-∞(看是否允许空);求最小值初始化 Infinitydp[0]=0
  • 递推顺序错:区间 DP 要先短后长;背包要外层物品内层容量——顺序反了依赖项未就绪。
  • 记忆化忘判「已算过」:递归入口先查 memo,没有才计算并存——否则等于没记忆化。
  • 空间优化后引用旧值:滚动数组若用「覆盖」而非「双缓冲」,要确认覆盖顺序不破坏后续依赖(0-1 背包倒序正是为此)。
  • 混淆 dp 维度下标:物品下标 i 常与 dp 行号 i 差 1(dp[i] 对应物品 w[i-1]),off-by-one 高频。

七、如何识别 DP 题

  • 问最值:「最大/最小/最长/最短/最少」。
  • 问方案数 / 可行性:「有多少种」「能否凑成/组成」。
  • 结构信号:连续子数组、子序列、子集、划分、凑金额。
  • 能拆成语义相同的子问题且子问题重叠。
  • 三者满足 → 按「定义状态 → 找转移 → 定边界 → 定顺序 → 优化空间」五步走。

八、进阶方向(链接其他叶)

  • 序列区间 DP:最长公共子序列(LCS)、最长递增子序列(LIS)、编辑距离 —— DP 三部曲之二
  • 进阶 DP:树形 DP、数位 DP、状压 DP、换根 DP —— DP 三部曲之三
  • 回溯 / 记忆化搜索:DP 的递归视角,状态空间大时优先 —— 见回溯叶
  • 贪心:能严格证明局部最优 ⇒ 全局最优时优先 —— 见贪心叶

权威链接