实现方式
包括记忆化搜索与递推两种.
线性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, 通过lt与len计算出右端点. 再枚举合并点k即可.
环的处理
延长一倍即可.
DP on DAG
通常来说是最长路或最短路问题.
一般可以使用拓扑排序实现状态转移.
树形DP
一般使用dfs实现状态转移. 从子节点向父节点转移需要先递归, 再转移, 从父节点向子节点则需要在递归前转移.
换根dp
某节点的状态并不只由叶节点状态决定, 而是由整棵树的状态决定.
- 先以1为根, 做一次树形dp, 维护出子树的某些信息.
- 再从父节点开始, 将父节点和兄弟节点推向子节点.
例: 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);}