Algorithm · Dynamic Programming

动态规划

重叠子问题的记忆化艺术:五步解题法 · 线性/子序列/背包三大族 · 记忆化与递推的一体两面

三要素

重叠子问题 · 最优子结构 · 无后效性——满足才谈 DP

五步法

状态定义 → 转移方程 → 初始化 → 遍历顺序 → 返回值

三大族

线性(打家劫舍)· 子序列(LIS/LCS)· 背包(01/完全)——面试 80% 的 DP 题

定位:DP 是面试算法的重头戏,但套路化程度最高——五步法 + 三大族模板覆盖绝大多数题。重点不是背题,是"状态定义"这个创造性动作。

Why Dynamic Programming

先看差距:同一个 f(3) 被算了几万次

做法算 fib(40) 要做多少次体感
照着公式裸递归3.3 亿次函数调用笔记本上要跑好几秒
算过的记在表里41 次微秒级,感觉不到
从小到大顺着推40 次循环微秒级,且不用递归栈
先看清浪费在哪:这不是"写得慢一点",而是同一个 f(3) 被反复算了几万次。fib(5) 的递归树里 f(3) 出现 2 次、f(2) 出现 3 次;n 每加 1,这棵树的规模就翻一倍——它是在指数级地重复劳动
DP 换掉的就是这一点:给每个子问题的答案安排一个固定的存放位置,算过一次就再也不算。代价是要开一张表(空间换时间)。但要小心——这张表不是想加就能加的:只有当"局部最优能拼出全局最优"时它才成立,这是下一页三要素要讲的事。

一个能在脑子里跑的最小例子

按定义 f(n) = f(n-1) + f(n-2) 展开 f(5):为了算 f(5) 要算 f(4) 和 f(3);算 f(4) 又要算 f(3) 和 f(2)……你会在树上看到第二个 f(3)——一个你已经算过、却完全不记得的答案。

加张表就完事了吗

对斐波那契,是的。但对"求最大值"类的问题还不够:你还得保证子问题的最优解真能拼出全局最优解。举个反例——图里的"最长简单路径",子路径最优未必拼得出全局最优,此时加表也没用。能不能加表,是前提问题,不是技巧问题。

那 DP 到底难在哪

不在填表,在"表的一行代表什么"。填表是机械劳动;难的是把"当前处境"抽象成一组参数(这就是状态定义)。同一个题状态定义错了,后面的转移方程怎么写都别扭。本 deck 的重点几乎全押在这一步。

本 deck 的路线

先立三个前提(什么时候配用 DP)→ 抽出五步解题法 → 三大族(线性 / 子序列 / 背包)逐个走一遍五步 → 记忆化与递推的一体两面 → 空间优化与高频陷阱。

动机页:用 fib(40) 的 3.3 亿次 vs 41 次这个可复现的具体数字,把"指数级重复劳动"变成体感,再点破"DP = 给每个子问题安排固定存放位置"。关键是先埋一个伏笔——"表不是想加就能加的,前提是局部最优能拼出全局最优",直接引出下一页的三要素,避免读者把 DP 误当成"加个缓存"的通用技巧。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(先记住这个,细节后面展开)
子问题 subproblem把原问题缩小规模后得到的同一个问题(f(5) 里套着的 f(4) 就是一个子问题)
重叠子问题展开后同一个子问题被反复求解。它是"记忆化能省钱"的前提——不重叠就不必建表
最优子结构原问题的最优解能由子问题的最优解拼出来。有它,最值问题才能用 DP
无后效性状态一旦确定,未来怎么走只取决于当前状态值,与"怎么走到这儿"无关
状态 state用一组参数把"当前处境"完整描述出来,如 dp[i]、dp[i][j]。DP 最难的一步就是它
转移方程状态之间的递推关系。写它的通用姿势是枚举"最后一步"的所有可能
DP 表 / memo存放子问题答案的数组或 map。表的下标 = 状态的参数
记忆化搜索(自顶向下)从目标出发递归,算过的顺手记下来,下次直接查。写法最接近暴力递归
递推(自底向上)从最小的状态开始,按依赖顺序把整张表填满。常数更小,但需要自己定顺序
滚动数组当 dp[i] 只依赖前几层时,把二维表压成一维(甚至两个变量)的省空间技巧

如果这些词还有一个是陌生的

先读这几篇再回来。本 deck 默认你已经能读懂递归与数组下标:
回溯 → DP 的起点就是它那样的暴力递归
复杂度分析 → O(n²)/O(n·m) 这些账怎么算
贪心 → 另一个"每步只留一个选择"的流派

本 deck 的最小心智模型

把 DP 记成一个固定动作:① 把"当前处境"用一组参数写成一个状态;② 写出"这个状态由哪些更小的状态推出来"。剩下的三件事——建表、定遍历顺序、填表——全是机械劳动。所有 DP 题的难点都不在填表,而在"用什么参数描述状态"

为什么"无后效性"值得单独记

它是判断"状态定义对不对"的试金石。如果发现未来的决策还要看你是怎么走到这一步的,说明状态漏了信息——正确做法是把"怎么来的"升级成状态的一个新维度(比如股票题加一维"是否持有"、带冷却的再加一维"是否处于冷却期")。

阅读提示:术语不用背,忘了回来查这一页;真正要能默写的只有速查页里那张"五步法 + 三大族对照表",以及遍历顺序那两条铁律。
前置页:十个术语先定义再使用。心智模型把 DP 拆成"定义状态 + 写转移"这一个创造性动作和"建表/定序/填表"三步机械劳动,明确本 deck 的重点在哪;并用"把'怎么来的'升级成状态新维度"给出无后效性被违反时的通用补救动作,为后面股票类多维 DP 铺路。

Overlapping Subproblems

DP 是什么:三个前提,缺一不可

上一页说"加一张表就能从 3.3 亿次降到 40 次"——但表不是想加就能加:只有同时满足下面三个前提的问题,才配得上 DP

① 重叠子问题

递归展开后同一个子问题被反复求解——斐波那契裸递归里 f(3) 被算几万次。这是"记忆化能省钱"的前提;没有重叠(如归并排序)就不需要 DP 表。

② 最优子结构

原问题的最优解由子问题的最优解构成——"和为最大的子数组"的最优包含"以 i−1 结尾"的最优。反例:最长简单路径(图里环的存在让子路径未必最优)。

③ 无后效性

状态一经确定,未来的决策只依赖状态值、不依赖到达路径。违反时的解法:把"怎么来的"塞进状态里(升级状态维度,如带冷却的股票)。

三要素的检验顺序

先问"能不能定义一个无后效的状态",再问"状态之间是否有最优子结构",最后看"子问题是否重叠值得记忆"。顺序反了会陷入套路陷阱。

流派与 DP 的边界
分治子问题不重叠(快排/归并左右独立)——分治+重叠=DP
贪心每步只保留一个选择且可证明局部最优=全局最优;DP 保留所有可能(贪心 deck
回溯所有/具体方案而非最值时用回溯;DP 丢弃路径只留最值(回溯 deck
暴力递归DP = 暴力递归 + 记忆化,一个字都不用改思路
面试金句:"DP 不是玄学,是带缓数的暴力——先写出暴力递归,找到重叠的参数对,加个缓存表,再把递归翻成递推。三步之内必有 DP。"
三要素各配一个反例或正例。右侧四流派对照是高频辨析题:"分治与 DP 的区别"(重叠)、"贪心与 DP 的区别"(保留选择数)、"回溯与 DP 的区别"(要方案还是要最值)。金句给出解题起手式。

Five Steps · Climbing Stairs

五步解题法:以爬楼梯为例全流程

① 状态定义(最重要的一步)

dp[i] = 爬到第 i 阶的方法数。状态定义决定了后面全部——常见两种句式:"以 i 结尾"(子数组类)与"前 i 个"(子序列/背包类)。

② 转移方程

最后一步只有爬 1 或爬 2 阶:dp[i] = dp[i−1] + dp[i−2]。转移 = 枚举最后一步的所有可能,把大问题拆到已定义的子状态。

③ 初始化(边界)

dp[0]=1(站在地面算一种"不动"),dp[1]=1。边界错 = 全错,且往往不报错只出怪数字。

④ 遍历顺序

dp[i] 依赖更小的 i → 从小到大。规则:保证算 dp[i] 时它依赖的状态已算好(背包的倒序就是这条规则的反向应用)。

⑤ 返回值

dp[n]。有的题要 max(dp)(最大子数组和),有的要 dp 最后一位——由状态定义自然决定,不要硬背。

// 爬楼梯(LC 70)完整五步落地
func climbStairs(n int) int {
    // ③ 初始化(含滚动优化:只留两个变量)
    prev, cur := 1, 1 // dp[0], dp[1]
    for i := 2; i <= n; i++ {
        // ②④ 转移 + 顺序
        prev, cur = cur, prev+cur
    }
    return cur // ⑤
}
// O(n) 时间 · O(1) 空间
// 状态依赖只有前两项 → 滚动变量
检查 DP 表的小技巧:n=1..5 手算出 1,1,2,3,5——和暴力递归对表。DP 出错 90% 在初始化或转移的边界(i 从 0 还是 1 起、取不取等号),小规模对表是最快的调试法。
五步法是本 deck 的方法论骨架,后面每一族题都按这五步走。强调两处:状态定义的两种句式(以 i 结尾 / 前 i 个)、小规模对表调试法。

DAG View

把 DP 表画成 DAG:拓扑序就是遍历顺序

爬楼梯问题的状态转移有向无环图 dp0 到 dp5 六个状态节点,每个节点有指向下一个节点和下下个节点的两条边,表示从第 i 阶可以爬一阶或两阶;状态按编号从小到大依次求解即为拓扑序。 dp0dp1dp2 dp3dp4 +1 阶 +1 阶 +1 阶 +1 阶 +2 阶 +2 阶 +2 阶 11235 dp[i] = dp[i−1] + dp[i−2]:节点值 = 两条入边的源头之和 DAG 无环 ⇒ 存在拓扑序 ⇒ 遍历顺序只需保证"先算依赖" —— 有环说明状态设计错了
把 DP 表升级成 DAG 视角:节点=状态、边=转移。"遍历顺序=拓扑序"这句话把第 3 页的规则④升华了;"有环说明状态设计错了"是调试 DP 的利器(如带环的最长路径问题)。

Linear DP · LC 70/198/53

线性 DP 三连:打家劫舍与 Kadane

// 打家劫舍(LC 198):不相邻取最大
// dp[i] = 前 i 间房能偷的最大金额
func rob(nums []int) int {
    prev, cur := 0, 0 // dp[i-2], dp[i-1]
    for _, x := range nums {
        prev, cur = cur, max(cur, prev+x)
        // 不偷 i → cur;偷 i → prev+x
    }
    return cur
}

// 最大子数组和(LC 53):Kadane
// f[i] = 以 i 结尾的最大子数组和
func maxSubArray(a []int) int {
    best, f := a[0], a[0]
    for i := 1; i < len(a); i++ {
        f = max(a[i], f+a[i]) // 接 or 重新开
        best = max(best, f)
    }
    return best
}

打家劫舍的状态句式

"前 i 间"的 dp 只依赖前两项 → 滚动成两个变量 O(1) 空间。环形版(213 首尾相连):拆成"不偷首"与"不偷尾"两个线性问题取大——化环为直的经典技巧。

Kadane 的状态为什么是"以 i 结尾"

"子数组必须连续"——只有"以 i 结尾"的状态才能接上 a[i](子数组类题的标配句式)。全局答案用 best 旁路记录(二叉树 deck 直径题同款手法)。

转移的语义

max(a[i], f+a[i]):要么"重新开一个子数组",要么"接在前一段后面"——枚举最后一步在这里就是"这个元素归不归前一段管"。

线性族清单

70 爬楼梯 · 198/213 打家劫舍 · 53/152 最大子数组与乘积 · 746 最小花费爬楼梯 · 122 买卖股票最佳时机 II(贪心可解)——共性:一维状态、常数依赖。

两段代码都值得默写。"以 i 结尾"句式在这里第一次出现——它和"前 i 个"的区分是状态定义的第一课。213 化环为直、152 乘积版(同时维护 max/min)是两个高频追问。

LIS · LCS

子序列 DP:LIS 与 LCS

// LIS(LC 300):O(n²) 版
// dp[i] = 以 i 结尾的最长递增子序列长度
func lengthOfLIS(a []int) int {
    dp := make([]int, len(a))
    best := 0
    for i := range a {
        dp[i] = 1
        for j := 0; j < i; j++ {
            if a[j] < a[i] && dp[j]+1 > dp[i] {
                dp[i] = dp[j] + 1
            }
        }
        best = max(best, dp[i])
    }
    return best
}
// O(n log n) 版:tails[k] = 长度 k+1 的
// 递增子序列的最小结尾,逐个 lower_bound
// 替换(详见二分 deck QA12)

LIS 的两种状态观

O(n²):"以 i 结尾"——转移必须知道结尾值才能比较;O(n log n):"长度为 k 的最小结尾"——tails 单调,可二分。同一个问题、两种状态设计,复杂度差一个 log——状态定义水平决定复杂度的活教材。

LCS(LC 1143):二维状态

dp[i][j] = a 前 i 个与 b 前 j 个的 LCS 长度:尾字符相等 → dp[i−1][j−1]+1;不等 → max(dp[i−1][j], dp[i][j−1])。O(mn) 时间空间,滚动数组压到 O(min(m,n))。

子序列 vs 子数组

子序列可不连续 → 状态是"前 i 个";子数组必须连续 → 状态是"以 i 结尾"。选错句式,转移写不出来——这是 DP 题的第一检查点。

变体家族

编辑距离(72,双串 DP 之王)、最长回文子序列(516)、俄罗斯套娃(354:W 升 H 降 + LIS)、最大整除子集(368)。

LIS 双状态观是本页招牌:"状态定义决定复杂度"的最佳案例。LCS 是双串 DP 的入门模板,编辑距离是它的进阶。子序列 vs 子数组的句式区分要反复强调。

0/1 Knapsack · Rolling Array

01 背包:为什么一维要倒序

01 背包一维滚动数组的倒序遍历原理 一维 dp 数组按容量从大到小倒序更新:写入 dp[大容量] 时读到的 dp[小容量] 还是上一行(未考虑当前物品)的值,保证每件物品只选一次;若正序则读到的是本行已更新的值,物品会被重复选取。 上一轮(不含物品 i) dp[c−w] dp[c] 倒序:写 dp[c] 时读到的 dp[c−w] 还是上一轮的 本轮(考虑物品 i) 未覆盖 新 dp[c] ← 遍历方向:c 从大到小 01 背包:c 倒序(每件最多选一次)· 完全背包:c 正序(可重复选) 正序时 dp[c−w] 已是"本行含物品 i"的值 → 同一件物品被叠加选取 —— 这正是完全背包想要的效果
01 背包最容易被追问的"为什么倒序",图上一句话讲清:倒序保证读到的 dp[c−w] 是"上一轮"的值(物品没被选过)。反向应用就是完全背包——正序读到的已是本行值,允许重复选。一个数组方向,两个算法。

Knapsack Family · LC 416/322/518/377

背包四问:可行性 / 最值 / 方案数 / 遍历序

01 背包原型(每件一件)

dp[i][c] = max(dp[i−1][c], dp[i−1][c−w]+v);一维倒序。416 分割等和子集:和为 sum/2 的可行性(bool 背包),是"能否装满"的标准问法。

完全背包(每件无限)

一维正序322 零钱兑换(最少硬币:求 min,初始化 +∞)、518 零钱兑换 II(组合数:外层物品内层容量)。

组合 vs 排列:遍历序定语义

外层物品、内层容量 → 组合(1,2 与 2,1 算一种,518);外层容量、内层物品 → 排列(算两种,377 组合总和Ⅳ)。两行代码之差,语义完全不同——面试经典陷阱。

识别背包的三步

① 有没有"选或不选"的资源约束(容量/和/天数);② 每件能用几次;③ 问可行性/最值/方案数哪一种。三问定位到具体变体。

变体遍历代表题
01 可行性倒序416 分割等和子集 M
01 方案数倒序494 目标和 M
完全最值正序322 零钱兑换 M · 279 完全平方数 M
完全组合数物→容518 零钱兑换 II M
完全排列数容→物377 组合总和Ⅳ M · 139 单词拆分 M
分组/二维费用组间正序1155 掷骰子 N 种方法 M
面试金句:"背包题只考三个维度:物品的重复性(01/完全)、问法(可行/最值/方案数)、遍历序(组合/排列)——任何背包题先答这三问,转移方程自己浮出来。"
背包家族用"三问"框架收编:重复性、问法、遍历序。组合 vs 排列的遍历序陷阱(518 vs 377)是每年必考的辨析题。

Top-Down vs Bottom-Up

记忆化搜索与递推:一体两面

// 记忆化搜索(自顶向下)
var memo map[int]int
func dfs(n int) int {
    if n <= 1 { return 1 }
    if v, ok := memo[n]; ok {
        return v
    }
    v := dfs(n-1) + dfs(n-2)
    memo[n] = v
    return v
}
// 与递推(自底向上)完全等价
// —— 只是计算顺序相反

什么时候记忆化更自然

① 状态转移是树形/图形难以线性枚举(树 DP、博弈 DP);② 合法状态集稀疏(递推会算大量无用状态);③ 转移写"从哪来"比"到哪去"顺(区间 DP)。

什么时候递推更优

① 状态是规整网格(线性/背包/双串)——循环无浪费、可滚动优化空间;② 需要空间 O(1) 滚动;③ 避免递归栈(n 大时)。

转换规则

记忆化 ↔ 递推一一对应:dfs(i) ↔ dp[i],递归出口 ↔ 初始化,返回处 ↔ 遍历计算,memo 查表 ↔ 数组直读。先写记忆化再翻递推是稳妥的解题路径。

Go 实现细节

记忆化用 map 或切片(状态可哈希即可);闭包里写 dfs 注意先声明 var;大 n 的树形递归留意 goroutine 栈(Go 栈可增长但深递归仍有成本)。

两种写法的选择标准:状态稀疏/树形 → 记忆化;规整网格 → 递推+滚动。转换规则四条是"先暴力递归、加缓存、翻递推"三步法的落地。

Optimization · Pitfalls

空间优化通则与高频陷阱

滚动数组通则

dp[i] 只依赖 dp[i−1] → 两行交替;只依赖常数个前驱 → 常数个变量(爬楼梯、打家劫舍);二维依赖上一行 → 一行 + 正确的遍历方向(背包)。

状态压缩 DP 一瞥

集合当状态:dp[mask] 位掩码表示"访问过的城市集合",TSP O(2ⁿ·n²)。n≤20 的信号词 + " visited/用过"语义 → 想到位压。

陷阱① 初始化错

求 min 时 dp 要初始化 +∞(且起点为 0);求方案数时 dp[0]=1("什么都不选"是一种方案)。初始化即边界条件,错了全盘错。

陷阱② 遍历顺序错

01 背包正序 → 变完全背包;求组合写成排列序。每次写完背包,默念"方向对不对、谁在外层"。

陷阱③ 状态不完备

股票题只记"第 i 天"不够——还要"是否持股/冷却/次数":状态少一个维度,转移就互相污染。无后效性检查就是查这个。

信号反应
子数组/连续"以 i 结尾" + 全局 best 旁路
子序列/前 i 个"前 i 个" + 内层枚举 j
双串二维 dp[i][j],LCS/编辑距离家族
选与不选+容量背包三问(重复性/问法/遍历序)
n ≤ 20 + 集合状态压缩位掩码
区间合并类区间 DP:枚举分割点(516/312)
面试金句:"DP 优化三板斧:滚动省空间、位压缩状态、贪心换状态——最后一招最厉害:如果贪心能证明正确,根本不用 DP(贪心 deck)。"
陷阱三条(初始化/遍历序/状态不完备)覆盖了 DP 调试 90% 的坑。信号反应表是"看到题→定套路"的映射表,建议整表记忆。

LeetCode Shortlist

必刷题单:按族群分组

族群题目(编号 · 难度)要点
线性70 爬楼梯 E · 198/213 打家劫舍 M · 53/152 最大子数组 M · 746 最小花费 E五步法练手;213 化环为直
子序列300 LIS M · 1143 LCS M · 72 编辑距离 H · 516 回文子序列 M300 双状态观;72 双串之王
背包416 M · 494 M · 322 M · 518 M · 377 M · 139 单词拆分 M三问定位变体;139 是字符串+完全背包
网格62 不同路径 M · 64 最小路径和 M网格 = 二维线性 DP
状态机121/122/309 买卖股票家族 M · 198 变体持股/冷却状态维度
位压/区间516 回文 M · 312 戳气球 H · 847 访问所有节点 H进阶:区间 DP 与 TSP
刷法建议:五步法在 70/198 上练到"边写边说" → 300/1143 掌握两大状态句式 → 背包六题按表刷 → 股票家族一次吃透状态机 → 312/847 只求理解思路。
六族群由易到难。股票家族(121/122/309)是状态机 DP 的标准教程;312 戳气球是区间 DP 门槛题。

Cheat Sheet

一页带走:五步法 · 三大族 · 顺序铁律

① 五步法:拿到题按这个顺序走

步骤要回答的唯一问题高频坑
① 状态定义用哪几个参数才能完整描述"当前处境"?漏维度 → 违反无后效性
② 转移方程枚举"最后一步"的所有可能,取 max/min/求和漏掉一种选择
③ 初始化最小状态(dp[0] / dp[0][0])的值是多少?求 min 要 +∞;求方案数 dp[0]=1
④ 遍历顺序算 dp[i] 时,它依赖的状态都已算好了吗?背包方向、组合/排列序
⑤ 返回值是 dp[n] 还是 max(dp)?由状态定义自然决定,别硬背

② 三大族:状态定义句式与转移

状态定义句式代表题
线性dp[i] = 前 i 个 / 以 i 结尾的最优值70 爬楼梯 · 198 打家劫舍 · 53 最大子数组和
子序列 / 双串dp[i][j] = a 前 i 个与 b 前 j 个的关系300 LIS · 1143 LCS · 72 编辑距离
背包dp[c] = 容量 c 下的最优值/方案数416 等和子集 · 322 零钱兑换 · 518 兑换 II

③ 遍历顺序:四条铁律(背下来)

场景顺序
01 背包(每件一次)容量 c 倒序——保证读到的是"上一轮"的值
完全背包(可重复选)容量 c 正序——故意让同一件被叠加选取
组合数(518,1+2 与 2+1 算一种)外层物品、内层容量
排列数(377,1+2 与 2+1 算两种)外层容量、内层物品

④ 检查清单(写完逐条扫一遍)

1 · 状态完备吗?如果发现"未来的决策还要看我是怎么走到这一步的",说明状态漏了信息——把"怎么来的"升级成状态的一个新维度

2 · 初始化对吗?求最小值初始化 +∞、起点置 0;求方案数 dp[0]=1("什么都不选"也是一种方案)。

3 · 方向和内外层对吗?背包写完必查:容量倒序还是正序?谁在外层?——这两处决定它是 01 还是完全、是组合还是排列。

4 · 小规模对表了吗?n=1..5 手算一遍和暴力递归对照。DP 出错 90% 在初始化或边界,且不报错只出怪数字。

一句话背下来:DP = 带缓存的暴力;先写出暴力递归,找到重叠的参数对,加张表,再决定要不要翻成递推——三步之内必有 DP
速查页:左半是"五步法 + 三大族状态定义句式"(设计工具),右半是"遍历顺序四条铁律 + 检查清单"(防错工具)。顺序铁律单独成表是因为它同时决定"01 还是完全""组合还是排列"两个语义,是背包族出错率最高的地方。

Interview QA · Part 1

高频追问:原理与状态设计

先自答,再对照:每题先说出你的答案(哪怕只说关键词),再展开下面那行——卡住的那 30 秒才是真正长记性的部分。

1 · DP 的适用条件?

三要素

重叠子问题(值得记忆化)、最优子结构(子最优能拼出原最优)、无后效性(状态不含路径历史)。三者缺一:分别退化成暴力、不可拆、状态污染。

2 · 无后效性违反了怎么办?

扩状态

把"缺失的历史"塞进状态维度:股票加"是否持股"、带冷却加"冷冻天数"、访问集合加"位掩码"。状态完备 = 无后效性成立。

3 · "以 i 结尾"和"前 i 个"怎么选?

连续性

子数组/连续段 → "以 i 结尾"(新元素必须挂上);子序列/计数 → "前 i 个"(新元素可选可不选)。选错的症状:转移方程写不出来。

4 · 01 背包一维为什么倒序?

防止重复选

倒序保证 dp[c−w] 读到的是"上一行"(未考虑物品 i)的值;正序读到本行已更新的值 → 同一物品叠加选取(那正是完全背包)。

5 · 记忆化和递推是同一个东西吗?

拓扑序两端

是。同一张 DAG:记忆化从目标往回走(自顶向下),递推从边界往前走(自底向上)。选型看状态稀疏度和是否需要滚动优化。

6 · 贪心与 DP 怎么区分?什么时候敢用贪心?

证明

DP 保留所有选择、贪心每步只留一个。敢用贪心的条件:能证明局部最优蕴含全局最优(交换论证)——证明不出来就老实 DP。

7 · 爬楼梯和斐波那契裸递归什么关系?

记忆化前后

同一个问题的两个阶段:裸递归 O(2ⁿ)(重叠子问题被重复算);加记忆化/递推后 O(n)。"DP = 暴力递归 + 缓存"在这里具象化。

8 · Kadane 为什么状态是"以 i 结尾"?

连续性

子数组连续,只有"以 i 结尾"的状态才能转移到 i+1(决定接不接);全局答案用 best 另存。若定义"前 i 个的最大子数组"则转移根本写不出。

前八题覆盖原理层:三要素、无后效性补救、两种状态句式、背包倒序、记忆化等价性、贪心边界、爬楼梯演进、Kadane 状态设计。

Interview QA · Part 2

高频追问:背包与进阶

先自答,再对照。这一组重点在"方向"——01 与完全、组合与排列,差别都只在一行遍历顺序上。

9 · 完全背包为什么正序就对了?

允许重复

正序时 dp[c−w] 已包含"本轮选过物品 i"的信息 → 再选 i 合法(无限件)。与 01 背包只差一个方向——两套语义共用一个数组。

10 · 518 和 377 只差遍历顺序?

组合 vs 排列

外层物品内层容量:同一物品集合只有一种计入顺序 → 组合数;外层容量内层物品:不同顺序分别计数 → 排列数。这题是"遍历序即语义"的铁证。

11 · 322 零钱兑换为什么初始化 +∞?

求 min 惯例

求最小值的转移是 min——初始值必须是"不变坏的上界"(+∞),dp[0]=0 是唯一合法起点;转移时防 +∞+1 溢出可先判 dp[c−w] 可达。

12 · LIS 怎么从 O(n²) 优化到 O(n log n)?

换状态

换状态定义:tails[k] = 长度 k+1 的 LIS 最小结尾(单调递增)→ 每个元素 lower_bound 替换 O(log n)。不是优化原状态,是重新设计状态

13 · 213 环形打家劫舍怎么处理?

化环为直

首尾不能同偷 → 拆成 [0..n−2] 和 [1..n−1] 两个线性问题取 max。环形约束的通用套路:枚举"断开点"或"首状态"。

14 · 股票家族的状态机怎么搭?

持股维度

每天两种状态:持股/不持股(309 加冷却、714 加手续费)。转移就是状态机边:买/卖/不动。无后效性要求把"能不能卖"编码进状态

15 · 什么时候 DP 不是最优选择?

边界意识

① 贪心可证明 → O(n) 更优(122 股票);② 双指针/滑动窗口能维持性质 → O(n)(11 盛水);③ 二分答案 → log×线性。先找更聪明的结构,DP 是兜底大杀器。

后七题:背包三连(正序/遍历序/初始化)、LIS 换状态、环形拆解、股票状态机、"DP 不是首选"的边界——最后一题展示算法选择的完整决策链。

Related & References

相关知识点与参考

算法系列(本分类)

复杂度分析 →(DP 时空的记账语言)
回溯 →(要具体方案时的兄弟流派)
贪心 →(可证明时的降维打击)
二分查找 →(LIS 的 O(n log n) 助攻)

数据结构系列

二叉树 →(树形 DP 的载体)
图 →(DAG 最长路的 DP 视角)
数组与链表 →(DP 表的物理形态)

参考来源(本 deck 结论可溯源至下列一手材料)

CLRS ch14/15(DP 与高级 DP)· 算法导论第 3 版 ch15最优子结构/重叠子问题形式化、LCS/背包标准分析
LeetCode 70/198/53/300/1143/416/322/518/377 题解各族代表题与遍历序论证
oi-wiki.org/dp · 背包九讲(dd_engi)背包分类(01/完全/分组)与遍历序的中文权威
Bellman (1950s) · Dynamic ProgrammingDP 概念起源("dynamic"是当时的营销词)
收尾:算法系列四向链接 + 数据结构三向链接(树形 DP/DAG/DP 表载体)。出处给到 CLRS、背包九讲与 Bellman 原始概念。总页数 14。