Luogu-P2015 二叉苹果树
题目
测试得分: 100
主要算法 : 树型DP、零一背包
题干:
树上零一DP
应试策略:
- 记忆化搜索
- 如果是空结点
- 如果是叶子结点直接返回苹果数
- 以上都不满足的话,DP状态转移
- 设置DP的转移,转移到左右儿子结点
- 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);
- 结果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为什么要小于等于i−1而不是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初始化与边界处理