常用算法代码模板与题单 | E - 动态规划

【记忆化搜索】

算法模板

int dfs(int x)
{
	// 定义退出条件
	if (x==1) return 1;
	if (a[x]) return a[x];  // 当该值被计算过则直接返回
	...
	return a[x] = ...;
}

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 901 滑雪
洛谷 AT_abc340_c Divide and Divide 普及-
洛谷 P1464 Function 普及-
洛谷 P1156 垃圾陷阱 普及+
洛谷 P3953 逛公园 提高+
学而思编程 vs怪物4 普及进阶 ⭐⭐⭐ 记忆化搜索(分治递归) 定义递归函数 dfs(x) 为消灭体力为 \(x\) 的怪物所需最少攻击次数,边界条件 x=1 时返回 1,利用 map 记忆化存储已计算结果避免重复递归,状态转移为 dfs(x) = dfs(x/2) + dfs(x/2) + 1(当前攻击 1 次加上消灭两只分裂怪物的次数),最终输出 dfs(H),时间复杂度 \(O(\log H)\)(每层 \(x\) 减半,共 \(\log H\) 层)。

【线性DP - 一维】

算法模板

// 状态表示
dp[i] = ... // 一维状态表示
dp[i][j] = ... // 二维状态表示
// 初值(依据题目而定)
memset(dp, 0x3f, sizeof(dp));
dp[0] = 0;
dp[0][0] = 0;
// 状态转移
for (int i=1; i<=n; i++)
	for (int j=0; j<=m; j++)
	{
		...
	}
// 结果
dp[n] 或 dp[n][m]

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 187 导弹防御系统
AcWing 271 杨老师的照相排列
AcWing 272 最长公共上升子序列
AcWing 273 分级
AcWing 275 传纸条
AcWing 317 陨石的秘密 中等 ⭐⭐⭐⭐ 区间DP / 多维DP \(f[d][i][j][k]\) 表示深度不超过 \(d\)、使用 \(i\){}\(j\)[]\(k\)() 的合法 \(SS\) 串数,按最外层括号类型分类,左边用 \(f[d-1]\)、右边用 \(f[d]\) 相乘转移,答案为 \(f[D] - f[D-1]\)(容斥求恰好深度为 \(D\))。
AcWing [[435 传球游戏]]
AcWing 455 小朋友的数字
AcWing 482 合唱队形
AcWing 895 最长上升子序列 简单 ⭐⭐ 线性DP-一维 \(f[i]\) 表示以 \(a[i]\) 结尾的最长上升子序列长度,枚举前面所有 \(j < i\)\(a[j] < a[i]\) 的位置,\(f[i] = \max(f[i],\, f[j] + 1)\),答案为 \(\max_{1 \leq i \leq n} f[i]\)
AcWing 896 最长上升子序列II 中等 ⭐⭐⭐ 贪心 + 二分查找(LIS 优化) 维护数组 \(q[i]\) 表示长度为 \(i\) 的上升子序列的最小结尾元素(\(q\) 单调递增),对每个 \(a[i]\) 二分查找最大的 \(r\) 使 \(q[r] < a[i]\),更新 \(q[r+1] = a[i]\),最终 \(len\) 即为 LIS 长度,时间复杂度 \(O(N \log N)\)
AcWing 897 最长公共子序列
AcWing 898 数字三角形
AcWing 899 编辑距离
AcWing 902 最短编辑距离
AcWing 1010 拦截导弹
AcWing 1012 友好城市
AcWing 1014 登山
AcWing 1015 摘花生
AcWing 1016 最大上升子序列和 简单 ⭐⭐ 线性DP-一维 \(f[i]\) 表示以 \(a[i]\) 结尾的严格递增子序列的最大和,枚举前面所有 \(j < i\)\(a[j] < a[i]\) 的位置,\(f[i] = \max(f[i],\, f[j] + a[i])\),初始化 \(f[i] = a[i]\),答案为 \(\max_{1 \leq i \leq n} f[i]\)
AcWing 1017 怪盗基德的滑翔翼
AcWing 1018 最低通行费
AcWing 1025 开餐馆
AcWing 1027 方格取数
AcWing 1046 橱窗布置
AcWing 1049 大盗阿福
AcWing 1050 鸣人的影分身
AcWing 1051 最大的和
AcWing 1052 设计密码
AcWing 1053 修复DNA
AcWing 1054 股票买卖
AcWing 1057 股票买卖IV
AcWing 1058 股票买卖V
AcWing [[3652 最大连续子序列]]
AcWing 4181 数的划分
AcWing 4547 伊格内修斯和公主IV
AcWing 4548 猴子和香蕉
AcWing 4552 免费馅饼
AcWing 4554 老鼠排队
AcWing 4555 公共子序列
AcWing 4561 挤奶时间
AcWing 6014 数字金字塔
AcWing 6015 求最长不下降序列
AcWing 6016 城市交通路网
AcWing 6017 最长公共子序列
AcWing 6024 计算字符串距离
AcWing 6025 股票买卖
AcWing 6026 最长公共子上升序列
洛谷 B3635 硬币问题 普及-
洛谷 B3636 文字工作 普及-
洛谷 B4069 字符排序 普及-
洛谷 [[B4089 勇敢的津津]] 普及-
洛谷 P1077 摆花 普及-
洛谷 P1091 合唱队形 普及-
洛谷 P1115 最大子段和 普及- ⭐⭐ 线性DP(Kadane算法) 定义 \(f[i]\) 为以 \(a[i]\) 结尾的最大子段和,状态转移方程 \(f[i] = \max(f[i-1] + a[i], a[i])\)(前一个子段和为负则放弃重新开始),最终答案为所有 \(f[i]\) 的最大值,可用滚动变量优化至 \(O(1)\) 空间,时间复杂度 \(O(n)\)
洛谷 P1130 红牌 普及-
洛谷 P1359 租用游艇 普及-
洛谷 [[P1569 Generic Cow Protests]] 普及-
洛谷 P1796 汤姆斯的天堂梦 普及-
洛谷 P2347 砝码称重 普及-
洛谷 [[P3009 Profits]] 普及-
洛谷 [[P6184 Building A Fence]] 普及-
洛谷 [[P10108 闯关游戏]] 普及-
洛谷 [[P10376 游戏]] 普及-
洛谷 [[P11246 小杨和整数拆分]] 普及-
洛谷 [[P11248 矩阵移动]] 普及-
洛谷 AT_abc336_d Pyramid 普及
洛谷 AT_abc403_d Forbidden Difference 普及
洛谷 AT_abc464_d Celester 普及 ⭐⭐⭐ 线性DP(一维状态转移) 定义 \(dp[i][0/1]\) 表示第 \(i\) 天最终为雨天(0)/晴天(1)时的最大幸福值,初始状态根据是否改变第1天天气确定,然后按天递推:\(dp[i][j] = \max_{k \in \{0,1\}}(dp[i-1][k] + cost(j) + gain(k,j))\),其中 \(cost(j)\) 为改变天气的代价(\(0\)\(-X_i\)),\(gain(k,j)\) 为若 \(k=0\)\(j=1\) 则获得 \(Y_{i-1}\) 收益,最终答案为 \(\max(dp[n][0], dp[n][1])\),时间复杂度 \(O(T \times N)\),空间复杂度 \(O(N)\)
洛谷 P1057 传球游戏 普及 ⭐⭐ 线性DP-二维 \(f[i][j]\) 表示传 \(i\) 次后球在第 \(j\) 个同学手中的方案数,每次只能传给左右相邻同学,即 \(f[i][j] = f[i-1][(j-1+n) \bmod n] + f[i-1][(j+1) \bmod n]\),答案为 \(f[m][0]\)
洛谷 P1313 计算系数 普及 ⭐⭐ 线性DP-二维(杨辉三角/组合数) \(f[i][j]\) 表示 \((by+ax)^i\) 展开式中 \(x^j\) 项的系数,\(f[i][j] = f[i-1][j] \cdot b + f[i-1][j-1] \cdot a\)\(x^j\) 可由上一层乘 \(by\) 或上一层 \(x^{j-1}\)\(ax\) 得到),答案为 \(f[k][n]\),对 \(10007\) 取模。
洛谷 P1435 回文子串 普及 ⭐⭐⭐ 线性DP-二维(LCS) 将字符串与其反转串求最长公共子序列 \(dp[i][j]\)\(s_1[i]=s_2[j]\)\(dp[i][j]=dp[i-1][j-1]+1\),否则 \(dp[i][j]=\max(dp[i-1][j],\,dp[i][j-1])\),最少插入字符数 = 字符串长度 \(-\) 最长回文子序列长度 \(dp[n][n]\)
洛谷 P1854 花店橱窗布置 普及 ⭐⭐⭐ 线性DP-二维 \(dp[i][j]\) 表示第 \(i\) 束花放入第 \(j\) 个花瓶时前 \(i\) 束花的最大美学值,\(dp[i][j] = a_{i,j} + \max\limits_{k < j} dp[i-1][k]\),用前缀最大值优化使转移 \(O(1)\),记录路径回溯输出方案,答案为最后一行的最大值。
洛谷 P1970 花匠 普及
洛谷 P2196 挖地雷 普及
洛谷 P2285 打鼹鼠 普及
洛谷 [[P3842 线段]] 普及
洛谷 P8816 上升点列 普及
洛谷 [[P10262 亲朋数]] 普及
洛谷 [[P10721 计算得分]] 普及
洛谷 [[P14075 划分字符串]] 普及
洛谷 P15800 选数 普及 ⭐⭐⭐⭐ 线性DP / 时间轴DP 定义 dp[i] 为到时间 \(i\) 为止的最大收益(不含 \(i\) 开始的任务),每个时刻 \(i\) 决策"执行"(收益 dp[i]+a[i],延迟到 i+b[i] 生效)或"跳过"(收益延续到 i+1),时间复杂度 \(O(n)\),空间复杂度 \(O(n)\)
洛谷 AT_abc362_e Count Arithmetic Subsequences 普及+
洛谷 P1004 方格取数 普及+
洛谷 P1006 传纸条 普及+
洛谷 P1020 导弹拦截 普及+
洛谷 P1052 过河 普及+
洛谷 P1070 道路游戏 普及+
洛谷 P1353 Running 普及+ ⭐⭐⭐⭐ 线性DP-二维(带状态跳跃) \(f[i][j]\) 表示第 \(i\) 分钟疲劳度为 \(j\) 时的最大跑步距离,跑步则 \(f[i+1][j+1] = \max(f[i+1][j+1],\, f[i][j] + d_{i+1})\)\(j < m\)),休息且 \(j=0\)\(f[i+1][0] = \max(f[i+1][0],\, f[i][0])\),休息且 \(j>0\) 则直接跳跃 \(j\) 分钟恢复 \(f[i+j][0] = \max(f[i+j][0],\, f[i][j])\),答案为 \(f[n][0]\)
洛谷 P1439 两个排列的最长公共子序列 普及+
洛谷 P1541 乌龟棋 普及+
洛谷 P1725 琪露诺 普及+
洛谷 P2098 Team Building 普及+ ⭐⭐⭐⭐ 线性DP-三维(容斥计数) \(dp[i][j][k]\) 表示从 FJ 前 \(i\) 头(降序)和 FP 前 \(j\) 头(降序)中选 \(k\) 对且 FJ 每对都获胜的方案数,容斥转移 \(dp[i][j][k] = dp[i-1][j][k] + dp[i][j-1][k] - dp[i-1][j-1][k]\),若 \(a_i > b_j\) 则额外加上 \(dp[i-1][j-1][k-1]\),答案为 \(dp[n][m][K]\)
洛谷 P2679 子串 普及+
洛谷 P4310 绝世好题 普及+
洛谷 P4933 大师 普及+
洛谷 P5020 货币系统 普及+
洛谷 P7074 方格取数 普及+
洛谷 P14078 金币收集 普及+
洛谷 P3861 拆分 提高+
洛谷 P5664 Emiya家今天的饭 提高+
洛谷 P9753 消消乐 提高+
洛谷 P10971 Cookies 提高+
洛谷 [[P11233 染色]] 提高+
洛谷 P5665 划分 省选
洛谷 P7962 方差 省选
洛谷 [[AT_abc466_e Range Flip]]
洛谷 U327333 Max Sum Plus Plus 2
AtCoder AT_awc0018_e Peak Collection ⭐⭐⭐⭐ 线性DP-二维(带约束LIS) \(dp[i][j]\) 表示以第 \(i\) 座山峰结尾、总费用为 \(j\) 时的最大攀登数量,枚举前面海拔更低的山峰 \(k\)\(s_k < s_i\))转移,即 \(dp[i][j] = \max(dp[i][j],\, dp[k][j-c_i] + 1)\),答案为满足 \(dp[i][j] \leq K\) 的最大值。
AtCoder AT_awc0028_e Counting Subsequences ⭐⭐⭐ 线性DP-二维(子序列匹配计数) \(dp[i][j]\) 表示主序列前 \(i\) 个元素匹配子序列前 \(j\) 个元素的方案数,不选 \(A_i\)\(dp[i][j] = dp[i-1][j]\),选 \(A_i\)(仅当 \(A_i = P_j\))时 \(dp[i][j] \mathrel{+}= dp[i-1][j-1]\),答案为 \(dp[n][k]\)
AtCoder AT_awc0044_e Restoration of Compressed Number Sequence ⭐⭐⭐⭐ 线性DP-一维(组合计数) \(dp[j]\) 表示处理到当前位置已使用 \(j\) 种不同颜色的方案数,根据 \(D_i\) 的值分类转移:\(D_i = 0\) 时可选择已有颜色(\(j\) 种)或新颜色(\(m-j\) 种);\(D_i \leq j\) 时必须选已有颜色;\(D_i = j+1\) 时必须选新颜色,答案为 \(\sum dp[j]\)
AtCoder AT_awc0048_b Footsteps in the Hallway
AtCoder AT_awc0080_c Reward for Carrying Luggage
AtCoder AT_awc0081_d Organizing the Bookshelf
AtCoder AT_awc0098_d City Tour Rally
学而思编程 FG操作 普及进阶 ⭐⭐⭐ 线性DP(一维状态扩展) 定义 \(f[i][j]\) 表示处理前 \(i\) 个数后,当前末尾数字为 \(j\) 的方案数,初始化 \(f[1][a_1] = 1\),然后从小到大递推:\(f[i][(j+a_i)\%10] += f[i-1][j]\)(操作F),\(f[i][(j \times a_i)\%10] += f[i-1][j]\)(操作G),每次取模 \(998244353\),最终答案为 \(f[n][k]\),时间复杂度 \(O(n \times 10)\),空间复杂度 \(O(n \times 10)\) 可滚动优化至 \(O(10)\)
学而思编程 倍数序列2 普及进阶 ⭐⭐⭐ 线性DP-二维 \(dp[i][j]\) 表示长度为 \(i\) 且最后一个数为 \(j\) 的合法序列数,枚举 \(j\) 的所有因数 \(k\)\(k \mid j\))转移,即 \(dp[i][j] \mathrel{+}= dp[i-1][k]\),答案为 \(\sum_{j=1}^{n} dp[m][j]\)
学而思编程 变种走格子问题 普及进阶 ⭐⭐⭐ 线性DP + 前缀和优化 定义 dp[i] 为到达第 \(i\) 格的方法数,对于每种跳跃范围 \([L_j, R_j]\),能从 \([i-R_j, i-L_j]\) 范围内的格子跳到 \(i\),利用前缀和数组 s[i] 将区间求和优化为 s[i-L_j] - s[i-R_j-1],状态转移为 dp[i] = Σ(s[i-L_j] - s[i-R_j-1]),然后更新 s[i] = s[i-1] + dp[i],最终输出 dp[n] mod 998244353,时间复杂度 \(O(n \cdot k)\)\(n \le 2 \times 10^5, k \le 10\))。
学而思编程 购买模型 普及进阶 ⭐⭐⭐ 线性DP-一维 \(dp[i]\) 表示以第 \(i\) 个模型结尾的最长合法序列,枚举 \(i\) 的所有因子 \(j\)\(j < i\)\(s_j < s_i\))转移,取 \(\max(dp[j]+1)\)
学而思编程 汽车变速 普及进阶 ⭐⭐ 线性DP-二维 \(f[i][j]\) 表示第 \(i\) 秒速度为 \(j\) 时的最大路程,枚举前一秒速度 \(k \in [\max(0,j-d), j+d]\) 转移,答案为 \(f[t][v_2]\)
学而思编程 删除一个元素 普及进阶 ⭐⭐⭐ 线性DP-一维(前后缀预处理) \(f[i]\) 表示以 \(i\) 结尾的最大子段和,\(g[i]\) 表示以 \(i\) 开头的最大子段和,枚举删除位置 \(i\),取 \(\max(f[i-1]+g[i+1],\, f[i-1],\, g[i+1])\) 作为跨越删除点的最大子段和,答案为所有情况的最大值。
学而思编程 数列分段3 普及进阶 ⭐⭐⭐ 线性DP(区间DP思想) 定义 dp[i] 为前 \(i\) 个元素最多能分成的段数,初始化 dp[0]=0 其余为负无穷;枚举分割点 \(j\)\(0 \le j < i\)),若区间 \([j+1, i]\) 的平均数不超过 \(k\)(即前缀和差 sa[i]-sa[j] <= k*(i-j)),则 dp[i] = max(dp[i], dp[j]+1),最终 dp[n] 即为答案(若仍为负无穷则输出 \(-1\)),时间复杂度 \(O(n^2)\)\(n \le 5000\))。
学而思编程 我们不喜欢WA 普及进阶 ⭐⭐⭐ 线性DP-二维 \(dp[i][a][b]\) 表示长度为 \(i\) 且最后两位为 \((a,b)\) 的合法串数,预处理三维合法性矩阵 \(v[a][b][c]\) 确保原串及任意相邻交换后均不含"WA",转移时 \(dp[i][b][c] \mathrel{+}= dp[i-1][a][b]\)
学而思编程 在两个数列之间 普及进阶 ⭐⭐⭐⭐ 线性DP + 前缀和优化 定义 f[i][j] 为第 \(i\) 个位置选 \(j\) 时的合法方案数,满足 \(a_i \le j \le b_i\)\(j \ge c_{i-1}\);利用前缀和数组 s[i][j] 优化转移:f[i][j] = s[i-1][\min(b_{i-1}, j)] - s[i-1][a_{i-1}-1],即第 \(i-1\) 个位置选不超过 \(j\) 且在 \([a_{i-1}, b_{i-1}]\) 范围内的方案数总和,最终答案为 \(\sum_{j=a_n}^{b_n} f[n][j] \bmod 998244353\),时间复杂度 \(O(n \cdot \max(b_i))\)\(n \le 3000, b_i \le 3000\))。
学而思编程 最大值和最小值 普及进阶 ⭐⭐⭐⭐ 双指针 / 线性扫描(位置追踪) 从左到右扫描数组,维护四个关键位置:\(fxy\)(最后一个 \(x\) 的位置)、\(fyy\)(最后一个 \(y\) 的位置)、\(fxn\)(最后一个大于 \(x\) 的位置)、\(fyn\)(最后一个小于 \(y\) 的位置),对于每个位置 \(i\) 作为右端点,合法左端点 \(L\) 的范围为 \((\max(fxn, fyn), \min(fxy, fyy)]\),以 \(i\) 结尾的合法子段数为 \(\max(0, \min(fxy, fyy) - \max(fxn, fyn))\),累加所有位置即为答案,时间复杂度 \(O(n)\),空间复杂度 \(O(n)\)
其他 [[输出最长上升子序列]]
其他 [[最长公共子序列问题]]

【线性DP - 二维】

算法模板

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 274 移动服务
洛谷 AT_abc464_e Fill-Rect Query 普及 ⭐⭐⭐ 逆向DP / 后缀最大值传播 由于每次操作都是覆盖以 \((1,1)\) 为左上角的矩形,格子 \((i,j)\) 的最终颜色由所有满足 \(R_k \ge i\)\(C_k \ge j\) 的操作中编号最大的那个决定;将每次操作仅记录在其右下角 \((R_k, C_k)\),然后从右下角向左上角做二维DP:\(a[i][j] = \max(a[i][j], a[i+1][j], a[i][j+1])\)\(a[i][j]\) 即为覆盖 \((i,j)\) 的最后操作编号,输出对应字符即可,时间复杂度 \(O(H \times W + Q)\),空间复杂度 \(O(H \times W)\)
其他 [[不同路径]]
其他 [[不同路径II]]

【01背包】

算法模板

朴素版及一维优化

// 最值型01背包
// 朴素解法
for (int i=1; i<=n; i++)
{
	for (int j=0; j<=m; j++)
	{
		dp[i][j] = dp[i-1][j];
		if (j>=v[i])
			dp[i][j] = max(dp[i][j], dp[i-1][j-v[i]]+w[i]);
	}
}
// 一维数组优化
for (int i=1; i<=n; i++)
	for (int j=m; j>=v[i]; j--)
        dp[j] = max(dp[j], f[j-v[i]]+w[i]);

布尔型01背包

// 布尔型01背包
dp[j] |= dp[j-w[i]]

计数型01背包

// 计数型01背包
dp[j] += dp[j-w[i]]

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 2 01背包问题 简单 ⭐⭐ 背包DP-01背包 \(f[i][j]\) 表示前 \(i\) 件物品、容量不超过 \(j\) 时的最大价值,不选则 \(f[i][j]=f[i-1][j]\),选则 \(f[i][j]=\max(f[i][j],\,f[i-1][j-v_i]+w_i)\)(需 \(j \geq v_i\)),可滚动优化为一维逆序遍历 \(f[j]=\max(f[j],\,f[j-v_i]+w_i)\),答案为 \(f[m]\)
AcWing 11 背包问题求方案数
AcWing 12 背包问题求具体方案
AcWing 278 数字组合
AcWing 280 陪审团
AcWing 423 采药
AcWing 426 开心的金明
AcWing 734 能量石
AcWing 1022 宠物小精灵之收服
AcWing 1024 装箱问题
AcWing 1047 糖果
AcWing [[1365 子集的和]] 中等 ⭐⭐⭐ 01背包 / 计数型DP 先判断总和是否为偶数,若为奇数则无解;定义 \(dp[i][j]\) 表示用前 \(i\) 个数凑出和为 \(j\) 的方案数,状态转移为 \(dp[i][j] = dp[i-1][j] + dp[i-1][j-a[i]]\)(不选或选第 \(i\) 个数),最终答案为 \(dp[n][x/2]/2\)(除以2是因为每个划分被两个互补子集各计算一次)。
AcWing 3417 砝码称重 中等 ⭐⭐⭐ 背包DP-01背包(布尔型+三状态) \(f[i][j]\) 表示前 \(i\) 个砝码能否称出重量 \(j\),每个砝码三种选择:不放则 \(f[i][j]=f[i-1][j]\),放右边则 $f[i][j]\mathrel{\
AcWing 6019 01背包问题
洛谷 P1048 采药 普及-
洛谷 P1049 装箱问题 普及-
洛谷 [[P1060 开心的金明]] 普及-
洛谷 P1802 5倍经验日 普及-
洛谷 [[P2639 Bessie's Weight Problem]] 普及-
洛谷 [[P2871 Charm Bracelet]] 普及-
洛谷 [[P2925 Hay For Sale]] 普及-
洛谷 [[P11377 武器购买]] 普及-
洛谷 AT_abc375_e 3 Team Division 普及
洛谷 [[B3873 小杨买饮料]] 普及
洛谷 P2340 Cow Exhibition 普及 ⭐⭐⭐⭐ 背包DP-01背包(偏移量处理负数) \(dp[j]\) 表示智商之和为 \(j - \text{OFFSET}\) 时情商之和的最大值,枚举每头奶牛做01背包转移 \(dp[j] = \max(dp[j],\, dp[j-s_i] + f_i)\),最后遍历 \(j \geq \text{OFFSET}\)\(dp[j] \geq 0\) 的状态,取 \(\max((j-\text{OFFSET}) + dp[j])\)
洛谷 P13018 调味平衡 普及 ⭐⭐⭐ 背包DP-01背包(差值转换+偏移量) 将每种食材转化为差值 \(d_i = a_i - b_i\) 和和值 \(s_i = a_i + b_i\)\(dp[j]\) 表示差值为 \(j - \text{OFFSET}\) 时的最大和值,转移 \(dp[j] = \max(dp[j],\, dp[j-d_i] + s_i)\),答案为 \(dp[\text{OFFSET}]\)
洛谷 P14360 多边形 普及
洛谷 P14920 道具商店 普及
洛谷 [[AT_dp_e Knapsack 2]] 普及
洛谷 [[P4377 Talent Show]] 提高+
AtCoder AT_awc0001_d Merchant on the Highway
AtCoder AT_awc0011_e Choosing Treasure Chests ⭐⭐⭐⭐ 背包DP-01背包(前缀后缀DP) 前缀 \(f[i][j]\) 表示前 \(i\) 个物品容量 \(j\) 的最大价值,后缀 \(g[i][j]\) 表示第 \(i\)\(n\) 个物品容量 \(j\) 的最大价值,枚举每个物品 \(i\) 和前置容量 \(j\),若存在 \(f[i-1][j] + b_i + g[i+1][m-j-a_i] = f[n][m]\) 则输出 Yes,否则 No
AtCoder AT_awc0018_d Smart Shopper
AtCoder AT_awc0019_d Conservation Plan for the Botanical Garden ⭐⭐⭐ 背包DP-01背包(带免费物品条件) \(dp[i][j]\) 表示前 \(i\) 个植物花费成本 \(j\) 时的最大观赏价值,\(B_i \geq T\) 的植物免费获得价值 \(dp[i][j] = dp[i-1][j] + A_i\)\(B_i < T\) 的植物需灌溉 \(dp[i][j] = \max(dp[i-1][j],\, dp[i-1][j-C_i] + A_i)\)(若 \(j \geq C_i\)),答案为 \(\max_{0 \leq j \leq M} dp[n][j]\)
学而思编程 火灾 普及进阶 ⭐⭐⭐ 背包DP-01背包(带截止时间) 按截止时间 \(d_i\) 升序排序后做01背包,\(f[j]\) 表示时间 \(j\) 内能获得的最大价值,对每个物品逆序枚举 \(j\)\(d_i-1\)\(t_i\) 转移 \(f[j]=\max(f[j],\,f[j-t_i]+p_i)\),答案为 \(\max_{1 \leq i \leq a[n].d-1} f[i]\)
学而思编程 热气球升空 普及进阶 ⭐⭐⭐ 背包DP-01背包(最值型) 将吊篮体积成本分摊到每个物品,物品 \(i\) 的总成本为 \(w_i + v_i \cdot c\)\(dp[j]\) 表示凑出体积恰好为 \(j\) 的最小总成本,逆序转移 \(dp[j] = \min(dp[j],\, dp[j-v_i] + w_i + v_i \cdot c)\),从大到小找到第一个满足 \(dp[j] \leq m\)\(j\) 即为答案。
学而思编程 三角形牧场 普及奠基 ⭐⭐⭐⭐ 三维01背包(子集划分) \(n\) 根木板分配到三条边,定义 f[i][j][k] 为前 \(i\) 根木板能否组成第一边长度 \(j\)、第二边长度 \(k\) 的方案(布尔值),状态转移为 `f[i][j][k] = f[i-1][j][k] \
学而思编程 真因数之和 普及进阶 ⭐⭐⭐ 背包DP-01背包(计数型+筛法预处理) 用筛法预处理每个数的真因数列表,对每个查询 \(x\) 做计数型01背包:\(dp[j]\) 表示和为 \(j\) 的方案数,逆序枚举 \(dp[j] \mathrel{+}= dp[j-\text{num}]\),答案为 \(dp[x]\)
学而思编程 智能饭盒 普及进阶 ⭐⭐⭐ 二维01背包(最少物品数) 定义 f[j][k] 为达到至少 \(j\) 克脂肪和至少 \(k\) 克蛋白质所需的最少食物数,初始化 f[0][0]=0 其余为无穷大;对每个食物倒序枚举脂肪和蛋白质维度,状态转移为 f[j][k] = min(f[j][k], f[max(j-a[i],0)][max(k-b[i],0)] + 1),最终 f[x][y] 即为答案(若仍为无穷大则输出 \(-1\)),时间复杂度 \(O(n \cdot x \cdot y)\)\(n \le 300, x,y \le 300\))。
其他 [[01背包问题]]

【完全背包】

算法模板

朴素版及一维优化

// 最值型完全背包
// 朴素解法
for (int i=1; i<=n; i++)
    for (int j=0; j<=m; j++)
    {
        dp[i][j] = dp[i-1][j];
        if (j>=v[i])
            dp[i][j] = max(dp[i][j], dp[i][j-v[i]]+w[i]);
    }
// 一维数组优化
for (int i=1; i<=n; i++)
    for (int j=v[i]; j<=m; j++)
        dp[j] = max(dp[j], dp[j-v[i]]+w[i]);

布尔型完全背包

// 布尔型完全背包
dp[j] |= dp[j-w[i]]

计数型完全背包

// 计数型完全背包
dp[j] += dp[j-w[i]]
dp[j] = min(dp[j], dp[j-w[i]]+1)

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 3 完全背包问题 简单 ⭐⭐ 完全背包 定义 \(f[i][j]\) 为前 \(i\) 种物品、容量为 \(j\) 时的最大价值,由于每种物品无限件,状态转移为 \(f[i][j] = \max\big(f[i-1][j],\, f[i][j-v_i] + w_i\big)\)(第二维用 \(f[i]\) 而非 \(f[i-1]\) 保证可重复选取),一维优化时容量从小到大遍历,最终答案为 \(f[m]\)
AcWing 279 自然数拆分
AcWing 532 货币系统
AcWing 900 整数划分
AcWing 1021 货币系统
AcWing 1023 买书
AcWing 6020 完全背包问题
洛谷 P1616 疯狂的采药 普及-
洛谷 P1679 神奇的四次方数 普及- ⭐⭐ 背包DP-完全背包(求最小个数) \(dp[j]\) 表示凑成数字 \(j\) 所需的最少四次方数个数,预处理四次方数 \(x=i^4\)\(i=1\)\(18\)),正向枚举 \(dp[j]=\min(dp[j],\,dp[j-x]+1)\),答案为 \(dp[m]\)
洛谷 [[P2722 总分Score Inflation]] 普及-
洛谷 P17012 条形蛋糕 普及- ⭐⭐⭐ 完全背包 dp[j] 为长度 \(j\) 的最大价格,正序枚举容量 j\(i\)\(n\),转移 dp[j] = max(dp[j], dp[j-i] + p[i]),允许同种长度重复选取,时间复杂度 \(O(n^2)\),空间复杂度 \(O(n)\)(可优化为一维)
洛谷 P1474 Cow Cash 普及 ⭐⭐ 背包DP-完全背包(计数型) \(dp[j]\) 表示组成金额 \(j\) 的方案数,外层遍历每种货币 \(i\),内层正向枚举金额 \(j\)\(a_i\)\(N\) 转移 \(dp[j] \mathrel{+}= dp[j-a_i]\),初始化 \(dp[0]=1\),答案为 \(dp[N]\)
洛谷 [[P2214 Mooo Moo]] 普及
洛谷 P1021 邮票面值设计 普及+
洛谷 P1941 飞扬的小鸟 普及+
洛谷 P2737 麦香牛块 普及+ ⭐⭐⭐ 完全背包 / 数论(Frobenius Coin Problem) 利用完全背包预处理 \(dp[j]\) 表示金额 \(j\) 能否被凑出,结合Frobenius 数理论可知:若 \(\gcd(a_1,\ldots,a_n)=1\) 则最大不可表示数不超过 \(a_{\max}^2 - a_{\max} - a_{\min}\),据此设定上界后从大到小扫描找第一个 \(dp[i]=0\)\(i\) 即为答案,若 \(\gcd\neq1\) 或上界内全可凑出则输出 \(0\)
洛谷 P5662 纪念品 普及+
学而思编程 假币购物 普及进阶 ⭐⭐⭐⭐ 背包DP-完全背包+01背包(两阶段组合) 第一阶段完全背包预处理 \(dp[j]\) 为制造面值恰好 \(j\) 的最小花费,第二阶段01背包 \(dp2[i][j]\) 表示前 \(i\) 件商品花费 \(j\) 的最大总价格,转移时成本为 \(dp[a_i]\),答案为 \(dp2[k][W]\)
学而思编程 买一送一 普及进阶 ⭐⭐⭐⭐ 背包DP-完全背包(预处理优惠) 按价格升序排序后预处理 \(maxd[i]\) 为价格小于 \(a_i.p\) 的物品中最大美味度,完全背包转移 \(f[j] = \max(f[j],\, f[j-a_i.p] + a_i.d + maxd[i])\),答案为 \(\max_{0 \leq j \leq K} f[j]\)
学而思编程 vs怪物5 普及进阶 ⭐⭐⭐ 背包DP-完全背包(最值型) \(f[j]\) 表示造成恰好 \(j\) 点伤害的最小魔法值消耗,完全背包转移 \(f[j] = \min(f[j],\, f[j-a_i] + b_i)\),初始化 \(f[0]=0\) 其余为 \(\infty\),答案为 \(\min_{j \geq H} f[j]\)
其他 [[奶牛打工]]
其他 [[完全背包问题]]
其他 [[硬币问题]]

【多重背包】

算法模板

朴素版及二进制优化

// 朴素解法
for (int i=1; i<=n; i++)
    for (int j=0; j<=m; j++)
        for (int k=0; k<=s[i] && k*v[i]<=j; k++)
            f[i][j] = max(f[i][j], f[i-1][j-v[i]*k] + w[i]*k);

二进制优化

// 二进制优化版本
int cnt = 0;  // 拆分计数
for (int i=1; i<=n; i++)
{
	// v, w, s: 体积,价值,数量
	cin >> v >> w >> s;
	// 二进制拆分
	for (int j=1; j<=s; j*=2)
	{
		cnt++;
		vv[cnt] = j*v;
		ww[cnt] = j*w;
		s -= j;
	}
	if (s)
	{
		cnt++;
		vv[cnt] = s*v;
		ww[cnt] = s*w;
	}
}
// 拆分完后做01背包
for (int i=1; i<=cnt; i++)
	for (int j=m; j>=vv[i]; j--)
		f[j] = max(f[j], f[j-vv[i]] + ww[i]);
cout << f[m];

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 4 多重背包问题I 简单 ⭐⭐ 多重背包 定义 \(f[i][j]\) 为前 \(i\) 种物品、容量为 \(j\) 时的最大价值,由于第 \(i\) 种物品最多有 \(s_i\) 件,状态转移为 \(f[i][j] = \max\limits_{0\le k\le s_i,\,k\cdot v_i\le j}\big(f[i-1][j-k\cdot v_i] + k\cdot w_i\big)\),通过枚举选取数量 \(k\) 实现,最终答案为 \(f[n][m]\)
AcWing 5 多重背包问题II 中等 ⭐⭐⭐ 多重背包 / 二进制优化 将每种物品的 \(s_i\) 件按二进制拆分(即拆成 \(1,2,4,\ldots\) 及剩余部分),使每种物品转化为 \(O(\log s_i)\) 个"新物品",每个新物品只有选或不选两种状态,从而将多重背包转化为 01 背包,容量从大到小遍历,最终答案为 \(f[V]\)
AcWing 6 多重背包问题III
AcWing 1019 庆功会
洛谷 [[CF106C Buns]] 普及
AtCoder AT_awc0024_e Making Rope from Materials

【混合背包】

算法模板

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 7 混合背包问题
AcWing 6021 混合背包

【二维背包】

算法模板

// 状态表示:dp[i][j][k]表示对于前i个,xx不超过j且xx不超过k的前提下的最大价值
// 初值:dp[i][j][k] = 0;
// 状态转移
dp[j][k] = max(dp[j][k], dp[j-a[i]][k-b[i]]+c[i]);
// 结果
dp[m][x]

// 如果 j 和 k 还需要满足特定条件才能转移,可以写成如下,注意这里不能用if else
if (j>=a[i]) ...
if (k>=a[i]) ...

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 8 二维费用的背包问题
AcWing 1020 潜水员
洛谷 P1507 NASA的食物计划 普及-

【分组背包】

算法模板

for (int i=1; i<=n; i++)
    for (int j=m; j>=0; j--)
        for (int k=0; k<s[i]; k++)
            if (v[i][k]<=j)
                f[j] = max(f[j], f[j-v[i][k]]+w[i][k]);

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 9 分组背包问题 中等 ⭐⭐⭐ 分组背包(01背包扩展) 每组物品最多选一个,对每组倒序枚举背包容量,再枚举组内每个物品进行状态转移 f[j] = max(f[j], f[j-v[i][k]] + w[i][k]),最终答案为 f[m],时间复杂度 \(O(N \cdot V \cdot \max(S_i))\)
AcWing 1013 机器分配
AcWing 6022 分组背包

【图上DP】

算法模板

for (int j=1; j<=k; j++)  // 枚举步数
	for (int u=1; u<=n; u++)  // 枚举当前结点u
		for (int v : g[u])  // 遍历u的所有邻接结点v
			...  // 转移方程

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结

【树形DP】

算法模板

基本操作

void dfs(int u, int fa)
{
	// 初始化
	for (int i=h[u]; i!=-1; i=ne[i])
	{
		int v = e[i];
		if (v==fa) continue;
		dfs(v, u);
		// 深搜到底后的处理
	}
	// 所有叶子结点遍历后的处理
}

求以u为根的连通块的节点数量最值

// 深度优先遍历
int dfs(int u)  // 以u为根节点的遍历
{
    st[u] = true; // st[u] 表示点u已经被遍历过
    for (int i = h[u]; i != -1; i = ne[i])
	{
        int j = e[i];
        if (!st[j])
		{
			int s = dfs(j);  // 计算每层节点数
			res = max(res, s);  // 计算以u为根的各个连通块所有节点数量的最大值
			sum += s;  // 先递到最深处,计算数量,再回溯
		}
    }
	res = max(res, n-sum);  // 计算以u为根的其他连通块节点数量的最大值
	ans = min(ans, res);  // 计算最大值的最小值
	return sum;
}

树的重心

void dfs(int u, int fa)
{
    cnt[u]=1;
    for (int i=h[u]; i!=-1; i=ne[i])
    {
        int j = e[i];
        if (j==fa) continue;
        dfs(j, u);
        cnt[u]+=cnt[j];
        maxx[u] = max(maxx[u], cnt[j]);
    }
    maxx[u]=max(maxx[u], n-cnt[u]);
}
    for (int i=1; i<=n; i++)
    {
        if (maxx[i]<minn)
        {
            root = i;
            minn = maxx[i];
        }
    }
// root就是重心

树的直径

// 求最大、次大值(有返回值)
int dfs(int u, int father)
{
    int dist = 0;  //以点u为子树根向下能到达最远的距离
    int d1=0, d2=0;  //点u为根向下能到达最远d1、次最远的距离d2
    for (int i=h[u]; i!=-1; i=ne[i])
	{  //遍历以点u根的树所有节点
        int j=e[i];
        if (j==father) continue;  //保证向下搜索
        int d = dfs(j, u) + w[i];
        dist = max(dist, d);
        if (d>=d1)
		{
            d2=d1; d1=d;
        } else if (d>d2) d2 = d;
    }
    ans = max(ans, d1+d2);  //d1+d2: 以点u为子树根的树直径
    return dist;
}
dfs(1, -1);  //先选节点1为树的总根,节点1没有父节点,为-1
// 求最大、次大值(无返回值)
int d[N];
void dfs(int u, int fa)
{
    for (int i=h[u]; i!=-1; i=ne[i])
    {
        int j = e[i];
        if (j==fa) continue;
        dfs(j, u);
		int t = d1[j] + w[i];
		if (t>d1[u])
		{
			d2[u] = d1[u];
			d1[u] = t;
		}
		else if (t>d2[u])
		{
			d2[u] = t;
		}
    }
	ans = max(ans, d1[u] + d2[u]);
}
dfs(1, 0);
int l, r, p, maxd, d[N], pre[N];
// 双dfs法
// DFS1:求树的直径端点,同时记录父节点信息
void dfs(int u, int fa)
{
    pre[u] = fa;                // 记录u的父节点
    // 遍历u的所有邻接点
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int v = e[i];
        if (v == fa) continue;  // 跳过父节点
        d[v] = d[u] + w[i];       // 更新v到起点的距离
        // 更新最远点
        if (d[v] > maxd)
        {
            maxd = d[v];
            p = v;
        }
        dfs(v, u);              // 递归DFS
    }
}
// 得到p(起点后再跑一次dfs)
// 第一次运行dfs(1, 0), 第二次运行dfs(p, 0)
p = maxd = 0;
dfs(1, 0);
l = p;                      // l是直径的一个端点
d[l] = maxd = p = 0;            // 重置距离数组,准备第二次DFS
// 第二次DFS:从l出发,找到最远点r,同时记录父节点
dfs(l, 0);
r = p;
// maxd = d[r];
// 更好懂的板子
void dfs(int u, int fa)
{
	for (int i=h[u]; i!=-1; i=ne[i])
	{
		int v = e[i];
		if (v==fa) continue;
		dfs(v, u);
		maxd = max(maxd, d[u]+d[v]+w[i]);
		d[u] = max(d[u], d[v] + w[i]);
	}
}
int main()
{
	cin >> n;
	for (int i=1; i<n; i++)
	{
		cin >> x >> y;
		add(x, y, 1), add(y, x, 1);
	}
	dfs(1, 0);
	cout << mxd << endl;
}

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 285 没有上司的舞会
AcWing 323 战略游戏
AcWing 846 树的重心 简单 ⭐⭐ 树形DP 以任意点为根DFS,\(sum\) 记录以 \(u\) 为根的子树大小,\(uson\) 记录 \(u\) 的最大子树大小,删除 \(u\) 后最大连通块为 \(\max(n-sum,\,uson)\),取所有节点的最小值即为重心对应的最大连通块大小。
AcWing 1072 树的最长路径
AcWing 1073 树的中心
AcWing 1074 二叉苹果树
AcWing 1075 数字转换 中等 ⭐⭐⭐⭐ 树形DP / 筛法 先用筛法预处理每个数的真因数之和,若 \(sum[i] < i\) 则建立有向边 \(sum[i] \to i\) 构成森林,问题转化为求森林中所有树的直径最大值,对每个根节点 DFS 维护向下的最长链 \(d1\) 和次长链 \(d2\),用 \(d1+d2\) 更新全局答案,最终返回所有树直径的最大值。
AcWing 1077 皇宫看守
洛谷 B4016 树的直径 普及 ⭐⭐ 两次DFS / BFS(树形DP思想) 基于树的直径性质:从树上任意一点出发 DFS/BFS 找到的最远节点,一定是直径的一个端点;因此第一次从任意节点(如 1)出发找到最远节点 \(t\),第二次从 \(t\) 出发找到的最远距离即为树的直径,时间复杂度 \(O(n)\)
洛谷 P1351 联合权值 普及
洛谷 P1364 医院设置 普及
洛谷 P1670 Tree Cutting 普及
洛谷 P10135 Potion Farming 普及
洛谷 [[P11251 美丽路径]] 普及
洛谷 [[P11378 燃烧]] 普及
洛谷 [[AT_agc001_c Shorten Diameter]] 普及+ ⭐⭐⭐ 树的直径 + 枚举中心 要使树的直径 \(\le K\),等价于找一个最大的节点集合使得集合内任意两点距离 \(\le K\),答案为 \(n\) 减去该集合大小;\(K\) 为偶数时枚举每个点作为中心,DFS 保留距离中心不超过 \(K/2\) 的所有点;\(K\) 为奇数时枚举每条边作为中心边,DFS 分别保留两端距离该边不超过 \((K-1)/2\) 的点,取所有方案中保留节点数的最大值,时间复杂度 \(O(n^2)\)\(n \le 2000\))。
洛谷 P1352 没有上司的舞会 普及+
洛谷 P2018 消息传递 普及+ ⭐⭐⭐⭐ 树形DP(换根DP) 以每个节点为根进行DFS,计算该节点向各子树传递消息所需轮数(按子树大小降序排列,第 \(k\) 大的子树需 \(k\) 轮后处理),取所有子树完成时间的最大值作为该根的结果;遍历所有节点找最小值并输出所有达到最小值的根,时间复杂度 \(O(N^2)\)
洛谷 [[P2195 HXY造公园]] 普及+ ⭐⭐⭐⭐ 并查集 + 树的直径(树形DP / 两次DFS) 用并查集维护连通分量,预处理每个连通分量的直径 zj[root](树形DP 或两次 DFS);操作 1 直接输出 zj[find(x)];操作 2 合并两个分量时,新直径为 max((zj[x]+1)/2 + (zj[y]+1)/2 + 1, max(zj[x], zj[y])),即两个分量半径之和加新边长度与原来两直径的最大值,时间复杂度 \(O((n+m+q) \cdot \alpha(n))\)\(n,q \le 3 \times 10^5\))。
洛谷 [[P2610 旅游]] 普及+ ⭐⭐⭐⭐ 树的直径(对偶树建图 + 两次DFS) 将凸 \(n\) 边形的三角剖分转化为对偶树:每个三角形城市为节点,共享边的两个城市之间连边,用 map<PII,int> 记录每条边首次出现的城市编号,第二次出现时在对偶树中建立连接;对偶树上旅游路线经过的城市数等于路径长度 \(+1\),因此问题转化为求对偶树的直径,用两次 DFS 找到直径端点,答案为 d[r] + 1,时间复杂度 \(O(n \log n)\)\(n \le 2 \times 10^5\)map 操作为 \(O(\log n)\))。
洛谷 [[P3304 直径]] 普及+ ⭐⭐⭐⭐ 树形DP(两次DFS + 直径端点回溯 + 公共边判定) 先用两次 DFS 找到一条主直径及其端点 \(l,r\) 并记录父节点 pre[] 回溯路径;然后对直径路径上的每个内部点 \(i\),分别向两侧(不经过直径路径)DFS 求最远距离,若从 \(i\) 出发能找到的最远距离等于 \(i\) 到某一端点的距离,则说明存在另一条直径不经过该侧的边,通过双向收缩端点 \(l\)\(r\) 确定所有直径的公共路径,最终统计 \(l\)\(r\) 路径上的边数即为答案,时间复杂度 \(O(n)\)
洛谷 P5536 核心城市 普及+
洛谷 [[P10725 最远点对]] 普及+
洛谷 [[U81904 树的直径]] 普及+
洛谷 [[CF911F Tree Destruction]] 提高+ ⭐⭐⭐⭐ 树的直径 + 贪心构造 先用两次 DFS 找到树的直径端点 \(l,r\) 并标记直径路径;对于非直径路径上的每个叶子节点 \(x\),贪心选择将其与距离更远的直径端点配对(比较 \(d[x]\)\(d[x] + d[r] - 2 \cdot d[rt]\),其中 \(rt\)\(x\) 在直径上的最近祖先),累加路径长度后删除该叶子;最后从 \(r\)\(l\) 依次将直径上的点与 \(l\) 配对删除,总答案为所有配对路径长度之和,时间复杂度 \(O(n)\)
洛谷 [[P2491 消防]] 提高+ ⭐⭐⭐⭐⭐ 树的直径 + 双指针(滑动窗口) 先用两次 DFS 找到树的直径端点 \(p,r\) 并记录父节点 pre[] 回溯路径;然后在直径路径上用双指针枚举长度不超过 \(s\) 的子路径 \([i,j]\)\(i\)\(r\) 向根移动,\(j\) 维护满足 \(d[j]-d[i] \le s\) 的最远点),计算直径两端到该子路径的最大距离 \(\max(d[i], d[r]-d[j])\) 取最小值作为候选答案;最后对直径路径上每个点 DFS 其非直径分支求最远距离,与候选答案取最大值即为最终答案,时间复杂度 \(O(n)\)
洛谷 [[P4408 逃学的小孩]] 提高+ ⭐⭐⭐⭐ 树的直径 + 树形DP(偏心距) 先用两次 DFS 求出树的直径端点 \(l,r\) 及直径长度 \(d_1\);保存从 \(l\) 出发的距离数组 dd[],再从 \(r\) 出发 DFS 求每个点到 \(r\) 的距离 d[];对于每个点 \(i\)(作为 Chris 家 \(C\)),min(d[i], dd[i]) 表示到较近朋友的距离,取所有点中该值的最大值 \(d_2\)(最坏情况下的最近端点距离),最终答案为 \(d_1 + d_2\),时间复杂度 \(O(n)\)\(n \le 2 \times 10^5\))。
洛谷 [[P3629 巡逻]] 省选 ⭐⭐⭐⭐⭐ 树的直径 + 树形DP 先用两次 DFS 求出树的直径 \(d_1\)\(K=1\) 时直接新建一条道路连接直径两端,直径上的边由往返两次变为只需一次,答案为 \(2(n-1) - d_1\)\(K=2\) 时先标记直径路径,将直径上的边权改为 \(-1\)(表示节省一次往返),然后对整棵树做一次树形 DP 求出经过直径路径上某点的最长链 \(d_2\)(即第二条新建道路能额外节省的最大距离),最终答案为 \(2(n-1) - d_1 - d_2\),时间复杂度 \(O(n)\)
AtCoder AT_awc0043_c Imbalance of the Organization ⭐⭐ 树形DP 以员工 \(1\) 为根DFS,\(s[u]\) 表示以 \(u\) 为根的子树工作量之和,对每个有至少 \(2\) 个子节点的员工 \(u\),计算其所有直接下属子树和的最大值与最小值之差,更新全局答案 \(\max(\text{ans},\, \max s[v] - \min s[v])\)
AtCoder AT_awc0098_e Maintenance of Waterways ⭐⭐⭐⭐⭐ 树形DP + 贪心排序 以聚落 \(1\) 为根DFS,\(dp0[u]\) 表示父边不由 \(u\) 负责时子树最小额外费用,\(dp1[u]\) 表示父边由 \(u\) 负责时子树最小额外费用;对每个节点计算子边"改由 \(u\) 负责"的费用差 \(\Delta = dp0[v] - dp1[v]\),排序后枚举 \(u\) 负责的子边数量 \(k\),取 \(\min(\text{cost} + \sum_{i=1}^{k}\Delta_i + W_u \times \max(0, m - C_u))\),答案为 \(N-1 + dp0[1]\)

【树上背包】

算法模板

树上01背包

// 状态表示:f[i][j],以i为根的树中,包含j个叶子节点的最大收益
// 初值:f[i][0]=0 (1-n-m),f[i][1]=p[i] (n-m+1,n)
// 状态转移:f[u][j] = max(f[u][j], f[u][j-k] + f[v][k] - w)
// 结果:f[1][i]>=0的最大i(倒序枚举i,从m-1)

void dfs(int u, int fa)  // 树形DP,u当前节点,fa父节点
{
    for (int i = 0; i < ve[u].size(); i++)  // 遍历所有子节点
    {
        int v = ve[u][i].v;  // 子节点
        int w = ve[u][i].c;  // 连接u和v的边权
        if (v == fa)  // 如果是父节点,跳过
            continue;
        dfs(v, u);  // 递归处理子树
        cnt[u] += cnt[v];  // 更新子树用户总数
        for (int j = cnt[u]; j >= 1; j--)  // 背包容量(用户数)从大到小枚举
        {
            for (int k = 1; k <= min(j, cnt[v]); k++)  // 从子树v中选择k个用户
            {
                // 如果u的j-k个用户或v的k个用户不可行,跳过
                if (f[u][j - k] == -0x3f3f3f3f || f[v][k] == -0x3f3f3f3f)
                    continue;
                // 状态转移:从子树v中选k个用户,加上从u的子树中选j-k个用户,减去边权w
                f[u][j] = max(f[u][j], f[u][j - k] + f[v][k] - w);
            }
        }
    }
}

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 10 有依赖的背包问题
AcWing [[286 选课]]
洛谷 P1064 金明的预算方案 普及+
洛谷 P1273 有线电视网 普及+ ⭐⭐⭐⭐⭐ 树上背包(树形DP) 定义 f[u][i] 为以 \(u\) 为根的子树选 \(i\) 个用户的最大净收益(用户付费减去传输费用),对每棵子树做背包合并(倒序枚举容量、枚举子树选取用户数),叶子节点初始化 f[i][1]=p[i],最终从大到小找 f[1][i] >= 0 的最大 \(i\),时间复杂度 \(O(N \cdot M^2)\)
洛谷 P2014 选课 普及+
洛谷 P2015 二叉苹果树 普及+
洛谷 P1272 重建道路 提高+

【状压DP】

算法模板

棋盘式状压DP

// 棋盘式状压DP模板
vector<int> state;
vector<int> head[M];  //状态转移关系

//自定义工具
bool check(int state)  // 检查状态是否合法
{
    for (int i=0; i<n; i++)
        if ((state>>i & 1) && (state>>i+1 & 1))  // 根据题意设计
            return false;
    return true;
}
int count(int state)  // 计算状态中1的个数
{
    int res = 0;
    for (int i=0; i<n; i++) res += state>>i & 1;
    return res;
}

//预处理过程
for (int i=0; i<(1<<n); i++)  // 预处理所有合法状态
    if (check(i))
    {
        state.push_back(i);
        id[i] = state.size() - 1;  // 记录合法状态对应的下标
        cnt[i] = count(i);  // 记录合法状态中1的个数
    }
for (int i=0; i<state.size(); i++)  // 预处理a状态和b状态之间的转移关系
    for (int j=0; j<state.size(); j++)
    {
        int a = state[i] , b = state[j];
        if ((a&b)==0 && check(a|b))  // 如果合法,则可以从b状态转移至a状态
            head[i].push_back(j);
    }

//状态转移
f[0][0][0] = 1;
for (int i=1; i<=n+1; i++)
    for (int j=0; j<=k; j++)
        for (int a=0; a<state.size(); a++)
            for (int b : head[a])
            {
                int c = cnt[state[a]];
                if (j>=c)
                    f[i][j][a] += f[i-1][j-c][b];  // 状态转移方程
            }
cout << f[n+1][k][0] << endl;

状态遍历

// 从s点出发,遍历所有节点,回到t点
// 初始化
cosnt int INF = 1e18;
for (int mask = 0; mask<(1<<k); mask++)
    for (int i=1; i<=k; i++)
        dp[mask][i] = INF;
dp[0][0] = 0;  // s点编号为0

for (int mask=0; mask<(1<<m); mask++)  // 遍历所有状态,m为节点数量(从0-m)
{
    for (int u=0; u<=m; u++)  // 遍历当前最后到达的点
    {
		if (dp[mask][u] == 0x3f3f3f3f) continue;
		for (int v=1; v<=m; v++)
		{
			if (mask & (1<<(v-1))) continue;  // v点已经访问过则跳过
			int new_mask = mask | (1<<(v-1));
			int new_value = dp[mask][u] + dist[u][v];
			if (new_value < dp[new_mask][v])
				dp[new_mask][v] = new_value;
		}
	}
}

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 91 最短Hamilton路径
AcWing 291 蒙德里安的梦想
AcWing 292 炮兵阵地
AcWing 327 玉米田
AcWing 487 金明的预算方案
AcWing 524 愤怒的小鸟
AcWing 529 宝藏
AcWing 1064 小国王 简单 ⭐⭐⭐⭐⭐ 状压DP(状态压缩动态规划) \(n\) 位二进制数表示每行国王放置状态,预处理筛选无相邻 \(1\) 的合法状态并计算各状态国王数,再预处理相邻两行合法转移关系(无同列、无对角线相邻),最后按行DP:f[i][j][a] 表示前 \(i\) 行放 \(j\) 个国王且第 \(i\) 行为状态 \(a\) 的方案数,时间复杂度 \(O(n \cdot k \cdot S^2)\)\(S\) 为合法状态数)。
洛谷 AT_abc415_c Mixture 普及
洛谷 AT_abc359_d Avoid Palindrome 普及+
洛谷 AT_abc402_e Payment Required 普及+
洛谷 P10447 最短Hamilton路径 普及+
洛谷 P10449 费解的开关 普及+
洛谷 P1879 Corn Fields 提高+
洛谷 P1896 互不侵犯 提高+
洛谷 P2704 炮兵阵地 提高+
洛谷 P2831 愤怒的小鸟 提高+
洛谷 P3959 宝藏 省选
AtCoder AT_awc0007_e Warehouse Robot Delivery ⭐⭐⭐⭐⭐ 状压DP(TSP型) 先预处理所有关键点(起点、\(M\) 个中间点、终点)两两之间的曼哈顿距离,然后用状态压缩DP:dp[mask][u] 表示已访问 mask 所代表的中间点集合且当前位于点 \(u\) 的最小距离,枚举下一个未访问点进行转移,最后枚举访问完所有中间点后的位置加上到终点的距离取最小值,时间复杂度 \(O(2^M \cdot M^2)\)
AtCoder AT_awc0016_e Optimal Route for a Sightseeing Tour ⭐⭐⭐⭐⭐ 状压DP + SPFA松弛(图上的状压最短路) 定义 dp[mask][u] 为已访问景点集合 mask 且当前在 \(u\) 的最大利润,用类似SPFA的迭代松弛方式更新:从 \(u\) 沿边 \((u,v,w)\) 走到 \(v\),新利润为当前利润减边权(若 \(v\) 首次访问则加 \(P_v\)),持续松弛直到无更新,最终答案为所有包含终点 \(T\) 的状态中 dp[mask][T] 的最大值,时间复杂度 \(O(2^N \cdot N \cdot M)\)(最坏情况)。
AtCoder AT_awc0017_d Team Building ⭐⭐⭐⭐ 状压DP(带惩罚项的子集选择) 定义 dp[mask][cnt] 为已选员工集合 mask(共 cnt 人)时的最大团队实力,枚举添加新员工 \(u\),惩罚值为 \(u\) 与已选员工中所有不兼容对的 \(B_j\) 之和,状态转移为 dp[new_mask][cnt+1] = max(dp[new_mask][cnt+1], dp[mask][cnt] + A[u] - penalty),最终答案为所有 popcount(mask)==Kdp[mask][K] 最大值,时间复杂度 \(O(2^N \cdot N \cdot M)\)
AtCoder AT_awc0017_e Optimization of Delivery Routes ⭐⭐⭐⭐⭐ 状压DP(标准TSP) 预处理所有点对之间的欧几里得距离平方,用状态压缩DP:dp[mask][u] 表示已访问 mask 中的点且当前在点 \(u\) 的最小花费,从地点1出发枚举未访问点转移,最终答案为 \(\min_i(dp[(1<<N)-1][i] + dist[i][1])\),时间复杂度 \(O(2^N \cdot N^2)\)
AtCoder [[AT_awc0022_e Delivery Driver's Route]]
AtCoder AT_awc0029_e Traveling Delivery ⭐⭐⭐⭐⭐ Floyd全源最短路 + 状压DP(TSP型) 先用 Floyd 算法 \(O(N^3)\) 预处理所有点对之间的最短距离,再用状压DP:dp[mask][u] 表示已访问 mask 中的目的地且当前在 \(T_u\) 的最小花费,从起点 \(S\) 出发枚举未访问目的地转移,最终答案为 \(\min_i(dp[(1<<K)-1][i] + dist[T_i][S])\),若不可达输出 \(-1\),总时间复杂度 \(O(N^3 + 2^K \cdot K^2)\)
AtCoder AT_awc0046_e A Walk and Barricades
AtCoder AT_awc0062_c Optimal Menu Selection for an Izakaya ⭐⭐ 状压DP(子集枚举) 枚举所有 \(2^N\) 个子集,利用 lowbit 技巧递推预处理每个子集的美味度总和 sumA[mask] 和浓郁度总和 sumB[mask],最终答案为所有子集中 \(\text{sumA}[mask] - D \times \max(0, \text{sumB}[mask] - K)\) 的最大值,时间复杂度 \(O(2^N)\)

【区间DP】

算法模板

区间扩张型(区间长度为2)

// 状态表示:f[i][j]表示从第i项到第j项之间的最优解

// 初值
// 初始化区间长度为1的值(依据题目要求修改初值)
memset(f, 0x3f, sizeof(f));
for (int i=1; i<=n; i++)
	f[i][i] = 0;

// 状态转移
for (int len=2; len<=n; len++)
	for (int i=1; i+len-1<=n; i++)
	{
		int j = i + len - 1;
		for (int k=i; k<=j)
		// 状态转移
	}

//结果
f[1][n]

区间扩张型(区间长度为3)

// 状态表示:f[i][j]表示从第i项到第j项之间的最优解

// 初值
// 初始化区间长度为1、区间长度为2的值(依据题目要求修改初值)
memset(f, 0x3f, sizeof(f));
for (int i=1; i<=n; i++)
{
	f[i][i] = 0;
	f[i][i+1] = 0;
}
// 状态转移
for (int len=3; len<=n; len++)
	for (int i=1; i+len-1<=n; i++)
	{
		int j = i + len - 1;
		for (int k=i; k<=j)
		// 状态转移
	}

//结果
f[1][n]

掐头去尾型

// 状态转移
f[i][j] = f[i][j-1]  // 去尾
		  f[i+1][j]  // 去头
          f[i+1][j-1]  // 掐头去尾(s[i]与s[j]需要满足特定关系)

环形

a[i+n]=a[i]
// 结果:枚举期间i,一圈长度为n,则终点 i+n-1
max或min (f[i][i+n-1]) (1<=i<=n)

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 282 石子合并
AcWing [[320 能量项链]]
AcWing 321 棋盘分割
AcWing 479 加分二叉树
AcWing 1026 乘积最大
AcWing 1028 复制书稿
AcWing 1048 鸡蛋的硬度 中等 ⭐⭐⭐⭐ 区间DP / 状态转换 将原问题转换为 \(f[i][j]\) 表示用 \(i\) 个鸡蛋、\(j\) 次尝试能确定的最大楼层数,枚举在第 \(k\) 层扔鸡蛋,状态转移为 \(f[i][j] = \min\limits_{1\le k\le i}\big(\max(f[k-1][j-1],\, f[i-k][j]) + 1\big)\),其中碎与不碎分别对应左右子区间的最坏情况,最终答案为使得 \(f[n][j] \ge m\) 的最小 \(j\)
AcWing 1068 环形石子合并
AcWing 1069 凸多边形的划分
AcWing 1262 鱼塘钓鱼
AcWing 4918 万圣节服饰
AcWing 4919 括号
AcWing 4920 乘法谜题 简单 ⭐⭐⭐⭐ 区间DP \(f[i][j]\) 表示保留端点 \(i\)\(j\)、消除区间 \((i,j)\) 内所有元素的最小得分,枚举最后删除的元素 \(k\)\(i < k < j\)),转移 \(f[i][j] = \min(f[i][j],\, f[i][k] + f[k][j] + a_i \cdot a_k \cdot a_j)\),按区间长度从小到大递推,答案为 \(f[1][n]\)
AcWing 4921 食品配送 简单 ⭐⭐⭐⭐ 区间DP 将餐厅位置作为特殊点加入数组后排序,定义 \(f[i][j][0/1]\) 表示送完区间 \([i,j]\) 内所有人后停在左端点/右端点的最小总不满值,从餐厅位置向左右双向扩展,每次移动的代价为未配送人员的不满值增长率之和 \(\times\) 移动距离 \(\times V\),最终答案为 \(\min(f[1][n][0],\, f[1][n][1])\)
AcWing 6023 合并石子
洛谷 P17015 消消乐 普及 ⭐⭐⭐⭐ 区间动态规划 dp[i][j] 为删除区间 \([i,j]\) 的最大得分,枚举最后删除的元素 \(k\),此时 \(k\) 的相邻元素固定为 \(a_{i-1}\)\(a_{j+1}\),转移 dp[i][j] = max(dp[i][k-1] + dp[k+1][j] + a[i-1] + a[j+1]),按区间长度从小到大计算,时间复杂度 \(O(n^3)\),空间复杂度 \(O(n^2)\)
洛谷 AT_abc419_e Subarray Sum Divisibility 普及+
洛谷 P1005 矩阵取数游戏 普及+
洛谷 P1026 统计单词个数 普及+
洛谷 P1040 加分二叉树 普及+
洛谷 P1063 能量项链 普及+
洛谷 P1880 石子合并 普及+
洛谷 [[P3004 宝箱]] 普及+
洛谷 CF149D Coloring Brackets 提高+
AtCoder AT_awc0063_d Card Taking Game ⭐⭐⭐⭐ 区间DP(minimax博弈) 定义 \(dp[i][j]\) 为当前玩家在区间 \([i,j]\) 能获得的最大得分差值(当前 \(-\) 对手),状态转移为 \(dp[i][j] = \max(V_i - dp[i+1][j],\; V_j - dp[i][j-1])\)(取左/右端后对手在剩余区间上的最优差值被反转),初始化 \(dp[i][i]=V_i\),最终答案为 \(dp[1][N]\),时间复杂度 \(O(N^2)\)
其他 [[快餐问题]]

【数位DP】

算法模板

基础数位DP(固定进制,如十进制)

// 计算 [0, X] 内满足条件的数的个数
// digits:X 的各位数字
// pos:当前处理位
// tight:是否受上界约束
// lead:是否有前导零
// state:其他状态

int dfs(int pos, bool tight, bool lead, int state) {
    if (pos == -1) return check(state);  // 到达末尾,检查状态

    int up = tight ? digits[pos] : 9;    // 当前位上限
    int res = 0;

    for (int i = 0; i <= up; i++) {
        res += dfs(pos - 1,
                   tight && (i == up),   // 新的tight状态
                   lead && (i == 0),     // 新的前导零状态
                   update(state, i));     // 更新状态
    }

    return res;
}

混合进制数位DP

// b:递减的有效模数序列
// g:预处理数组,g[i] = [0, b[i]-1] 内满足条件的个数

// 计算 [0, x] 内从第 pos 层开始满足条件的数的个数
int f(int x, int pos) {
    // 基准:超出模数范围
    if (pos >= m) return x / b.back() + 1;

    // 找到第一个 <= x 的模数
    auto it = lower_bound(b.begin() + pos, b.end(), x, greater<int>());
    if (it == b.end()) return x / b.back() + 1;

    int p = it - b.begin();          // 当前层位置
    int q = x / b[p];                // 完整周期数
    int r = x % b[p];                // 剩余部分

    // q 个完整周期 + 递归处理剩余
    return q * g[p] + f(r, p + 1);
}

// 预处理 g(从右往左)
void precompute() {
    g[m - 1] = 1;  // 最后一层:[0, b[m-1]-1] 内只有 0 满足
    for (int i = m - 2; i >= 0; i--) {
        int full = (b[i] - 1) / b[i + 1];
        int rem = (b[i] - 1) % b[i + 1];
        g[i] = full * g[i + 1] + f(rem, i + 2);
    }
}

带记忆化的通用数位DP

map<tuple<int, bool, int>, int> memo;

int dfs(int pos, bool tight, int state) {
    if (pos < 0) return check(state);

    auto key = make_tuple(pos, tight, state);
    if (memo.count(key)) return memo[key];

    int up = tight ? digits[pos] : LIMIT;
    int res = 0;

    for (int i = 0; i <= up; i++) {
        res += dfs(pos - 1, tight && (i == up), update(state, i));
    }

    return memo[key] = res;
}

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 338 计数问题
AcWing 1081 度的数量
AcWing 1082 数字游戏
AcWing 1083 Windy数
AcWing 1084 数字游戏II
AcWing 1085 不要62
AcWing 1086 恨7不成妻
洛谷 AT_abc387_c Snake Numbers 普及

【期望DP】

算法模板

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
洛谷 P2719 搞笑世界杯 普及
洛谷 AT_abc360_e Random Swaps of Balls 普及+
洛谷 AT_abc421_e Yacht 普及+
洛谷 P1654 OSU 提高+
洛谷 P1850 换教室 提高+

【斜率优化DP】

算法模板

算法应用

题目来源 标题 难度 星级 考察算法 一句话思路总结
AcWing 300 任务安排1
AcWing 301 任务安排2
AcWing 302 任务安排3
AcWing 303 运输小猫
posted @ 2026-06-27 14:22  团爸讲算法  阅读(32)  评论(0)    收藏  举报