- yeyou26 的博客
浅谈动态规划
- 2025-7-2 17:29:57 @
DP 入门
序列 DP
LIS(最长严格上升子序列)
- 转移方程:
- 优化:
- 方式一:
- 设 表示结尾为 的子序列的最大长度
- 用 BIT 维护
- 转移:查询 前缀最大值,再单点修改
- 时间优化到 ,需要 的空间
- 方式二:
- 设 表示长度为 的子序列的最小结尾
- 转移:二分查询到最大的比 小的 , 即为 ,并用 ckmin
- 时间优化到
- 有的题只能用第一种,也有的只能用第二种,所以都要会
- 方式一:
LCS(最长公共子序列)
- 转移方程:
- 排列的 LCS:设 表示 在 中的位置, 的 LIS 即为排列的 LCS
最大子段和
- 转移方程:
最大不交区间价值和
- 个区间,每个区间为 ,价值为 ,从中选出若干个没有交集的区间,求选出区间的最大价值和
- 区间按 排序,设 为前 个区间的最优解。
- 转移 :二分查找当 时满足 的最大的 值
- 转移方程:
区间 DP
- 区间 DP 有以下特点:
- 可合并:能将两个或多个部分进行整合
- 可分解:能将问题分解为能两两合并的形式
- 常见求解方式:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,更新最优结果
- 破环成链:区间 DP 常用技巧,我们无法接受枚举环断点的复杂度,于是将环复制一份,序列长度变为环的二倍,转为序列 DP
背包 DP
- 约定: 表示第 个物品的价值, 表示第 个物品的代价
简单背包
01 背包
- 转移方程:
- 一般压掉一维倒序枚举:
for (int i = 1; i <= n; i++)
for (int j = m; j >= 0; j--)
if (j >= w[i]) dp[j] = std::max(dp[j], dp[j - w[i]] + v[i]);
完全背包
- 转移方程:
- 一般压掉一维顺序枚举:
for (int i = 1; i <= n; i++)
for (int j = 0; j <= m; j++)
if (j >= w[i]) dp[j] = std::max(dp[j], dp[j - w[i]] + v[i]);
分组背包
- 组物品,每组最多选 个
- 表示第 组物品个数
for (int i = 1; i <= n; i++)
for (int j = m; j >= 0; j--)
for (int k = 1; k <= cnt[i]; k++)
if (j >= w[i][k]) dp[j] = std::max(dp[j], dp[j - w[i][k]] + v[i][k]);
多重背包
- 二进制分组:把每种物品拆成 个,然后 01 背包
- 单调队列优化:复杂度更优,但难推难懂且常数巨大大,大部分情况跑的还不如二进制分组,基本认为没有用(坑)
多维费用背包
for (int i = 1; i <= n; i++)
for (int j = m; j >= w1[i]; j--)
for (int k = m; k >= w2[i], k--)
f[j][k] = std::max(f[j][k], f[j - w1[i]][k - w2[i]] + v[i]);
其他
- 混合背包:第 个物品是啥就套用哪种背包的转移
- 输出方案:可以用 记录第 个物品在容量为 时是否被选,输出伪代码:
int v = V;
for (从最后一件循环至第一件)
if (g[i][v]) {
选了第 i 项物品;
v -= 第 i 项物品的重量;
}
else 未选第 i 项物品;
}
- 求装到某容量的方案数:以 01 背包为例,转移方程变为
- 求最优方案数:设 为前 个物品容量正好为 的方案数,转移不难
for (int i = 1; i <= n; i++)
{
for (int j = m; j >= w[j]; j--)
{
int tmp = std::max(dp[j], dp[j - v[i]] + w[i]);
int c = 0;
if (tmp == dp[j]) c += cnt[j]; // 如果从dp[j]转移
if (tmp == dp[j - v[i]] + w[i]) c += cnt[j - v[i]]; // 如果从dp[j-v[i]]转移
dp[j] = tmp;
cnt[j] = c;
}
}
- 第 优解:感觉没鸟用(坑)
树上背包
- 常见模板:01 背包,但是如果要选第 个物品,则必须选其父链上所有物品
- 相当于对每个子节点做分组背包
- 朴素转移:
- 时间复杂度:
- 空间复杂度:
- 均摊优化:
- 令 为以 为根的子树内所有物品的重量之和,因此转移的两个内层循环(枚举 的容量)的上界均可以对 取
- 可以证明时间复杂度均摊
- DFS 序优化:
- 核心思想:
- DFS 序:对树做 DFS 遍历,这样每个子树对应 DFS 序上的一个连续区间
- 线性DP:把树上的依赖背包问题转化为序列上的 DP,利用 DFS 序的连续性优化状态转移
- 状态定义:设 表示 DFS 序的前 个节点(即 DFS 遍历的前 步)总代价不超过 的最大价值
- 状态转移方程:
- 时间复杂度:
- 核心思想:
for (int i = dfs_clock; i >= 1; i--) {
int u = id[i]; // 当前节点
for (int j = 0; j <= V; j++) {
f[i][j] = f[i+siz[u]][j]; // 不选当前节点(跳过子树)
if (j >= w[u]) // 选当前节点
f[i][j] = max(f[i][j], f[i + 1][j - w[u]] + v[u]);
}
}
联通性树上背包
- 完全背包,但是选的东西必须在给定的树上构成连通块
- 考虑转化为树上背包
- 考虑点分治,这样每一种树上连通块都对应以某个点为根开始的树上背包 dp,即连通块内在点分树上深度最小的点
- 考虑设计一种复杂度与 相关的 dp
- 表示当前用 容量的最大价值
- 算法流程:
- 入 ,把 复制一份存起来,表示不选 情况
- 强制选一个物品,然后把剩下的物品二进制优化,做 01 背包逐个插入更新 ,复杂度为
- 带着新的 向子树 dp
- 回 ,用之前存起来的 与当前的 取
- 时间复杂度
- 感觉没啥用,属于偏怪难
树形 DP
换根 DP
- 需要以每个节点为根进行一系列统计
- 一般有两个过程:
- 第一次扫描,任选一个节点为根节点,执行树形DP进行统计,自底向上转移
- 第二次扫描,从第一次选择的根节点出发进行第二次 dfs,自顶向下更新每一个节点为根节点时的答案
- 是一种常用思想,没有固定套路
点覆盖/独立集/支配集
最小点覆盖
- 设 表示不选节点 时以 为根的子树的最小点覆盖大小, 表示选节点 时以 为根的子树的最小点覆盖大小
- 状态转移方程:
最大独立集
- 设 表示不选节点 时,以 为根的子树的最大独立集大小, 表示选节点 时以 为根的子树的最大独立集大小
- 状态转移方程:
最小支配集
- 表示不选节点 ,且 未被其子节点支配 时,以 为根的子树的最小支配集大小(即 必须被父节点支配)
- 表示不选节点 ,但 已被某个子节点支配 时,以 为根的子树的最小支配集大小
- 表示选节点 时,以 为根的子树的最小支配集大小(此时 支配自身及其邻接点)
- 状态转移方程:
状压 DP
- 把状态压缩成(通常是二进制)整数的一类 DP
子集枚举
- 状压 DP 的问题中,经常需要遍历一个集合的所有子集的所有子集,这一过程被称为子集枚举
for (int m = 0; m <= (1 << n); m++) // 遍历 n 这一状态的子集 m
for (int s = m; ~s; s = (s - 1) & m) // 遍历 m 这一状态的子集 s
- 时间复杂度为
- 证明考虑二项式定理: $$\begin{array}{ll} \sum_{k=0}^n C_{n}^{k} 2^k=(1+2)^n \end{array}$$
插头 DP
(不会,坑)
计数 DP
- 是一种利用类似 DP 的记忆化搜索方法(与在狭义上的 DP,即最优化问题有一定区别),用于解决计数(以及求和)问题
- 本质上是组合数学,但一般不与 DP 做区分
组合数恒等式
恒等式 | 描述 |
---|---|
不合法 | |
选 个等效于不选另外 个 | |
分子分母分别补上对应系数 | |
帕斯卡定理,考虑选不选最后一个 | |
个元素的所有子集;组合数的和;二项式定理 | |
二项式定理 | |
组合数加权和;利用第二条恒等式推出 | |
范德蒙德卷积;每种选法与合并后选法一一对应 | |
范德蒙德卷积在 时的特殊情况 | |
范德蒙德恒等式;枚举最后一个元素是哪个 | |
$\sum_{i = s}^n C_i^m = C_{n + 1}^{m + 1} - C_s^{m + 1}$ | 范德蒙德恒等式引理;整体减去前缀 |
卢卡斯定理
- 对于质数 ,有 $C_n^m (mod\ p)=C_{n\%p}^{m\%p} C_{n/p}^{m/p}(mod\ p)$
long long Lucas(long long n,long long m)
{
if(!m) return 1;
return Lucas(n/P,m/P)%P*C[n%P][m%P]%P;
}
插板法
- 是一种思想,应用较广,用两个例题来理解
- 问题 1:有 个完全相同的元素,要求将其分为 组(组间有序),每组至少有一个元素,一共有多少种分法?
- 转化问题, 个元素分成 组,可以视作将 个元素排成 一列,然后在其中的 个间隔中选择 个间隔插入隔板,答案为
- 也可以看作整数拆分方案数,即把 拆分成不为 0 的 个数的方案数
- 问题 2:有 个完全相同的元素,要求将其分为 组(组间有序),每组可以为空,一共有多少种分法?
- 考虑每组“借”一个元素进来,就变成了 个元素分成 组,每组至少有一个元素的情况,原问题和新问题的解一一对应,答案相同,根据问题 1 得答案为
- 同样可以看作把 拆分为可以为 0 的 个数的方案数
- 问题 1:有 个完全相同的元素,要求将其分为 组(组间有序),每组至少有一个元素,一共有多少种分法?
- 不难推广到每组至少有 个元素的情况
容斥原理
- $|S_1 \cup S_2 ∪ \cdots ∪ S_n|=\sum|S_i|-\sum|S_i\cap S_j|+\cdots+(-1)^{n-1}|S_1\cap S_2\cap \cdots \cap S_n|$
- $|A_1∪A_2∪⋯∪A_n|=∑_{i=1}^n(−1)^{i−1}∑|A_1∩A_2∩⋯∩A_i|$
- 简单来说,就是对 种非空的交集求和,系数为
- 证明:
数位 DP
- 特定的一类冷门计数 DP,时代的眼泪
- 这种问题比较好辨认,一般具有这几个特征:
- 要求统计满足一定条件的数的数量(即,最终目的为计数)
- 这些条件经过转化后可以使用「数位」的思想去理解和判断
- 输入会提供一个数字区间(有时也只提供上界)来作为统计的限制
- 上界很大,暴力枚举验证会超时
- 没什么套路或可讲的,做点题大概就明白了
期望概率 DP
(只会一点,坑)
DP 的优化
数据结构优化
斜率优化
决策单调性优化
wqs 二分
卷积和 FFT
生成函数
Trick
- 状态与目标函数互换