DP-树形DP-luogu2015-二叉苹果树

题目大意

一棵二叉苹果树,上结了苹果需要砍去一些边,求保留q条边时最多能保留多少苹果。(1<=n,q<=100)

2   5
 \ / 
  3   4
   \ /
    1

问题分析

  1. 题目保证了原始苹果树有分叉则必为二叉,且1为root
  2. 注意输入时父子顺序是不确定的,所以需要做处理
  3. 定义 d p i , j dp_{i,j} dpi,j表示编号为i的子树在保留j条边的情况下获得的最大苹果数目,则状态转移为
    d p i , j = m a x { d p l c , k + d p r c , j − k − 2 + a p p l e l c + a p p l e r c d p l c , j − 1 + a p p l e l c d p r c , j − 1 + a p p l e r c dp_{i,j} = max \begin{cases} dp_{lc,k} + dp_{rc,j-k-2} + apple_{lc} + apple_{rc} \\ dp_{lc, j-1} + apple_{lc} \\ dp_{rc,j-1} + apple_{rc} \\ \end{cases} dpi,j=maxdplc,k+dprc,jk2+applelc+applercdplc,j1+applelcdprc,j1+applerc
  4. 对应上述转移方程的情况分别为:
    1. 保留边数j至少为2前提下,保留i的左右两条子边,然后从左右子树中总共挑选j-2条边
    2. 左子树总边数大于等于保留边数j前提下,可以砍掉右子树,只在左子树中保留边,显然为了保留左子树的边,就得先保留i的左子边,所以左子树还要保留j-1条边
    3. 情况与2类似
  5. 注意在任何时候,都要保证cnt[i]>=j,即最多保留树i的总边数。但换种想法来看, d p i , j dp_{i,j} dpi,j表示树i最多保留j条边时的最大苹果总数,此时不存在问题(与另一种做法不同)
  6. 关于遍历顺序的问题,一般采用DFS回溯时做DP;为防止爆栈,也可以使用BFS,然后逆序遍历
  7. 一开始,我采用了另外一种DP思路,定义 d p i , j dp_{i, j} dpi,j为树i砍去j条边的最大苹果总数。简单地看,这样也没什么不妥,并且状态转移与保留DP差异不大,但是这样DP存在以下神坑
    1. 必须保证砍去的边j总是小于树i的总边数,而不能出现虚拟砍伐。比如,树i(5边)总共砍5刀,左树lc(4边),右树rc(1边),如果给分配成右树砍4刀,左数砍1刀,则在事实上只是砍了2刀。这样的错误主要来自于错误的DP状态定义,在这之前,我的DP状态定义是树i最多砍j刀的最大苹果数目。显然,不砍时最优,故最多砍j刀的最大数相当于砍0刀的最大数。
    2. 当把树i的左边砍掉时,也就意味着整个左子树都被一定砍掉了,此时右子树需砍伐j - (cnt[lc] + 1) - 1条边。这里还有一个误区在于,虽然主观上只砍去1条边,给人感觉右子树还要砍j - 1 ;或者是砍去1条边,左子树上的cnt条边还可以选择性地砍伐k条,造成右半部分可以对应地砍伐j - (1 + k)条边。但这两种想法都是错的,是把砍边当成了机会而非事实,正确的想法是 砍去1条左子边,就造成了整个左半部分被去除的事实,即树失去了cnt[lc] + 1条边。因为我们最终要的结果是保留多少条边,所以砍去1条主边和砍去左半部分所有边造成的结果都是一样的。
  8. 基于以上神坑,所以极不推荐那种做法!

AC代码

#include <cstring>
#include <cstdio>
#include <algorithm>
#include <vector>

using namespace std;
const int maxn = 110;
const bool DBG = false;

int n,q;
vector<int> son[maxn];
int father[maxn];
int apple[maxn];

int que[maxn], front, back;
int dp[maxn][maxn];
int cnt[maxn];

int main()
{
    //建树
    scanf("%d%d", &n, &q);
    for(int i=0;i<(n-1);i++)
    {
        int u, v, w;
        scanf("%d%d%d", &u, &v, &w);
        if(v == 1 || father[v])swap(u, v);//保证u是父节点
        apple[v] = w;
        father[v] = u;
        son[u].push_back(v);
    }

    //BFS排序
    que[back++] = 1;
    while(back!= n)
    {
        int cur = que[front++];
        for(int s:son[cur])
            que[back++] = s;
    }
    
    //统计每棵子树的枝干总数(边数)
    for(int i=n-1;i>=0;i--)
    {
        int cur = que[i];
        if(son[cur].size())
            cnt[cur] = cnt[son[cur][0]] + cnt[son[cur][1]] + 2;
    }
    
    //从叶子按顺序DP
    while(back)
    {
        int i = que[--back];
        for(int j=0;j<=cnt[i];j++)
        {
            if(!son[i].size())
                dp[i][j] = 0;
            else
            {
                int lc = son[i][0], rc = son[i][1];
                int temp = 0;
                if(cnt[i] >= j && j >= 2)
                {
                    for(int k=0;k<=(j-2);k++)
                        if(cnt[lc] >= k && cnt[rc] >=(j-k-2))
                            temp = max(temp, dp[lc][k] + dp[rc][j-k-2] + apple[lc] + apple[rc]); 
                }
                
                if(cnt[lc] >= (j - 1) && j >= 1)
                    temp = max(temp, dp[lc][j-1] + apple[lc]);
                
                if(cnt[rc] >= (j-1) && j >= 1)
                    temp = max(temp, dp[rc][j-1] +apple[rc]);
                
                if(DBG)printf("\n");
                dp[i][j] = temp;
            }
        }
    }
    
    //输出结果
    printf("%d\n", dp[1][q]);
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值