从斐波那契到股票买卖,DP 入门到放弃(不,是精通)的快乐之旅!
大家好,我是你们的 DP 路人甲(不是那个甲骨文的甲),今天带大家从零开始征服力扣动态规划专题。别被“动态规划”这四个字吓到——它听起来像是某种高深算法黑话,其实本质就是:把大问题拆成小问题,用空间换时间,避免重复劳动。
而我们今天要做的,就是把这些“小问题”串起来,变成你刷题时的肌肉记忆!
🔥 为什么 DP 让人又爱又恨?
- 爱:一旦掌握套路,很多看似复杂的题目秒变送分题。
- 恨:状态转移方程写不出来、边界条件搞不清、dp 数组定义反了……
但别慌!DP 的核心就三步:
- 定义状态(dp[i] 或 dp[i][j] 到底代表啥?)
- 找出状态转移方程(怎么从前面的状态推出来当前状态?)
- 确定初始值和遍历顺序(别把背包问题写成完全背包!)
接下来,我们就用真实力扣题目 + 幽默解读,带你打通任督二脉!
🐣 第一关: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从后往前,j从i+1开始!
🎯 总结:DP 刷题心法
- 识别题型:背包?序列?路径?状态机?
- 定义状态:问自己:“dp[i] 到底表示什么?”
- 写转移方程:分情况讨论(相等?不等?选 or 不选?)
- 初始化 + 遍历顺序:别搞反了!
- 空间优化(进阶):滚动数组、只存必要状态。
🚀 最后送你一句 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