本题难度不亚于提高组,被评为普及应该是误评了。要对树型递归结构,链式前向星,和动态规划以及背包压维有着比较深的了解。如果以上知识不是太了解,可以先各刷五六道入门
树上分组背包常用格式就是dp[i][j]代表了i节点为根节点,容量为j时的最大价值。虽然是二维,但它是被三维压缩过来的。
我们设dp[i][u][j]表示以u为根的子树,仅用前i个儿子,满足j体积时取得最大价值,
那么dp[i][u][j]=max(dp[i-1][u][j-k]+dp[full_son_size[v]][v][k]); 也就是说状态转移时,是u节点前i-1个儿子使用j-k个体积,加上被遍历的儿子v使用了全部孙子full_son_size[v],体积为k时的价值。
有点绕,需要注意的是必须儿子把孙子全部遍历之后父亲节点才能遍历儿子。
那么如何压维成二维呢。
树形背包一直有个疑惑,为什么在枚举背包容量的时候要反向枚举,浅层次的理解就是套用01背包枚举体积;但是细想又不对,因为01背包只有在使用滚动数组的时候才必须逆推;如果使用2维的话不需要;
那是不是意味着树形背包中原本应该是3维,滚动优化为2维了呢???
仔细回味01背包的状态定义:f[i][j]表示在前i个物品占用体积为j的空间能够得到的最大价值;这里有一个限定前i个,初次学习树形背包一看人家的代码感觉就是这么写的,并没有深入去思考为什么。
所以仿照01背包定义dp[i][u][j]表示在以u为根的子树的前i棵子树中选择j个节点能够获得的最大费用
dp[i][u][j]=max(dp[i-1][u][j-k]+dp[v的总儿子数][v][k]-w(u,v))
以上就是树形dp没有滚动优化的状态方程,为什么需要滚动优化?为什么可以滚动优化?
为什么需要滚动优化?————不滚动很可能会MLE
为什么可以滚动优化???
1.对于u而言很显然是可以滚动优化的,只不过j-k<j要保证dp[u][j-k]是第i-1轮的值则j必须要逆推!!!
2.关键在于v,滚动优化后怎么保证每次dp[v][k]==dp[v的总儿子数][v][k]? 其实关键还是在于01背包的本质的理解,dp[i][v][j]从v的前i个子树中选择j个节点,
递推求解的时候是i依次增大的,所以结束时一定有"i==v的总儿子数" 因此使用滚动数组计算v结束后,dp[v][k]中一定是保存的从v的所有儿子中选k个节点的值;
从而保证dp[v][k]==dp[v的总儿子数][v][k]
上代码,逐一讲解
# include<iostream>
# include<algorithm>
# include<iomanip>
# include<vector>
# include<map>
# include<math.h>
# include<cstring>
using namespace std;
typedef long long int ll;
int val[3010];
int dp[3010][3010];
int len;
typedef struct
{
int b,e,w;
}xinxi;
xinxi s[100000+10];
int f[100000+10];
int nex[100000+10];
void add(int x,int y,int z)
{
s[len].b=x;
s[len].e=y;
s[len].w=z;
nex[len]=f[x];
f[x]=len;
len++;
}
int n,m;
int dfs(int now,int father)
{
if(now>=n-m+1)
{
dp[now][1]=val[now];
dp[now][0]=0;
return 1;
}
int x=f[now];
int sum=0;
dp[now][0]=0;
while(x!=-1)
{
int j=s[x].e;
if(j==father)
{
x=nex[x];
continue;
}
int t=dfs(j,now);
sum+=t;
for(int i=sum;i>=1;i--)
{
for(int k=1;k<=t;k++)
{
if(i>=k)
dp[now][i]=max(dp[now][i],dp[now][i-k]+dp[j][k]-s[x].w);
}
}
x=nex[x];
}
return sum;
}
int main()
{
memset(f,-1,sizeof(f));
fill(dp[0],dp[0]+3010*3010,-99999999);
cin>>n>>m;
for(int i=1;i<=n-m;i++)
{
int t;
cin>>t;
while(t--)
{
int x,y;
cin>>x>>y;
add(i,x,y);
add(x,i,y);
}
}
for(int i=n-m+1;i<=n;i++)
cin>>val[i];
dfs(1,0);
for(int i=m;i>=0;i--)
{
if(dp[1][i]>=0)
{
cout<<i;
return 0;
}
}
return 0;
}
树上分组背包套路就是先算儿子,再用儿子状态转移父亲。
有两个细节,一个是递归边界, 也就是叶子节点,在本题是“用户”
if(now>=n-m+1)
{
dp[now][1]=val[now];
dp[now][0]=0;
return 1;
}
遍历到叶子时,直接返回叶子所包含的体积,在这里是1,并且对DP数组做一些变化,方便状态转移父亲
另一个细节,就是倒序遍历体积,不断扩展体积
int t=dfs(j,now);
sum+=t;
for(int i=sum;i>=1;i--)
{
for(int k=1;k<=t;k++)
{
if(i>=k)
dp[now][i]=max(dp[now][i],dp[now][i-k]+dp[j][k]-s[x].w);
}
}
x=nex[x];
和01背包一样,之所以倒序遍历体积,是为了让当前节点完全由刚才遍历到的儿子转移状态,否则正序遍历,大些的体积会受到小些体积的影响。即小体积完成了儿子的装入后,大体积又在这个已经完成状态转移的小体积 基础上再转移一次,显然违背了物体只有一个的性质。