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

动态规划之【树形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. 输入处理
  • 第一行两个整数 NM,分别表示课程总数和需要选修的门数。
  • 接下来 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;
}
相关推荐
七点半7702 小时前
FFmpeg C++ AI视觉开发核心手册 (整合版)适用场景:视频流接入、AI模型预处理(抽帧/缩放/格式转换)、高性能算法集成。
c++·人工智能·ffmpeg
A.A呐2 小时前
【C++第二十八章】单例模式
c++·单例模式
玖釉-2 小时前
C++ 硬核剖析:if 语句中的“双竖杠” || 到底怎么运行的?
开发语言·c++
m0_716765233 小时前
数据结构三要素、时间复杂度计算详解
开发语言·数据结构·c++·经验分享·笔记·算法·visual studio
And_Ii3 小时前
3740. 三个相等元素之间的最小距离 I
c++·算法
biter down3 小时前
C++11 可变参数模板
开发语言·c++
YYYing.3 小时前
【Linux/C++网络篇(一) 】网络编程入门:一文搞懂 TCP/UDP 编程模型与 Socket 网络编程
linux·网络·c++·tcp/ip·ubuntu·udp
bkspiderx3 小时前
libwebsockets 详解:介绍、交叉编译与使用指南
c++·websocket·libwebsockets
W23035765733 小时前
经典算法详解:最大子数组和(暴力 / 分治 / 动态规划 / 线段树)
算法·动态规划·最大字段和