动态规划之【树形DP】第4课:树形DP应用案例实践3

选课
题目描述
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 N N N 门功课,每门课有若干学分,分别记作 s 1 , s 2 , ⋯ , s N s_1,s_2,\cdots,s_N s1,s2,⋯,sN,每门课有一门或没有直接先修课(若课程 a a a 是课程 b b b 的先修课即只有学完了课程 a a a,才能学习课程 b b b)。一个学生要从这些课程里选择 M M M 门课程学习,问他能获得的最大学分是多少?
题目保证课程安排无冲突。(即不会有 a a a 是 b b b 的先修课, b b b 也是 a a a 的先修课这类情况存在。)
输入格式
第一行有两个整数 N N N, M M M 用空格隔开 ( 1 ≤ N ≤ 300 (1 \leq N \leq 300 (1≤N≤300 , 1 ≤ M ≤ 300 ) 1 \leq M \leq 300) 1≤M≤300)。
接下来的 N N N 行,第 i + 1 i+1 i+1 行包含两个整数 k i k_i ki 和 s i s_i si, k i k_i ki 表示第 i i i 门课的直接先修课, s i s_i si 表示第 i i i 门课的学分。若 k i = 0 k_i=0 ki=0 表示没有直接先修课 ( 0 ≤ k i ≤ N (0 \leq {k_i} \leq N (0≤ki≤N, 1 ≤ s i ≤ 20 ) 1 \leq {s_i} \leq 20) 1≤si≤20)。
数据保证至少存在一个 k i = 0 k_i=0 ki=0,即至少一门课无先修课。
输出格式
只有一行,选 M M M 门课程的最大学分。
输入输出样例 1
输入 1
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
输出 1
13
思路分析
题目是典型的"树形依赖背包"问题。
每门课可能有先修课,构成一个森林。为了方便,我们添加一个虚拟根节点 0,将每个无先修课的课程作为 0 的子节点,从而将森林转化为一棵以 0 为根的树。
核心思想:
- 定义
dp[u][j]表示在以u为根的子树中,必须选修课程u的前提下,总共选修j门课程能获得的最大学分。 - 对每个子节点
v进行 DFS,递归得到dp[v][*]。 - 合并子节点时,采用分组背包的方式:当前节点
u已经处理了部分子树,现在要将子节点v的选课方案合并进来。- 外层循环倒序遍历当前已选课程数
j(从大到小,避免重复使用同个子树)。 - 内层循环枚举在子节点
v中选修k门课(k>=1,因为选了u之后才能选v,且v必须选才能获得dp[v][k])。
- 外层循环倒序遍历当前已选课程数
- 初始状态:
dp[u][1] = w[u],即只选自己。
处理虚拟根 0 :
0 不是实际课程,不消耗选课名额,因此对它的所有子节点直接做分组背包,dp0[j] 表示在整棵树中选 j 门课的最大学分。最终答案即为 dp0[m]。
复杂度 :O(N * M^2),但由于子树大小限制,实际运行效率较高,N、M ≤ 300 完全可过。
代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 305;// 最大课程数
int n, m; // n: 课程总数, m: 要选的课程数
vector<int> g[N]; // 邻接表,g[i] 存储课程 i 的直接后继(子节点)
int w[N]; // w[i] 课程 i 的学分
int sz[N]; // sz[i] 以 i 为根的子树大小(包含 i)
int dp[N][N]; // dp[u][j] 以 u 为根的子树中,强制选 u 且共选 j 门课的最大学分
// 深度优先搜索,计算以 u 为根的子树(u 必须选)的 DP 值
void dfs(int u) {
sz[u] = 1; // 初始化子树大小为 1(只有自己)
dp[u][1] = w[u]; // 只选 u 一门课,学分就是 w[u]
// 遍历每个子节点 v
for (int v : g[u]) {
dfs(v); // 先递归处理子节点,得到 dp[v][*] 和 sz[v]
// 合并子节点 v 到 u 的背包中
// 倒序枚举当前已经选了的课程数 j(从 sz[u] 到 1)
// 因为要防止在本次合并中重复使用 v 的贡献
for (int j = min(sz[u], m); j >= 1; --j) {
// 枚举从子节点 v 中选 k 门课(k 至少为 1,因为选了 u 才能选 v)
// 且总课程数不能超过 m
for (int k = 1; k <= sz[v] && j + k <= m; ++k) {
dp[u][j + k] = max(dp[u][j + k], dp[u][j] + dp[v][k]);
}
}
sz[u] += sz[v];// 更新子树大小(所有子节点合并完后才是真实大小)
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
int k, s;
cin >> k >> s;
w[i] = s;
g[k].push_back(i);// k=0 表示无先修课,作为虚拟根 0 的子节点
}
// 对虚拟根 0 的每个子节点分别进行 DFS
for (int v : g[0]) dfs(v);
// 处理虚拟根 0:0 本身不是实际课程,不占用选课名额
// dp0[j] 表示在 0 的子树中选 j 门课的最大学分
int dp0[N] = {0};
int cur = 0; // 当前已经处理过的子树的课程总数上限
for (int v : g[0]) {
// 分组背包合并,倒序枚举当前已选课程数 j
for (int j = min(cur, m); j >= 0; --j) {
// 枚举从子节点 v 中选 k 门课(k>=1)
for (int k = 1; k <= sz[v] && j + k <= m; ++k) {
dp0[j + k] = max(dp0[j + k], dp0[j] + dp[v][k]);
}
}
cur += sz[v];
}
cout << dp0[m] << '\n';
return 0;
}
功能分析
1. 输入处理
- 第一行两个整数
N和M,分别表示课程总数和需要选修的门数。 - 接下来
N行,每行两个整数 k i k_i ki 和 s i s_i si, k_i 是先修课编号(0 表示无先修课), s i s_i si 是学分。
2. 核心功能
- 将课程依赖关系构建成以 0 为根的树。
- 通过树形 DP + 分组背包,计算出在满足先修依赖的条件下,恰好选
M门课能获得的最大总学分。
3. 输出
- 一个整数,表示最大学分。
4. 算法复杂度
- 时间:
O(N * M^2),最坏情况约 300×300×300 ≈ 2.7e7,常数较小,可 AC。 - 空间:
O(N * M),约 300×300 的 DP 数组。
5. 关键点
- 虚拟根:简化森林为树,统一处理。
- 强制选当前节点:保证依赖关系正确。
- 分组背包合并:每个子节点是一个"物品组",在组内只能选一种选法(即选 k 门课)。
- 子树大小优化:限制循环上界,避免无效状态。
完整系列资料,请查看专栏:《csp信奥赛C++动态规划》
https://blog.csdn.net/weixin_66461496/category_13096895.html
各种学习资料,助力大家一站式学习和提升!!!
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"########## 一站式掌握信奥赛知识! ##########";
cout<<"############# 冲刺信奥赛拿奖! #############";
cout<<"###### 课程购买后永久学习,不受限制! ######";
return 0;
}
【秘籍汇总】(完整csp信奥赛C++学习资料):
1、csp/信奥赛C++,完整信奥赛系列课程(永久学习):
https://edu.csdn.net/lecturer/7901 点击跳转

2、CSP信奥赛C++竞赛拿奖视频课:
https://edu.csdn.net/course/detail/40437 点击跳转

https://edu.csdn.net/course/detail/41081 点击跳转

3、csp信奥赛高频考点知识详解及案例实践:
CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转
CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转
信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html 点击跳转
4、csp信奥赛冲刺一等奖有效刷题题解:
CSP信奥赛C++初赛及复赛高频考点真题解析(持续更新): https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转
信奥赛C++提高组csp-s初赛&复赛真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13125089.html 点击跳转
5、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html 点击跳转
· 文末祝福 ·
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"跟着王老师一起学习信奥赛C++";
cout<<" 成就更好的自己! ";
cout<<" csp信奥赛一等奖属于你! ";
return 0;
}
