🧠 动态规划不是“动态”打怪,而是“稳如老狗”的刷题秘籍!

5 阅读5分钟

从斐波那契到股票买卖,DP 入门到放弃(不,是精通)的快乐之旅!


大家好,我是你们的 DP 路人甲(不是那个甲骨文的甲),今天带大家从零开始征服力扣动态规划专题。别被“动态规划”这四个字吓到——它听起来像是某种高深算法黑话,其实本质就是:把大问题拆成小问题,用空间换时间,避免重复劳动

而我们今天要做的,就是把这些“小问题”串起来,变成你刷题时的肌肉记忆


🔥 为什么 DP 让人又爱又恨?

  • :一旦掌握套路,很多看似复杂的题目秒变送分题。
  • :状态转移方程写不出来、边界条件搞不清、dp 数组定义反了……

但别慌!DP 的核心就三步:

  1. 定义状态(dp[i] 或 dp[i][j] 到底代表啥?)
  2. 找出状态转移方程(怎么从前面的状态推出来当前状态?)
  3. 确定初始值和遍历顺序(别把背包问题写成完全背包!)

接下来,我们就用真实力扣题目 + 幽默解读,带你打通任督二脉!


🐣 第一关:DP 入门三件套

✅ 509. 斐波那契数(Fibonacci)

var fib = function(n) {
  let db = []; db[0] = 0; db[1] = 1;
  for(let i = 2; i <= n; i++) {
    db[i] = db[i-1] + db[i-2];
  }
  return db[n];
};

解读:最经典的递推入门题。记住:别用递归!会爆栈! 用数组存中间结果,效率拉满。


✅ 70. 爬楼梯

“每次可以爬 1 或 2 阶,问有多少种方法?”

var climbStairs = function(n) {
  if (n <= 2) return n;
  let prevPrev = 1, prev = 2;
  for (let i = 3; i <= n; i++) {
    [prevPrev, prev] = [prev, prev + prevPrev];
  }
  return prev;
};

幽默点:这题其实是斐波那契的“马甲”!爬楼梯 = 兔子生崽,懂了吧?


✅ 198. 打家劫舍

“不能偷相邻房子,求最大收益。”

dp[i] = Math.max(dp[i-2] + nums[i], dp[i-1]);

灵魂拷问:你是选择偷这家(加上前前家),还是放过这家(继承上一家的最大值)?
现实映射:人生也一样,有时候“跳过眼前诱惑”才能拿到更大蛋糕!


💰 第二关:背包问题全家桶

背包问题是 DP 的“黄埔军校”,掌握它,你就毕业了!

✅ 416. 分割等和子集(0-1 背包变形)

“能不能把数组分成两个和相等的子集?” → 能否装满 sum/2 的背包?

for (let j = sum/2; j >= nums[i]; j--) {
  dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
}

关键逆序遍历!防止一个物品被重复使用。


✅ 518. 零钱兑换 II(完全背包)

“硬币无限,凑出 amount 有几种组合?”

for (let coin of coins)
  for (let j = coin; j <= amount; j++)
    dp[j] += dp[j - coin];

注意:这里是正序遍历!因为硬币能重复用。

💡 口诀

  • 0-1 背包:倒着来(防重复)
  • 完全背包:正着走(允许多次)

✅ 474. 一和零(二维背包)

“最多 m 个 0 和 n 个 1,最多选几个字符串?”

for (let i = m; i >= zeros; i--)
  for (let j = n; j >= ones; j--)
    dp[i][j] = Math.max(dp[i][j], dp[i-zeros][j-ones] + 1);

升级版背包:两个限制条件,照样拿下!


📈 第三关:股票买卖六重奏(DP 状态机)

股票题是 DP 的“天花板”,但套路惊人一致!

题号特点状态设计
121只能交易一次dp[i][0/1] 持有/不持有
122无限交易贪心 or DP
123最多两次5 个状态(0→1→2→3→4)
188最多 k 次buy[j], sell[j]
309含冷冻期4 个状态(持/卖/冻/闲)
714含手续费卖出时 -fee

通用思路

  • 持有股票 = max(继续持有, 之前不持有 + 今天买入)
  • 不持有股票 = max(继续空仓, 之前持有 + 今天卖出 - fee)

🤣 段子
“我炒股亏了 50%,只要再涨 100% 就回本!”
—— DP 告诉你:别做梦了,先算清楚状态!


🧩 第四关:字符串 DP 大乱斗

✅ 1143. 最长公共子序列(LCS)

if (c1 === c2) dp[i][j] = dp[i-1][j-1] + 1;
else dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);

经典模板!后续好多题都是它的变种。


✅ 1035. 不相交的线

“画不交叉的连线” → 本质就是 LCS!
因为连线不交叉 ⇨ 相对顺序不变 ⇨ 子序列!


✅ 583. 两个字符串的删除操作

“最少删几次让两字符串相同?”
总长度 - 2 × LCS 长度


✅ 72. 编辑距离

插入、删除、替换 → 三种操作取最小:

dp[i][j] = Math.min(
  dp[i-1][j] + 1,     // 删除
  dp[i][j-1] + 1,     // 插入
  dp[i-1][j-1] + 1    // 替换
);

面试高频!建议背下来。


🏗️ 第五关:树形 & 区间 DP

✅ 337. 打家劫舍 III(树形 DP)

后序遍历返回 [不偷, 偷] 两种状态:

const DoNot = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
const Do = node.val + left[0] + right[0];

精髓:孩子节点的状态决定父节点的选择。


✅ 516. 最长回文子序列

区间 DP 经典!从短区间推长区间:

if (s[i] === s[j]) dp[i][j] = dp[i+1][j-1] + 2;
else dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);

注意遍历顺序i 从后往前,ji+1 开始!


🎯 总结:DP 刷题心法

  1. 识别题型:背包?序列?路径?状态机?
  2. 定义状态:问自己:“dp[i] 到底表示什么?”
  3. 写转移方程:分情况讨论(相等?不等?选 or 不选?)
  4. 初始化 + 遍历顺序:别搞反了!
  5. 空间优化(进阶):滚动数组、只存必要状态。

🚀 最后送你一句 DP 真言:

“过去的状态,决定了现在的选择;现在的选择,影响未来的答案。”
—— 这不仅是 DP,也是人生啊!


附:本文覆盖的力扣题清单(共 36 题)
53, 70, 96, 115, 121, 122, 123, 139, 188, 198, 213, 279, 300, 309, 322, 337, 343, 377, 392, 416, 474, 494, 509, 516, 518, 583, 62, 63, 674, 714, 718, 72, 746, 1035, 1049, 1143