算法可视化 · 交互式讲解

动态规划
一步一步,看懂每一步

不用背公式。把递归树摊开、让表格自己填满、再沿着箭头把答案找回来——每一步你都可以按下「下一步」,看清这一格到底是从哪来的。

dp[i][j] 逐格填充i × j
dp[i][j] = max( dp[i-1][j] , dp[i-1][j-w] + v )
SCROLL
00 / 核心思想

动态规划到底在干什么

一句话:把大问题拆成一堆小问题,答案存进表里;大问题需要时直接查表,而不是重新算一遍。

01

重叠子问题

同一个子问题在递归里被反复求解。这是 DP 存在的前提——既然要重复算,那就只算一次、存下来。

省时间
02

最优子结构

大问题的最优答案,一定是由某个子问题的最优答案「拼」出来的。子问题选错了,这条路就走不通。

能递推
03

状态定义

用一句人话说清 dp[i] 到底代表什么。定义错了,后面全错;定义对了,方程自己会浮出来。

最关键
04

状态转移

从「已经知道的格子」推出「当前格子」。本质上是在枚举:最后一步有哪几种走法,分别对应哪个旧状态。

方程
01 / 为什么要 DP

朴素递归,到底慢在哪里

用最朴素的递归算 f(4) = f(3) + f(2)。整棵树里只有 5 个不同的子问题,却发生了 9 次函数调用。点击「下一步」,看看重复是从哪里冒出来的。

调用次数0
不同子问题0
重复计算0
!
算 f(30) 需要约 134 万 次函数调用,但其中只有 31 个不同的子问题。每多算一层,重复量翻倍——这就是为什么朴素递归会「慢到跑不完」。把算过的结果存进数组,复杂度立刻从 O(2ⁿ) 掉到 O(n)。
02 / 入门第一题

爬楼梯:亲手把表填一遍

一次可以走 1 级或 2 级,问走到第 9 级共有多少种走法。先别想公式——从「最后一步是怎么迈上来的」想,递推关系自己就出来了。

dp[i-1](走 1 级上来) dp[i-2](跨 2 级上来) 正在计算的 dp[i]
→
边界是递推的地基。dp[0] = 1(站在地面算 1 种走法,不是 0)、dp[1] = 1。写转移方程前先把这两个值想清楚,否则后面全是 0。顺带一提:每格只依赖左边两格,所以可以只用两个变量滚动,空间从 O(n) 压到 O(1)。
03 / 二维状态

0-1 背包:第二个维度从哪来

4 件物品,背包容量 5。每件只能拿或不拿。当「容量」和「物品数」同时变化时,一维的 dp[i] 就不够用了——必须开成二维:前 i 件物品、容量恰好为 j 时的最大价值。

↖
格子左上角的小箭头是「决策记录」:↑ 表示没拿这件物品,答案来自正上方;↖ 表示拿了这件物品,答案来自「上一行、容量减去这件重量」的那一格。填完表之后,从右下角顺着箭头往回走,就能倒推出到底选了哪几件。
04 / 双序列

最长公共子序列:填表 + 回溯

"ABCB" 和 "BDCB" 两条轴摆成网格,格子里是「两个前缀的答案」。填完之后别急着走——从右下角顺着箭头回溯,答案是一路捡回来的。

S1
S2
已匹配的公共子序列 —
✓
字符相同 → 走斜线;字符不同 → 挑两边大的。相同的时候没得选,一定来自左上角加 1;不同的时候才需要 max(上方, 左方)。这个「同则斜、异则取大」的结构,编辑距离、最长回文子序列几乎照抄。
05 / 收口

五步法,套任何一道 DP

上面四道题看起来差很远,但动手时的思路是同一套。按顺序问自己下面五个问题,基本不会卡住。

01

定义状态 用一句人话说出来

把 dp[i] / dp[i][j] 念成一句完整的中文:「前 i 件物品、容量为 j 时能装的最大价值」。如果这句话说不通顺,说明状态维度还没想明白。

02

枚举「最后一步」 转移方程的唯一来源

问自己:到达这个状态的最后一步,有哪几种可能?爬楼梯是「最后迈 1 级还是 2 级」,背包是「这件拿还是不拿」,LCS 是「这两个字符相不相等」。每一种可能对应一个旧状态,全部列出来取 max / min / 求和。

03

写边界 不用算就知道的格子

dp[0]、dp[0][j]、dp[i][0] 这些格子递推式覆盖不到,必须手写。它们错了,整张表都会跟着错,而且错得很隐蔽。

04

确定遍历顺序 保证依赖已算好

算 dp[i][j] 时要用到 dp[i-1][j] 和 dp[i-1][j-w]——上一行、左边。所以要外层按物品、内层按容量,从小到大。顺序错了会读到还没算的值(通常是 0),答案就莫名其妙偏小。

05

定位答案 答案不一定在最后一格

爬楼梯的答案是 dp[n],LCS 是 dp[n][m],但背包是 dp[n][W](容量维度也要看满),区间 DP 往往需要遍历所有 dp[i][j] 取最优。填完表先回头确认一遍:题目要的是哪一格。

模型状态定义典型题型
线性 DPdp[i] 前 i 个元素的答案爬楼梯、打家劫舍、最大子段和、最长递增子序列
背包 DPdp[i][j] 前 i 件、容量 j0-1 背包、完全背包、子集和、零钱兑换
双序列 DPdp[i][j] S1 前 i 个 vs S2 前 j 个最长公共子序列、编辑距离、最长公共子串
区间 DPdp[l][r] 区间 [l, r] 的答案最长回文子序列、石子合并、括号匹配
树形 DPdp[u][0/1] 以 u 为根的子树、u 选或不选打家劫舍 III、树的直径、没有上司的舞会
← 回上一门课:基础算法