1406 字
7 分钟
竞赛算法(9) 动态规划
2025-12-03
无标签

实现方式#

包括记忆化搜索与递推两种.

线性DP#

没什么好说的. 规模在n < 1e6左右.

背包DP#

规模一般在n < 5000左右.

0-1 背包#

dp[i][j] 表示前i个物品, 重量总和为j时最大价值. dp[i][j] = max(dp[i][j], dp[i - 1][j - w[i]] + v[i]).

也可以进行值域交换.

dp[i][j] 表示前i个物品, 价值总和为j时最小重量. dp[i][j] = min(dp[i][j], dp[i - 1][j - v[i]] + w[i]). 注意初始化.

第一维可以压掉, 变成dp[j]表示前i个物品价值总和为j时最大价值. dp[i][j] = max(dp[i][j], dp[i - 1][j - w[i]] + v[i])注意横向要倒序遍历, 保证旧值更新旧值.

完全背包#

0-1 背包横向改成顺序遍历即可, 用新值更新新值.

多重背包#

每样物品最多拿k[i]次. 朴素一点的做法是当成k[i]个物品, 转化成0-1 背包来做.

可以使用二进制来进行分组, 减少分组数量.

混合背包#

加个if判断即可.

二维费用背包#

dp[i][j][k]. 一般要压掉第一维, 不然容易空间超限.

分组背包#

与0-1 背包如出一辙. 不过外层循环改成枚举每个组, 再加一层枚举组内每个物品的循环即可.

有依赖的背包#

写成主件+若干附件的形式. 转化成0-1 背包.

输出方案#

多开一个数组g[i][j]表示前i个物品质量总和为j时是否选择了此物品. 要从最终状态逆推.

求方案数#

没什么好讲的. max换成求和即可.

区间DP#

规模一般在n < 400.

区间DP的问题常具有如下特征: 将两个或多个部分进行整合, 或将一个部分分裂成两个/多个部分. 我们可以将问题分成两两合并的形式. 求解时枚举合并点, 使合并两个部分的最优值最优即可.

枚举时先枚举长度len, 再枚举左端点lt, 通过ltlen计算出右端点. 再枚举合并点k即可.

环的处理#

延长一倍即可.

DP on DAG#

通常来说是最长路或最短路问题.

一般可以使用拓扑排序实现状态转移.

树形DP#

一般使用dfs实现状态转移. 从子节点向父节点转移需要先递归, 再转移, 从父节点向子节点则需要在递归前转移.

换根dp#

某节点的状态并不只由叶节点状态决定, 而是由整棵树的状态决定.

  1. 先以1为根, 做一次树形dp, 维护出子树的某些信息.
  2. 再从父节点开始, 将父节点和兄弟节点推向子节点.

例: 1324F(维护可差分信息, 直接从父节点中减去子节点的信息即可).

如果需要维护不可差分信息的话也很简单. 在父节点推向子节点之前: 维护出所有子节点的前缀信息pref和后缀信息suff, 剔除子节点y的信息只需要用pref[i - 1]和suff[i + 1]进行运算即可. 注意: 为了统一下标, dfs的时候需要先把所有子节点提取到children序列里, 再进行前缀max与后缀max等处理.

状压DP#

一般n < 22左右. 用二进制存状态.

概率DP#

典型的比如马尔科夫链等.

数位DP#

一个典型框架如下

int a[20];
ll dp[20][StateSize]; // 记忆化数组,注意:不要把 limit 记进去!
// pos: 当前处理到第几位(从高到低,len -> 1)
// state: 当前的状态(比如:数字之和 % D,或者已有的非零个数)
// limit: 当前位是否受到 N 的最高位限制
// lead: 当前是否处于前导零状态(视题目而定是否需要)
ll dfs(int pos, int state, bool limit, bool lead) {
// 1. 递归边界:填完了所有位
if (pos == 0) {
// 返回 1 表示找到一个合法数字
// 或者检查 state 是否满足最终条件(例如 sum % D == 0)
return 1;
}
// 2. 记忆化搜索
// 只有当 !limit && !lead 时,状态才是通用的,可以读取/记录 dp
// 解释:如果 limit=true,说明当前是在贴着 N 的边缘走,这个状态是特例,不能复用。
if (!limit && !lead && dp[pos][state] != -1) return dp[pos][state];
// 3. 确定当前位能填的最大数字
// 如果 limit 为 true,只能填到 a[pos];否则可以填到 9
int up = limit ? a[pos] : 9;
ll ans = 0;
// 4. 枚举当前位填什么数字 i
for (int i = 0; i <= up; ++i) {
// 更新状态
// 比如题目要求数位之和: new_state = (state + i) % D
// 比如题目要求非零个数: new_state = state + (i != 0)
// 处理前导零对 state 的影响:
// 如果原本是前导零且当前填了 0,state 通常不变
// 递归
// new_limit 的逻辑:只有当前本来就受限(limit=true) 且 当前填了最大值(i==up) 时,下一位才继续受限
// new_lead 的逻辑:当前是前导零 且 当前填了 0,下一位还是前导零
ans += dfs(pos - 1, new_state, limit && (i == up), lead && (i == 0));
}
// 5. 记录 memo
if (!limit && !lead) dp[pos][state] = ans;
return ans;
}
ll solve(string s) {
int len = s.length();
// 把字符串转成 int 数组,注意顺序,通常 a[len] 是最高位,a[1] 是最低位
for (int i = 0; i < len; ++i) a[len - i] = s[i] - '0';
// 初始化 dp 数组,通常为 -1
memset(dp, -1, sizeof(dp));
return dfs(len, 0, true, true);
}
竞赛算法(9) 动态规划
https://fuwari.vercel.app/posts/竞赛算法9-动态规划/
作者
ykindred
发布于
2025-12-03
许可协议
CC BY-NC-SA 4.0