P1273 有线电视网-树上DP之树上分组背包

原题链接有线电视网 - 洛谷

本题难度不亚于提高组,被评为普及应该是误评了。要对树型递归结构,链式前向星,和动态规划以及背包压维有着比较深的了解。如果以上知识不是太了解,可以先各刷五六道入门

树上分组背包常用格式就是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背包一样,之所以倒序遍历体积,是为了让当前节点完全由刚才遍历到的儿子转移状态,否则正序遍历,大些的体积会受到小些体积的影响。即小体积完成了儿子的装入后,大体积又在这个已经完成状态转移的小体积 基础上再转移一次,显然违背了物体只有一个的性质。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qinsanma and Code

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值