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

信息学奥赛一本通 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.xdnf.cn/news/15907.html

相关文章:

  • 子网划分核心原理 (网络原理1)
  • [学习] Hilbert变换:从数学原理到物理意义的深度解析与仿真实验(完整实验代码)
  • 《通信原理》学习笔记——第五章
  • Spring 源码阅读(二) 核心概念解析 ApplicationContext、类型转化
  • 【PyTorch】图像二分类项目
  • 【JS逆向基础】数据库之mysql
  • 7.19-7.20 Java基础 | File类 I/O流学习笔记
  • pyhton基础【27】课后拓展
  • 【Linux】3. Shell语言
  • 深度相机的工作模式(以奥比中光深度相机为例)
  • SQL Server(2022)安装教程及使用_sqlserver下载安装图文
  • 《计算机网络》实验报告四 TCP协议分析
  • 0719代码调试记录
  • IsaacLab学习记录(四)
  • Milvus Dify 学习笔记
  • 题单【循环结构】
  • 基于单片机出租车计价器设计
  • 30天打牢数模基础-决策树讲解
  • 【C语言】字符串与字符函数详解(上)
  • C++ 并发 future, promise和async
  • 数位 dp
  • 「Java案例」利用方法打印乘法表
  • WPF学习笔记(28)Interaction.Triggers的意义与使用方式
  • dify创建OCR工作流
  • NX584NX559美光固态闪存NX561NW993
  • AI(学习笔记第六课) 使用langchain进行AI开发 load documents(csv和文件夹)
  • 开源社区贡献指南:如何通过Three.js插件开发提升企业技术影响力?
  • Windows批量修改文件属性方法
  • swift-关联性/范型
  • 每日算法刷题Day50:7.20:leetcode 栈8道题,用时2h30min