当前位置: 首页 > 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.lryc.cn/news/595383.html

相关文章:

  • ​NVIDIA V100、H100、A100 和 RTX 5090​​ 的显存
  • C++高性能日志库spdlog介绍
  • 【郑州课工场】深入解析Kubernetes 1.33版本Pod Priority and Preemption功能
  • 【免费版】开启 Youtube 双语字幕
  • C/C++---emplace和emplace_back
  • Go语言的包
  • TSN(时间敏感网络)协议栈在STM32平台(尤其是STM32MP2系列)上的实现
  • 设备虚拟化技术-IRF
  • C++ 中的默认构造函数:非必要,不提供
  • 苍穹外卖Day5
  • B树、B+树的区别及MySQL为何选择B+树
  • Git核心功能简要学习
  • GraphRAG快速入门和原理理解
  • 关于JVM
  • AXI接口学习
  • 上网行为管理-身份认证1
  • 剖析Sully.ai:革新医疗领域的AI助手功能启示
  • Hyperledger Fabric V2.5 生产环境部署及安装Java智能合约
  • 【OD机试】模拟数据序列号传输
  • 09_Spring Boot 整合 Freemarker 模板引擎的坑
  • 用简鹿视频格式转换器轻松制作 GIF 表情包教程
  • 牛客周赛 Round 101(题解的token计算, 76修地铁 ,76选数,76构造,qcjj寄快递,幂中幂plus)
  • 解决vscode中vue格式化后缩进太小的问题,并去除分号 - 设置Vetur tabSize从2到4,设置prettier取消分号semi
  • 元宇宙工厂漫游指南:VR可视化在设备巡检与远程运维中的沉浸式应用
  • zabbix企业级分布式监控
  • Java 实现 UDP 多发多收通信
  • C++unordered系列的map和set类(封装)
  • WAMP配置局域网https服务
  • C# 实现:动态规划解决 0/1 背包问题
  • Nacos 探活机制深度解析:临时 / 永久实例差异及与 Sentinel 的熔断协作