P2014 [CTSC1997] 选课(树上背包模板题)题解
本蒟蒻最近写了一道树上背包模板题的题,对奆佬们来说简单,对本蒟蒻有亿点点吃力。详情请[戳此(https://www.luogu.com.cn/problem/P2014)]
具体内容如下:
题目描述
在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 N 门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b)。一个学生要从这些课程里选择 MM 门课程学习,问他能获得的最大学分是多少?
输入格式
第一行有两个整数 N , M用空格隔开。( 1 ≤ N ≤ 300 , 1 ≤ M ≤ 300 )
接下来的 N 行,第 I+1 行包含两个整数 ki 和 si,ki 表示第 I 门课的直接先修课,si 表示第 I 门课的学分。若 ki=0 表示没有直接先修课(1 ≤ ki ≤ N , 1 ≤ si ≤ 20)。
输出格式
只有一行,选 M 门课程的最大得分。
输入输出样例
Sample In
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
Sample Out
13
一看到题目,我们就可以发现,这道题我们可以建立一棵树来存储 ki 与 i 的关系, ki 为节点 i 的父节点,利用树形 dp 求解。
我们可以建立一个 f[i][j] 的滚动数组,表示以 i 为根的节点选了 j 门课程取得的最大学分数。
这个状态我们可以转移到其他子树上,那么问题来了,状态转移方程怎么推?
由题可知,我们要想选择第i门课,就一定要选择它的爸爸,假使选择它的爸爸,那么我们可以不选第i门课,所以
f[i][j]=max( f[i][j] , f[i][j-k]+f[y][k])
注:f[y][k]表示在以i的儿子y为根的子树中选择k门课程,f[i][j-k]即为在以i为根的子树中选择剩余的j-k门课程,这比较类似于分组背包,只不过是在树上操作罢了。
首先要写一个dp部分的代码,如果不知如何写的小盆友可以理解一下
void dp(int x){
f[x][0]=0;//表示在以x为根的子树中 选择0门课程所得到的最大学分
for(int i=0;i<son[x].size();i++){//枚举每一个儿子
int y=son[x][i];
dp(y);
for(int j=m;j>=0;j--) //枚举背包体积
for(int k=j;k>=0;k--)
//为何从j开始枚举?因为当x=0时是一个虚拟节点,需要选的就是j门课
f[x][j]=max(f[x][j],f[x][j-k]+f[y][k]);
//在以x为根的子树中选择j门课程取得的最大学分
//就是在自身(即只选择自己而不选择自己的儿子)和在以自己为根的子树中选择j-k门课
//以及在自己的儿子中选择k门课,取一个学分较大的
}
if ( x!=0 ) //当前节点不为虚拟节点,进行状态转移,
//就等于选择了前j-1门课程后,再加上自己的学分,
//从而保证x这个节点一定要选取,因为它是先修课
for(int i=m;i>0;i--)
f[x][i]=f[x][i-1]+sc[x];
}
注:我们给没爸爸的节点(即没有直接选修课的节点)的爸爸赋为0。
疑问1:背包枚举时为何要倒着循环?
答:保证同一个物品最多只能选一次。
疑问2:树上背包时间复杂度?
答:树上背包时间复杂度可以优化成O(nm),(注:n为节点数,m为体积的值域,即取
值范围),具体的优化方法请见此,本蒟蒻实力有限qwq,请各位小盆友参考奆佬
的文章。[戳这(https://www.cnblogs.com/ouuan/p/BackpackOnTree.html)]
疑问3:为何不直接用二维数组存储状态?
答:此处用滚动数组存储状态,是因为使用二维数组会出现MLE,使用滚动数组vector可以避免这种情况。
最后附上AC代码,供参考
#include<bits/stdc++.h>
using namespace std;
const int N=330,M=330;
int n,m,f[N][M],sc[N];
vector <int> son[N];
void dp(int x){
f[x][0]=0;
for(int i=0;i<son[x].size();i++){
int y=son[x][i];
dp(y);
for(int j=m;j>=0;j--)
for(int k=j;k>=0;k--)
f[x][j]=max(f[x][j],f[x][j-k]+f[y][k]);
}
if ( x!=0 )
for(int i=m;i>0;i--)
f[x][i]=f[x][i-1]+sc[x];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
int fa;
scanf("%d%d",&fa,&sc[i]);
//fa是第i门课的直接选修课,
//是i的爸爸,sc[i]表示选修第i门课取得的学分
son[fa].push_back(i);//即输入son[x].size的长度查询
}
memset(f,0xcf,sizeof(f));
dp(0);
//x=0(代表从根节点的爸爸(设一个虚拟节点)开始搜)
printf("%d\n",f[0][m]);
return 0;
}
这是本蒟蒻第一次写博客,如果有写的不好的地方求各位奆佬指正,本蒟蒻感激不已!!!
最后希望各位能理解基础的树上背包,做题常AC!!!

浙公网安备 33010602011771号