椰程信奥·互动课件 专题16·动态规划1:背包与线性DP
10:00 1 / 15
🧭 椰程信奥 · CSP复赛专题系列

专题16 · 动态规划1

动态规划最难的不是写代码,而是定义状态。
本讲抓住三件事:状态怎么定、转移怎么写、一维优化时循环方向为什么不能反。
四道题:01 背包、完全背包、最长上升子序列、最长公共子序列。

三大特性:重叠子问题 / 最优子结构 / 无后效性 01 背包 · 完全背包 LIS · LCS · 采药 · 导弹拦截
概念

先判断"这道题能不能用 DP"

DP 的三句话:重叠子问题 + 最优子结构 + 无后效性① 重叠子问题同一个小问题会被反复用到② 最优子结构大答案由小答案拼出来③ 无后效性只关心结果不关心怎么来的五步法:定状态写转移定初值定顺序定答案时间几乎都花在第 1 步:状态定义错了,后面写得再漂亮也是 0 分。

① 重叠子问题

同一个子问题被反复计算(如斐波那契 f(5) 里 f(3) 算了很多次)。 → 所以用数组把答案存下来。

② 最优子结构

大问题的最优解由小问题的最优解拼出来。 → 所以可以写"取 max / min"的转移。

③ 无后效性

当前状态一旦确定,以后怎么决策都不会再影响它。 → 所以可以按某个顺序一路填下去。

做一道 DP 题的固定五步

① 定状态(dp[?] 代表什么)→ ② 写转移(怎么从小状态推出来)→ ③ 定初值(dp[0]、dp[i][0] 是多少)→ ④ 定顺序(先算谁后算谁)→ ⑤ 定答案(最后取哪个格子)。
新手 90% 的错误出在第 ① 步和第 ④ 步。

⚠️ DP 不是"递归的改写"。 递归是自顶向下拆问题,DP 是自底向上填表。填表的顺序决定了转移能不能成立。
3D🎲 01 背包 = 把二维 DP 表立起来🖱 拖拽旋转 · 双击复位
为什么用 3D:DP 表的行、列、值是三个维度,压成平面表就丢了值这一维。立起来后柱高即价值,填表过程一目了然。
建模

状态只有一句话:前 i 件、容量 j

状态定义

dp[i][j] = 只考虑前 i 件物品、在容量不超过 j 的限制下,能拿到的最大价值。
答案就是 dp[N][T]。

转移:对第 i 件,只有两种可能

不选 → dp[i][j] = dp[i-1][j]
选(要 j ≥ w[i])→ dp[i-1][j-w[i]] + v[i]
取两者最大。

for (int i = 1; i <= N; i++)
    for (int j = 0; j <= T; j++){
        dp[i][j] = dp[i-1][j];                          // 不选
        if (j >= w[i]) dp[i][j] = max(dp[i][j], dp[i-1][j-w[i]] + v[i]);   // 选
    }
为什么叫"01":每件物品只有选 / 不选两种状态,即 0 和 1。 「每件只有一件」是它和完全背包的唯一区别。
动画①

一格一格填:看清"选"还是"不选"

物品:① 重量2 价值3 ② 重量3 价值4 ③ 重量4 价值5 ④ 重量5 价值6;容量 T = 8。
动画②

为什么 j 必须倒着走

二维降到一维后,dp[j] 要表示"容量 j 的最优值"。 关键是:你读到的 dp[j−w] 到底是哪一轮的?切到「正序(错误写法)」看它怎么把一件物品用了两次。

倒序:j 从 T 走到 w,dp[j−w] 还是上一轮的 → 每件只用一次。
⚠️ 一句话记住: 01 背包倒序(每件只用一次);完全背包正序(每件可用多次)。 这一行写反,样例都过不了。
动画③

同一个 dp 数组,换个方向就是另一道题

完全背包:j 从 w 正序走到 T,dp[j−w] 已经是本轮更新过的 → 同一件可以反复取。

为什么正序能"重复取"

算 dp[j] 时,dp[j-w] 已经在本轮被更新过, 它可能已经包含了这件物品 → 再加一次就是取了两次、三次……

为什么倒序不能

倒序时 dp[j-w] 还是上一轮(还没考虑过这件物品)的值, 所以这件物品最多只会被加进去一次。

线性DP

状态是"以 i 结尾",不是"前 i 个"

状态定义(关键)

d[i] = 以第 i 个数结尾的最长上升子序列长度。
必须"以 i 结尾",否则转移时不知道最后一个数是谁,无法判断能否接上。

转移与答案

d[i] = max(d[j]) + 1,其中 j < i 且 a[j] < a[i];
初值 d[i] = 1(至少自己一个);
答案 = max(d[i]),不是 d[n]!

for (int i = 0; i < n; i++){
    d[i] = 1;
    for (int j = 0; j < i; j++)
        if (a[j] < a[i]) d[i] = max(d[i], d[j] + 1);
}
int ans = 0; for (int i = 0; i < n; i++) ans = max(ans, d[i]);
⚠️ "不升"和"上升"只差一个等号: 导弹拦截第一问用 a[j] >= a[i](允许相等),第二问用 a[j] < a[i](严格)。 把 >= 写成 >,样例就从 6 变成 5。
动画④

看每个位置怎么"接"到前面

点「算一个」:d[i] 会去前面找所有比它小的,取最大 +1。
线性DP

二维:两个串的前缀对前缀

状态定义

dp[i][j] = A 的前 i 个字符与 B 的前 j 个字符的 LCS 长度。
答案是 dp[n][m]。

转移(只有两种情况)

若 A[i] == B[j] → dp[i][j] = dp[i-1][j-1] + 1;
否则 → dp[i][j] = max(dp[i-1][j], dp[i][j-1])。

为什么"不等"时是取两个 max

A[i] 和 B[j] 不相等,说明它们不可能同时出现在 LCS 的末尾。 那么最优解要么不用 A[i](= dp[i-1][j]),要么不用 B[j](= dp[i][j-1]), 两种都试,取大的。

经典应用:求"最少插入几个字符让字符串变成回文" → 把原串反转后与原串求 LCS,答案 = 原串长度 − LCS 长度。
动画⑤

相等走斜、不等取大

A = "ABCBDAB",B = "BDCABA"。点「填一格」。
动画⑥

同一组数据:01 得 3,完全得 140

样例:T=70,草药 ①(71,100) ②(69,1) ③(1,2)。 01 背包每株只有一株 → 最优 3;完全背包可以反复采 → 第 ③ 种采 70 株得 140。

切到完全背包,看同一个 dp 数组怎么被反复刷新。
代码

两张背包的代码,只差循环方向

01 背包(采药)

int dp[1005];
for (int i = 0; i < M; i++){
    int t, v; cin >> t >> v;
    for (int j = T; j >= t; j--)   // ★ 倒序
        dp[j] = max(dp[j], dp[j-t] + v);
}
cout << dp[T];

完全背包(疯狂的采药)

long long dp[100005];
for (int i = 0; i < M; i++){
    int t; long long v; cin >> t >> v;
    for (int j = t; j <= T; j++)   // ★ 正序
        dp[j] = max(dp[j], dp[j-t] + v);
}
cout << dp[T];

① 循环方向写反

01 写成正序 → 变成完全背包,答案偏大; 完全写成倒序 → 变成 01,答案偏小。样例必挂。

② 答案取错位置

LIS 的答案是 max(d[i]) 而不是 d[n]; 背包若容量为"恰好"则要初始化 -INF。

③ 价值溢出

完全背包价值可以很大(70×2=140,N 大时更大), int 不够 → long long。

拿分

DP 是"要么满分、要么零分"——所以先拿保底

复赛一道题 100 分是分好几档给的。会正解当然好, 但不会正解时,前面几档照拿。四道题各拿 30~40 分,加起来就是一等奖和二等奖的分界。

第 1 档 特判 5~10 分 约 2 分钟 第 2 档 暴力搜索 20~30 分 约 8 分钟 第 3 档 特殊性质 40~60 分 约 15 分钟 第 4 档 正解 DP 100 分 约 25 分钟

① 特判档:先别想算法,看数据范围表

题目给的数据范围表就是出题人给你的送分清单。看到「n = 1」「只有 1 件物品」这类一行, 就先写个 if 直接输出答案。2 分钟换 5~10 分,全场最划算。

② 暴力档:DFS 枚举所有选法

每件物品「选 / 不选」,n ≤ 20 时 2²⁰ ≈ 100 万,完全跑得完。 先把暴力写对,它既是保底分,又是后面对拍的标尺。

③ 特殊性质档:题目里那句"若……"

常见送分性质:所有物品时间相同(→ 按价值排序贪心)、代价都是 1(→ 退化成计数)、 数据已经有序(→ 不用 DP,直接扫一遍)。

④ 正解档:状态定了,就赢了八成

DP 的时间几乎都花在定义状态上,不是写代码。写完先跑样例, 再用暴力档造小数据对拍——这是唯一能证明你状态定义对的办法。

考场纪律:每档设时间上限,到点还没调通就 立刻提交当前版本,保住已有分数,再往上冲。很多同学是"正解写了 50 分钟没过,连暴力分都没交"。
拿分

本讲三道题,各自的四档怎么走

采药 HERB(01 背包)

第1档 · 2 分钟
M == 1:时间够就采,不够输出 0。

第2档 · 8 分钟
DFS 每件选/不选,M ≤ 20 稳过。

第3档 · 10 分钟
若所有草药时间相同 → 按价值从大到小取。

第4档 · 15 分钟
一维 dp,容量 倒序。

疯狂的采药 HERB2

第1档 · 2 分钟
M == 1:答案就是 (T/t) × v。

第2档 · 8 分钟
T ≤ 10000 时,O(T×M) 完全背包直接过。

第3档 · 10 分钟
若所有 t == 1 → 全采价值最大的,T × maxV。

第4档 · 15 分钟
容量 正序 + long long。

导弹拦截 MISSILE

第1档 · 2 分钟
n == 1 → 输出 1 / 1。

第2档 · 8 分钟
n ≤ 20:枚举所有子序列 2ⁿ。

第3档 · 12 分钟
只做第一问(最长不升,注意是 >=); 第二问用贪心:每颗导弹放进能拦它的最低那套系统。

第4档 · 15 分钟
两个 LIS,答案取 max(d[i])。

50 分钟时间盒(一题的节奏):
0–5 分钟 读题 + 圈出数据范围表里最小的那几档 → 5–15 分钟 写完第 1、2 档并先交一次(保住 20~30 分)→ 15–40 分钟 冲第 3、4 档 → 40–50 分钟 造小数据对拍、检查 freopen 与文件名。
课堂上别让学生"从头想正解"。先要求每个人 15 分钟内交出第 1、2 档, 再放开冲正解。这个顺序一换,平均分通常能涨一大截——因为它杜绝了"想了 40 分钟,交上去 0 分"。
检测

五道题

① 01 背包一维优化,容量要怎么枚举?

② 完全背包正序的本质是?

③ LIS 的 d[i] 定义是什么?

④ 采药样例(70 3 / 71 100 / 69 1 / 1 2)的答案是?

椰程锦囊:检测④故意把 01 与完全的答案并列(3 与 140), 学生一旦选错,正好引出"循环方向"这一节的核心结论——这一道题能顶三道。

目录