本文写于2025.10.28,旨在复习所有做过的 DP 题目,以及它们引出的 DP 思想。
一、DP 概念
1. 概念
DP 是 Dynamic Programming 的简称,专指动态规划算法。
2. 性质
能用 DP 求解的问题,必须满足如下三个性质:
-
最优子结构
指该问题的最优解依赖于其子问题的最优解。
注意:上述信息同时表明,具有最优子结构的问题可能也适用贪心算法。
-
无后效性
已经求解的子问题,不依赖于后续将要求解的子问题。
-
子问题重叠
如果某些子问题是相同的,那么可以通过记忆化存储的方式避免求解相同的子问题。
3. 求解思路
- 定义状态:将问题划为多个阶段,每个阶段对应若干子问题,归纳这些子问题的共有特征,并以某种统一的方法来表示这些子问题(即状态);
- 值得注意的是,一些复杂的 DP 问题可能需要使用多个状态定义模式;
- 推式子:寻找状态之间的依赖关系,即状态转移方程,通常是一个严谨的数学表达式;
- 一个问题的状态转移方程可能不止一个,这导致多个状态转移方程的触发条件有别;
- 找边界:声明状态转移时的起点;
- 边界通常与极小数(如0)有关;
- 找答案:弄清答案对应哪个状态;
- 按顺序求解。
4. DAG 与 DP
DAG,即有向无环图。
如果把每个状态对应图上的一个节点,决策对应连边,这样问题就转变成了在 DAG 上寻找最长/最短路的问题。
二、贪心与 DP
我们在解决一道求最优方案的问题时,常会陷入一种纠结:这道题适用贪心还是 DP?接下来,我将对贪心算法与动态规划算法进行细致区分。
1. 相同点
都利用了历史信息进行求解,即利用之前已经计算过的答案更新下一步的答案。
2. 不同点
(1)贪心
贪心算法的实质是,每一步都进行当前局面下最优的决策,使用局部最优推导全局最优,不存在回溯和反悔。
贪心算法的每一步都包含上一步的最优解,这导致其利用的历史信息有且仅有上一步的最优解。
(2)DP
由于子问题重叠和最优子结构的性质,DP 可以使用多个局部最优解求解全局最优解。
三、线性 DP 问题
注:特别地,在数学中,可以将某些线性 DP 问题称为递推。
1. 一维线性 DP 问题
(1)P1028 [NOIP 2001 普及组] 数的计算(线性 DP)
如果把 n 称为生成因子,我们可以定义 dp[i] 表示以i为生成因子的合法序列总数。
-
对于偶数
i:dp[i] = dp[i-1] + dp[i/2]-
dp[i-1]:不选择i作为起点,继承前一个数的方案数;下一个可加数的最大值为(i-1)/2,严格小于i/2 -
dp[i/2]:选择i作为起点,那么下一个可以加的数最大是i/2,补全第一种决策转移的空缺
-
-
对于奇数 i:
dp[i] = dp[i-1]- 因为
i是奇数,i/2向下取整,所以方案数与i-1相同
- 因为
例题还有:
- P4933 大师
- P1874 快速求和
- P4310 绝世好题
(2)P1164 小A点菜(背包 DP)
0-1 背包问题。我们可以认为 0-1、完全、多重背包问题都属于一维线性 DP 问题。
每个物品只能选一次,所以考虑倒序遍历第二层循环。
定义 f[j] 表示背包容量为 j 时的方案总数。对于第 i 个物品,如果选它,那么背包现在所装物品的容量总和会比原来少 a[i],即 f[j] 的状态完全依赖于 f[j-a[i]]。所以可以推导出状态转移方程。
同样的例题还有:
-
P2834 纸币问题 3
每个纸币看作一个物品,体积为其面值,不考虑其价值,跑完全背包求方案数。类似 P1164。
-
P1049 [NOIP 2001 普及组] 装箱问题
将每个物品的体积和价值都看作其体积,定义
f[j]表示j容量下最多能装的体积,然后跑 0-1 背包板子,输出V-f[v]即可。 -
P2340 [USACO03FALL] Cow Exhibition G
将智商看作体积,情商看作价值,加入负数偏移量(很重要的技巧,可以使负数下标变得合法),跑 0-1 背包板子。
(3)P1020 [NOIP 1999 提高组] 导弹拦截(LIS 问题)
此题是 LIS(最长上升子序列)板子。
-
对于 LIS \(O(n^2)\) 复杂度的解法:
- 使用一维状态定义
f[i]表示对于前i个数的 LIS 长度。 - 枚举到
i这个位置时,前一个决策应当保证j严格小于i且a[j]严格小于或等于a[i]且f[j]为最大。因此,我们有f[i]=max(f[j]+1)。 i与j的双重遍历导致复杂度变为 \(O(n^2)\) 的。
- 使用一维状态定义
-
对于 LIS \(O(n\log n)\) 的做法:
- 定义
f[i]表示长度为i的 LIS 的末尾元素的最小值。容易证明,该数组始终保持严格递增; - 在
f数组中找到第一个大于或等于a[i]的位置,并更新该位置的值为a[i]; - 在每次循环中更新答案。
- 定义
LIS 的优化还有很多种方式(如树状数组优化)。
变式:
P1091 [NOIP 2004 提高组] 合唱队形
(4)P1439 两个排列的最长公共子序列(两个排列的 LCS 问题)
由于给定的是两个排列,所以可以建立 a 序列的一个键值映射表 ind,其中 ind[i] 表示键值 i 在 a 序列中出现的位置。
因为容易证明在位置序列中的递增子序列恰好等价于在两个原序列中的公共子序列,所以该问题转变成在映射序列内(即 ind[b[1]], ind[b[2]], ... , ind[b[n]] )求 LIS 的问题。
(5)P2679 [NOIP 2015 提高组] 子串(双序列线性 DP 问题)
录入这道题旨在说明:凡是遇到两个或多个序列的 DP 问题,一般都要在状态定义中融入所有序列目前枚举到的下标。
格外注意:LCS 问题之所以不需要开二维数组,正是因为其”排列“的特殊性质,导致两个序列的键值能一一对应。
定义 f[i][j][p][0/1] 表示 a 串枚举到第 i 位,b 串枚举到第 j 位,且恰好在 a 串中使用 p 个子串,位置 a[i] 是否纳入子串的方案总数。
枚举 i 和 j ,考虑两种情况:
- 当
a[i]==b[j]时:- 若使用字符
a[i]进行匹配,决策来源于:- 将当前字符纳入前一个子串,且使用前一个字符,即
f[i-1][j-1][p][1]; - 将当前字符纳入新的子串,且不使用前一个字符,即
f[i-1][j-1][p-1][0]; - 将当前字符纳入新的子串,且使用前一个字符,即
f[i-1][j-1][p-1][1]; - 将以上三者求和,转移至
f[i][j][p][1];
- 将当前字符纳入前一个子串,且使用前一个字符,即
- 若不使用字符
a[i]进行匹配,则对于前一个字符,只有选或不选两种选择;且b串的枚举位置不变,子串数目也不变,即f[i][j][p][0]=f[i-1][j][p][1]+f[i-1][j][p][0];
- 若使用字符
- 当
a[i]!=b[j]时,不能使用字符a[i]进行匹配,即f[i][j][p][1]=0;否则,第四维为0,那么有f[i][j][p][0]=f[i-1][j][p][1]+f[i-1][j][p][0]。
注意到第一维 i 依赖且仅依赖于 i-1 ,那么就可以使用滚动数组优化掉。值得注意的是,滚动数组不能优化多维这样的转移。
类似的问题有:
- P2758 编辑距离
- P1140 相似基因