当前位置: 首页 > news >正文

信息学奥赛一本通 1576:【例 2】选课 | 洛谷 P2014 [CTSC1997] 选课

【题目链接】

ybt 1576:【例 2】选课
洛谷 P2014 [CTSC1997] 选课

【题目考点】

1. 树形动规
2. 有依赖的背包(树形背包)

选择一个物品的前提是选另一件物品,而这些依赖关系构成了一个树形关系。在背包容量有限的情况下,求选出的物品的最大总价值。

【解题思路】

本题是有依赖的背包模板题。该类背包问题可以通过树形动规来解决。
每门课程是一个结点。课程是学分是结点的权值(点权)。如果a课程是b课程的先修课,那么结点a与结点b之间有一条边。
假想所有不要求有先修课的课程,都以0号课程为先修课。这样整棵树就以0号结点为根。该0号结点必须选择,因此结点数n与边数m要在输入的数值基础上增加1。
本题抽象为:在有n个结点的树上选择m个结点,必须选择一个结点的前提是选择这个结点的双亲,求选出结点的最大点权加和。
使用树形动规算法解决该问题。

解法1:树形动规 有依赖的背包 原解法

状态定义
  • 阶段:以u为根的树的前i个子树,选择结点数量j
  • 决策:选择几个结点
  • 策略:选择结点的方案
  • 策略集合:根结点u的前i个子树中选择j个结点的所有方案
  • 条件:权值和最大
  • 统计量:权值和

dpu,i,jdp_{u,i,j}dpu,i,j: 根结点u的前i个子树中选择j个结点的所有方案中,权值和最大的方案的权值和。

状态转移方程
  • 策略集合:根结点u的前i个子树中选择j个结点的所有方案
  • 分割策略集合:根据对每个子树选择结点数不同来分割策略集合

wuw_uwu为结点u的权值。
对于以u为根的树,如果选0个结点,权值和为0。dpu,0,0=0dp_{u,0,0} = 0dpu,0,0=0
如果选1个结点,一定会选择结点u,dpu,0,1=wudp_{u,0,1} = w_udpu,0,1=wu
如果要选择j个结点,选择结点数最少1个,最多m个。在子树中需要选择j-1个结点。
对于结点u的第i个孩子v(v的孩子数量为cvc_vcv)
如果在以v为根的子树中选择k个结点,选出结点的最大权值加和为dpv,cv,kdp_{v,c_v,k}dpv,cv,k
接下来需要在结点u的前i-1个子树中选择j-k个结点,选出结点的最大权值加和为dpu,i−1,j−kdp_{u, i-1, j-k}dpu,i1,jk
二者加和即为在以u为根树的前i个子树中选出j个结点能得到的最大权值加和。
k最小为0,最大为j-1,对所有k的取值情况,求该表达式的最大值,即为所求的状态。
dpu,i,j=max⁡0≤k<j{dpu,i−1,j−k+dpv,cv,k}dp_{u,i,j} = \underset{0\le k < j}\max\{dp_{u,i-1,j-k}+dp_{v,c_v,k}\}dpu,i,j=0k<jmax{dpu,i1,jk+dpv,cv,k}

由于本题输入时就已知结点之间的双亲-孩子关系,那么就可以建有向图。使用vector数组vector<int> edge[N];保存有向图的邻接表。那么edge[v].size()就是结点v的孩子数量cvc_vcv
在dfs的过程中,访问到结点u,先设初始状态dpu,0,0dp_{u,0,0}dpu,0,0。而后遍历孩子,从结点u访问孩子v,在完成从v出发的搜索后,再根据状态转移方程完成从孩子v到双亲u的递推。
最终以结点0为根,前c0c_0c0个子树中选择m个结点能获得的最大权值dp0,c0,mdp_{0,c_0,m}dp0,c0,m即为本题的结果。
时间复杂度:dfs中双重循环递推是O(m2)O(m^2)O(m2)复杂度,dfs搜索递归调用的次数为n,因此总体时间复杂度为O(nm2)O(nm^2)O(nm2)
空间复杂度为O(n2m)O(n^2m)O(n2m)

优化解法1:树形动规 滚动数组优化

在求dpu,i,jdp_{u,i,j}dpu,i,j时,只用到了第二维为i-1时的数据dpu,i−1,j−kdp_{u,i-1,j-k}dpu,i1,jk
可以进行滚动数组优化,同时必须将第三维jjj倒序遍历。
滚动数组优化方法见:ybt 1267:【例9.11】01背包问题
而后就可以将i所在的第二维优化掉。
空间复杂度降为:O(nm)O(nm)O(nm)

优化解法2:树形动规 滚动数组优化 结点数优化

考察子树结点总数对状态转移的限制。
sizusiz_usizu表示根结点为u的树的结点数。
深搜过程中,sizusiz_usizu表示根结点为u的树的根结点加上前i个子树的结点数。
在访问到u的孩子v时,先将sizusiz_usizu增加以v为根的子树的结点数sizvsiz_vsizv

  1. 根结点为u的树取前i个子树的结点,取到的结点数量一定小于等于sizusiz_usizu,所以j的最大值取min⁡{sizu,m}\min\{siz_u,m\}min{sizu,m}
  2. 根结点为v的子树取到的结点数量一定小于等于sizvsiz_vsizv,因此在子树v中选择的结点数量k的最大值为min⁡{j−1,sizv}\min\{j-1, siz_v\}min{j1,sizv}

加上以上限制后,代码的时间复杂度降至O(n2)O(n^2)O(n2),具体证明见证明:有依赖背包结点数优化后为O(n^2)

【题解代码】

解法1:树形动规 有依赖的背包 三维状态

时间复杂度:O(nm2)O(nm^2)O(nm2) ,空间复杂度:O(n2m)O(n^2m)O(n2m)

#include<bits/stdc++.h>
using namespace std;
#define N 305
int n, m, w[N], dp[N][N][N];//dp[u][i][j]:结点u的前i个孩子,最多选择j门课能获得的最大学分 
vector<int> edge[N];
void dfs(int u)
{int i = 1;dp[u][0][1] = w[u];for(int v : edge[u]){dfs(v);		for(int j = 1; j <= m; ++j)//在树中选j门课 for(int k = 0; k < j; ++k)//在子树v中选了k门课 (因为还要选v,最多选j-1门) dp[u][i][j] = max(dp[u][i][j], dp[u][i-1][j-k]+dp[v][edge[v].size()][k]); i++;}
}
int main()
{int f;cin >> n >> m;n++, m++;//算上0号结点 for(int i = 1; i < n; ++i){cin >> f >> w[i];edge[f].push_back(i);}dfs(0);cout << dp[0][edge[0].size()][m];return 0;
}

优化解法1:树形动规 有依赖的背包 滚动数组优化

时间复杂度:O(nm2)O(nm^2)O(nm2) ,空间复杂度:O(nm)O(nm)O(nm)

#include<bits/stdc++.h>
using namespace std;
#define N 305
int n, m, w[N], dp[N][N];//dp[u][i][j]:结点u的前i个孩子,最多选择j门课能获得的最大学分 
vector<int> edge[N];
void dfs(int u)
{dp[u][1] = w[u];//选1门课,就只能选自己 for(int v : edge[u]){dfs(v);for(int j = m; j >= 1; --j)//在树中选j门课 for(int k = 1; k < j; ++k)//在子树v中选了k门课 (因为还要选v,最多选j-1门) dp[u][j] = max(dp[u][j], dp[u][j-k]+dp[v][k]); }
}
int main()
{int f;cin >> n >> m;n++, m++;//算上0号结点 for(int i = 1; i < n; ++i){cin >> f >> w[i];edge[f].push_back(i);}dfs(0);cout << dp[0][m];return 0;
}

优化解法2:树形动规 有依赖的背包 结点数优化

时间复杂度:O(n2)O(n^2)O(n2) ,空间复杂度:O(nm)O(nm)O(nm)

#include<bits/stdc++.h>
using namespace std;
#define N 305
int n, m, w[N], siz[N], dp[N][N];//dp[u][i][j]:结点u的前i个孩子,最多选择j门课能获得的最大学分 
vector<int> edge[N];
void dfs(int u)
{dp[u][1] = w[u];//选1门课,就只能选自己 siz[u] = 1;for(int v : edge[u]){dfs(v);siz[u] += siz[v];for(int j = min(m, siz[u]); j >= 1; --j)//在树中选j门课 for(int k = 1; k < j && k <= siz[v]; ++k)//在子树v中选了k门课 (因为还要选v,最多选j-1门) dp[u][j] = max(dp[u][j], dp[u][j-k]+dp[v][k]); }
}
int main()
{int f;cin >> n >> m;n++, m++;//算上0号结点 for(int i = 1; i < n; ++i){cin >> f >> w[i];edge[f].push_back(i);}dfs(0);cout << dp[0][m];return 0;
}
http://www.dtcms.com/a/290698.html

相关文章:

  • Netty中CompositeByteBuf的使用
  • 位标志法处理多选字段在数据库中的存储方式 查询效率与扩展性之间的权衡
  • https正向代理 GoProxy
  • 苹果最新系统iOS 17的调试和适配方法 - Xcode 14.3.1 真机调试指南
  • How does Misinformation Affect Large Language ModelBehaviors and Preferences?
  • Spring Boot 集成 RabbitMQ:普通队列、延迟队列与死信队列全解析
  • iOS WebView 调试实战 页面跳转失效与历史记录错乱的排查路径
  • 物流链上的智慧觉醒:Deepoc具身智能如何重塑搬运机器人的“空间思维”
  • 达梦数据库JSON_TABLE使用说明
  • grpc: debug: GRPC_TRACE
  • ESP32开发——基于idf框架使用NVS操作存储设备读写
  • 家庭服务具身智能机器人体系架构
  • 一次 POI 版本升级踩坑记录
  • lesson20:Python函数的标注
  • docker nginx 部署前端踩坑记录
  • WinUI3开发_Frame用法
  • MYSQL:数据库约束
  • 【PTA数据结构 | C语言版】拓扑排序
  • 通信刚需小能手,devicenet转PROFINET网关兼容物流分拣自动化
  • 自动化计算机经过加固后有什么好处?
  • OpenAI API(2) OpenAI Responses API使用
  • 设备管理系统(MMS)如何在工厂MOM功能设计和系统落地
  • 深入解析 Linux 硬链接与软链接:原理、区别及应用场景
  • 龙虎榜——20250721
  • Linux中ELF区域与文件偏移量的关系
  • 【AI论文】EXAONE 4.0:融合非推理模式与推理模式的统一大语言模型
  • Neovim 安装与解压 tar.gz 文件
  • AXI接口学习
  • Python 模块未找到?这样解决“ModuleNotFoundError”
  • Dev C++下载安装和使用教程(图文并茂,保姆级教程)