题目链接:[CTSC1997] 选课 - 洛谷
分析:这是一道树上背包的模板题,什么是树上背包呢?就是说物品之间有依赖关系,就比如说选a就必须要先选b,选b又必须要先选c,大概就是这个意思,类似于我们大学课程之间的关系,但又不完全是,因为我们大学课程中一门课可能有多门先修课,但是树上背包要求每一个物品只能依赖于其他1个物品或者不依赖于其他物品,这个也比较容易理解,毕竟树不就是一对多的关系吗?知道了这个之后,我们来对这道题目进行讲解,首先设f[i][j]表示在以i为根节点的子树中选了j门课所获得的最多学分,下面我们分析动态转移方程:
我们只能用一个点的子节点去更新这个点,所以假如我们当前这颗树的最优解选了k门课,其中一定包含根节点这门课(这个条件一定不能忽视),然后我们可以枚举每一个子节点选的课程数i,然后k-i就是以x为根的子树中去掉当前子节点之外其他节点所选的课程数了,就这样更新完就可以得到最后的答案了。
下面给出一个错误的更新代码,我来带着大家分析一下这样更新为什么是错误的:
for(int j=1;j<=m+1;j++)
for(int k=1;k<=j-1;k++)
f[x][j]=max(f[x][j],f[x][k]+f[son][j-k]);
我们当前更新的是以第x个节点为根的子树,用到的子节点是son,j是以x为根的子树中所选的节点总数,而k是用到的除了当前用到的子节点之外的节点,而j-k就是以当前子节点为根的子树中所选的节点数目了,明白了这些之后我来分析一下这样为什么是错的。
我们假如当前j是7,那么k可以是1~6,在我们k是7之前已经更新完了k是1~6的情况,而且这个更新过程中可能用到了以当前子节点为根的子树中的节点,这就会导致我们先在更新较大的j时f[x][k]也会包含以当前子节点为根的子树中的节点,而由我们刚才的分析明确可以看出k是用到的除了当前用到的子节点之外的节点数,所以这就会导致错误
更正的方法也比较简单,只需要把j的那层循环倒置即可,也就是
for(int j=m+1;j>=1;j--)
for(int k=j-1;k>=1;k--)
f[x][j]=max(f[x][j],f[x][k]+f[son][j-k]);
这样我们在用小于j的k更新j时f[x][k]还没有被更新到,这样更新就不会出现问题了,这里还需要说明的一个技巧就是可能有多个节点没有先修课,所以我们最终的答案就很难统计,我们可以建立一个权值为0的节点,然后让所有没有先修课的课程的先修课为0,这样我们最后就只需要通过f[0][m+1]就可以知道答案了,为什么是m+1呢?因为这个时候我们包含了第个节点,而这只是一个虚拟节点,所以就需要把选m门课改成选m+1门课,前提是别忘了将第0门课的学分设置为0.
下面是代码:
#include<cstdio>
#include<iostream>
#include<cstring>
#include<vector>
#include<algorithm>
#include<map>
#include<cmath>
#include<queue>
using namespace std;
const int N=1003;
int f[N][N];//f[i][j]表示以i为节点的子树中选j门课所获得的最大学分
int n,m,h[N],e[N],ne[N],w[N],idx;
void add(int x,int y)
{
e[idx]=y;
ne[idx]=h[x];
h[x]=idx++;
}
void dfs(int x,int fa)
{
f[x][1]=w[x];//只选根节点
for(int i=h[x];i!=-1;i=ne[i])
{
int son=e[i];
if(son==fa) continue;
dfs(son,x);
for(int j=m+1;j>=1;j--)
for(int k=j-1;k>=1;k--)
f[x][j]=max(f[x][j],f[x][k]+f[son][j-k]);
}
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
int k;
for(int i=1;i<=n;i++)
{
scanf("%d%d",&k,&w[i]);
if(k==0) add(0,i);//第i门课没有先修课,所以我们可以给其加上一门权值为0的先修课0
else add(k,i);
}
dfs(0,-1);
printf("%d",f[0][m+1]);
return 0;
}