题目链接
https://www.luogu.com.cn/problem/P2014
题意
给你若干先修课-课程的关系,保证课程最多有一个先修课。每个课有自己的学分,最多选m门,问最大得分。
思路
容易发现是个树形DP的题目。
我们考虑dp[x][i]为x结点最多选i门时最大收益,给出的图是一个森林,不妨建立虚拟源点0,答案即为dp[0][m]。
考虑如何转移,树形DP中则为如何统计子树数据得到当前数据。显然,我们是在不同的子树选择一个dp状态相加再加上根节点的值。也就是类似一个分组背包。对于求解每个状态dp[x],我们将自己的背包重量设置为i,我们将每个子树看成一个组,每个组里有若干物品,其价值为dp[son][j],重为j,每个组最多选一个。我们这样就可以利用完全背包问题的转移来进行考虑了。
回想完全背包(说来惭愧,我这也是看树形DP才爬回去学的),我们可以将01背包每个物品看作一组,要么选要么不选。当这个组扩充到若干个物品时,我们也只需要改一下他的决策过程即可。因为最多一组选一个,我们在内层决策时取一下max即可。也就是
枚举组号
枚举容量(倒序)
枚举物品
类 01 转移方程,但是取MAX
回到这里,我们的枚举组号,就是枚举子树,这个在dfs最外层for就实现了。
中层枚举容量,那就是从m到0嘛,对应我们dp第二维的变量(也就是压缩空间后的完全背包第一维)
内层枚举物品,也就是枚举子树的状态咯,其价值为dp[son][j],重为j。
在处理完子树之后,我们还需要处理一下自己的学分。显然我们之前的dp方程第二维是没处理自己的,所以我们还需要转换一下
for(int t=m;t;t--)
dp[x][t]=dp[x][t-1]+va[x];
注意如果是虚拟源点,因为我们不必选修这门“虚拟课”,也就无需再处理这一步了。
代码
#include<cstdio>
#include<iostream>
#include<iomanip>
#include<map>
#include<unordered_map>
#include<string>
#include<queue>
#include<stack>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
#include<chrono>
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define endl "\n"
//#define int long long
//#define double long double
using namespace std;
typedef long long ll;
const int maxn=405;
const int inf=0x3f3f3f3f;
int n,m,k;
int dp[maxn][maxn];
int va[maxn];
vector<int>e[maxn];
void dfs(int x,int fa){
for(auto y:e[x]){
if(y==fa) continue;
dfs(y,x);
for(int i=m;~i;i--){
for(int j=i;j;j--){
dp[x][i]=max(dp[x][i],dp[x][i-j]+dp[y][j]);
}
}
}
if(x)
for(int t=m;t;t--)
dp[x][t]=dp[x][t-1]+va[x];
}
signed main(){
IOS
#ifndef ONLINE_JUDGE
freopen("IO\\in.txt","r",stdin);
freopen("IO\\out.txt","w",stdout);
#endif
cin>>n>>m;
for(int i=1;i<=n;i++){
int u;
cin>>u>>va[i];
e[u].push_back(i);
e[i].push_back(u);
}
dfs(0,-1);
cout<<dp[0][m]<<endl;
}