P2014 [CTSC1997] 选课
题目描述
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 NNN 门功课,每门课有若干学分,分别记作 s1,s2,⋯ ,sNs_1,s_2,\cdots,s_Ns1,s2,⋯,sN,每门课有一门或没有直接先修课(若课程 aaa 是课程 bbb 的先修课即只有学完了课程 aaa,才能学习课程 bbb)。一个学生要从这些课程里选择 MMM 门课程学习,问他能获得的最大学分是多少?
输入格式
第一行有两个整数 NNN,MMM 用空格隔开 (1≤N≤300(1 \leq N \leq 300(1≤N≤300 , 1≤M≤300)1 \leq M \leq 300)1≤M≤300)。
接下来的 NNN 行,第 i+1i+1i+1 行包含两个整数 kik_iki 和 sis_isi,kik_iki 表示第 iii 门课的直接先修课,sis_isi 表示第 iii 门课的学分。若 ki=0k_i=0ki=0 表示没有直接先修课 (0≤ki≤N(0 \leq {k_i} \leq N(0≤ki≤N,1≤si≤20)1 \leq {s_i} \leq 20)1≤si≤20)。
输出格式
只有一行,选 MMM 门课程的最大学分。
输入输出样例 #1
输入 #1
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
输出 #1
13
解析
废话不多说,切入正题!
如果将 000 点当做根节点的话,这道题的数据结构显然是树形的,且可以将题意简化成:给定一个以 000 为根节点的树,除根以外有 nnn 个点,每个点都有点权,现保留除 000 以外的点 mmm 个,且保留的点构成以 000 为根节点的树,问保留的点的最大权值为多少?
思考如何动态规划。我们可以发现对于某个节点 uuu ,它可能有多个儿子,设它的子节点集合为 VuV_uVu。而我们发现,在处理以 uuu 为根节点的树时,会涉及到 以 uuu 的儿子为根的子树 的答案,但我们并不关心具体怎么选。所以想到如下策略:
①定义:dpu,jdp_{u,j}dpu,j 表示以 uuu 为根节点的树在保留 jjj 个点时所能获得的最大权值。
②初始状态:
因为以 uuu 为根节点的树保留 000 个点时,啥权值也没有,所以dpu,0=0dp_{u,0}=0dpu,0=0;因为当保留一个点时只能保留根节点,子树权值和就是根权值,所以dpu,1=sidp_{u,1}=s_idpu,1=si
③状态转移方程:
这个比较难推。想象一下,面对一个点 uuu,它的儿子 v∈Vuv\in V_uv∈Vu,我们对这个儿子可选可不选,这似乎是个 010101背包 了。于是把每个子树看做一个物品,子树选择的节点数看做物品重量 weiweiwei,子树权值和看做价值 valvalval,可选的点数看做背包空间。枚举物品 v∈Vuv\in V_uv∈Vu ,背包空间 j∈[1,m]j\in[1,m]j∈[1,m] ( j=0j=0j=0 的情况不必计算,已初始化为 000,枚举时别忘了要倒序),和 010101背包 一样取舍各个子树。
然而由于无法确定各个子树选几个点时对原树来说更优,即每个物品的 weiweiwei,valvalval 随子树选几个点而变化,所以我们还要枚举各棵子树保留 k∈[0,j)k\in[0,j)k∈[0,j) 个点 (显然单棵子树保留不能超过 jjj 个,还要留一个点的份额给 vvv 节点) 时的 weiweiwei,valvalval。计算 010101背包 中的最大价值。(其实就是分组背包,即一类物品中只能选一个,显然以一个点为根的所有树不论大小都是一类)
终于得到动态规划转移方程(在代码里看或许肯定会更直观):
dpu,j=maxv∈Vumaxm≥j≥1max0≤k<j(dpu,j−k+dpv,k)dp_{u,j}=\max_{v\in V_u}\max_{m\geq j\geq 1}\max_{0\leq k<j}{(dp_{u,j-k}+dp_{v,k})}dpu,j=v∈Vumaxm≥j≥1max0≤k<jmax(dpu,j−k+dpv,k)
看上去怪怪的,但想要用一个式子表达只能这样,奇特处解说如下:
当第二个 max\maxmax 中的 jjj 变化时,dpu,jdp_{u,j}dpu,j 中的 jjj 也跟着变化,但 vvv 依旧不变,直到这个 vvv 的所有 jjj 枚举完。
代码
#include<bits/stdc++.h>
using namespace std;
int n,m;
int s[305];
struct EDGE{
int to,nxt;
}edge[305];
int head[305];
int tot;
void add(int u,int v){//链式前向星存图
tot++;
edge[tot].nxt=head[u];
edge[tot].to=v;
head[u]=tot;
}
int dp[305][305];//dp[u][j]表示u号节点选j门课时最大学分
void dfs(int u){
for(int i=head[u];i;i=edge[i].nxt){
dfs(edge[i].to);//必须先处理儿子,因为想要算父亲,要用到儿子的各个状态
}
for(int i=head[u];i;i=edge[i].nxt){//枚举各棵子树
int v=edge[i].to;
for(int j=m;j>=1;j--){//倒着枚举背包空间
for(int k=0;k<j;k++){//枚举这个子树保留k个点
dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]);
}
}
}
}
int main(){
cin>>n>>m;
m+=1;//0点必选,但它不应算在m中,所以可选点加一
for(int i=1;i<=n;i++){
int f;
cin>>f>>dp[i][1];//直接把点权给dp[i][1]
add(f,i);//存图
}
dfs(0);
cout<<dp[0][m];
return 0;
}
值得一提的是,大多数 树形DP树形DP树形DP 都是依赖 DFSDFSDFS 进行的,因为 DFSDFSDFS 能够完美完成子节点回溯到父节点的行为,这是 树形DP树形DP树形DP的关键。

461

被折叠的 条评论
为什么被折叠?



