【记忆化搜索】
算法模板
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 |
|
|
|
|
【混合背包】
算法模板
算法应用
【二维背包】
算法模板
// 状态表示: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]) ...
算法应用
【分组背包】
算法模板
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;
}
}
}
算法应用
【区间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;
}
算法应用
【期望DP】
算法模板
算法应用
【斜率优化DP】
算法模板
算法应用