Luogu-P2015 二叉苹果树

题目

题目链接

测试得分:  100

主要算法 :  树型DP、零一背包

题干:

   树上零一DP

应试策略:

  1. 记忆化搜索
  2. 如果是空结点
  3. 如果是叶子结点直接返回苹果数
  4. 以上都不满足的话,DP状态转移
  5. 设置DP的转移,转移到左右儿子结点
  6. for(int i=1;i<cnt-1;i++) f[u][cnt]=max(f[u][cnt],Dp(edge[p[u].lc].to,i)+Dp(edge[p[u].rc].to,cnt-i-1)+edge[p[u].lc].dis+edge[p[u].rc].dis);
  7. 结果31分  

   错因分析:

    1.说白了,记忆化搜索还不是掌握的特别好

    2.其次状态设计不是特别好,在存储左右儿子时,应该该需要存储边的

   代码

#include<stdio.h>
#include<stdlib.h>
#define LL long long 
#define FORa(i,s,e) for(LL i=s;i<=e;i++)
#define FORs(i,s,e) for(LL i=s;i>=e;i--)

using namespace std;
const LL N=1000;
LL n,m,num_edge,sz[N+1],head[N+1],f[N+1][N+1];
/*f[u][j]表示的是树根为u的子树中保留j条树枝的最多的苹果数 
sz[u]表示的是树根为u的子树的大小 
*/
struct Edge{
    LL next,to,dis;
}edge[2*N+2];
struct Mess{
    LL lc,rc;
}p[N+1];
inline void Add_edge(LL from,LL to,LL dis)
{
    edge[++num_edge]=(Edge){head[from],to,dis},head[from]=num_edge;
}
inline LL max(LL fa,LL fb){return fa>fb?fa:fb;}
inline LL min(LL fa,LL fb){return fa<fb?fa:fb;}

void Dfs(LL u,LL fa)
{
    sz[u]=1;
    for(LL i=head[u];i;i=edge[i].next)
    {
        LL v=edge[i].to;
        if(v!=fa)
        {
            Dfs(v,u);
            sz[u]+=sz[v];
            FORs(j,min(sz[u],m),0)//零一背包,对于每一种选择只能选一次 
                FORa(k,0,min(sz[v],j-1))
                    f[u][j]=max(f[u][j],f[v][k]+f[u][j-k-1]+edge[i].dis);
            /*对于状态转移,以u为树根,保留j条树枝的最多的苹果数
            等于以u的儿子节点为树根,保留j-k-1条树枝的最多的苹果数加上以v为树根,保留k(k<=min(sz[v],j-1))条树枝的最多的苹果数*/
        }
    }
}
int main()
{
    LL from,to,dis;
    scanf("%lld%lld",&n,&m);
    FORa(i,2,n)
    {
        scanf("%lld%lld%lld",&from,&to,&dis);
        Add_edge(from,to,dis),Add_edge(to,from,dis);
    }
    Dfs(1,0);
    printf("%lld",f[1][m]);
    return 0;
}
/*5 2
1 3 1
1 4 10
2 3 20
3 5 20*/

优化策略:

  记忆化搜索,树型背包DP

  这道题有一个隐含的条件,当某条边被保留下来时,从根节点到这条边的路径上的所有边也都必须保留下来

  设f[u][i]表示uu的子树上保留ii条边,至多保留的苹果数目

  那么状态转移方程也就显而易见了

    FORs(j,min(sz[u],m),0)//零一背包,对于每一种选择只能选一次
      FORa(k,0,min(sz[v],j-1))
      f[u][j]=max(f[u][j],f[v][k]+f[u][j-k-1]+edge[i].dis);

  u表示当前节点,v是u的一个子节点,sz[u]表示uu的子树上的边数,m就是题目中要求的最多保留边数

  那么为什么是

  首先,为什么是f[u][i-j-1]而不是f[u][i-j]

  为前文提到了,保留一条边必须保留从根节点到这条边路径上的所有边,那么如果你想从u的子节点v的子树上留边的话,也要留下u,v之间的连边

  那么取值范围k为什么要小于等于i1而不是ii呢?

  同上,因为要保留u,v连边

  对了,别忘了i,j要倒序枚举因为这是01背包

  代码

#include<stdio.h>
#include<stdlib.h>
#define LL long long 
#define FORa(i,s,e) for(LL i=s;i<=e;i++)
#define FORs(i,s,e) for(LL i=s;i>=e;i--)

using namespace std;
const LL N=1000;
LL n,m,num_edge,sz[N+1],head[N+1],f[N+1][N+1];
/*f[u][j]表示的是树根为u的子树中保留j条树枝的最多的苹果数 
sz[u]表示的是树根为u的子树的大小 
*/
struct Edge{
    LL next,to,dis;
}edge[2*N+2];
struct Mess{
    LL lc,rc;
}p[N+1];
inline void Add_edge(LL from,LL to,LL dis)
{
    edge[++num_edge]=(Edge){head[from],to,dis},head[from]=num_edge;
}
inline LL max(LL fa,LL fb){return fa>fb?fa:fb;}
inline LL min(LL fa,LL fb){return fa<fb?fa:fb;}

void Dfs(LL u,LL fa)
{
    sz[u]=1;
    for(LL i=head[u];i;i=edge[i].next)
    {
        LL v=edge[i].to;
        if(v!=fa)
        {
            Dfs(v,u);
            sz[u]+=sz[v];
            FORs(j,min(sz[u],m),0)//零一背包,对于每一种选择只能选一次 
                FORa(k,0,min(sz[v],j-1))
                    f[u][j]=max(f[u][j],f[v][k]+f[u][j-k-1]+edge[i].dis);
            /*对于状态转移,以u为树根,保留j条树枝的最多的苹果数
            等于以u的儿子节点为树根,保留j-k-1条树枝的最多的苹果数加上以v为树根,保留k(k<=min(sz[v],j-1))条树枝的最多的苹果数*/
        }
    }
}
int main()
{
    LL from,to,dis;
    scanf("%lld%lld",&n,&m);
    FORa(i,2,n)
    {
        scanf("%lld%lld%lld",&from,&to,&dis);
        Add_edge(from,to,dis),Add_edge(to,from,dis);
    }
    Dfs(1,0);
    printf("%lld",f[1][m]);
    return 0;
}
/*5 2
1 3 1
1 4 10
2 3 20
3 5 20*/

总结:

  1.DP状态设计自己一定要铭记

  2.DP初始化与边界处理

原文地址:https://www.cnblogs.com/SeanOcean/p/11312952.html