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