二叉苹果树|codevs5565|luoguP2015|树形DP|Elena

时间:2020-12-03 15:10:45

二叉苹果树

题目描述

有一棵苹果树,如果树枝有分叉,一定是分2叉(就是说没有只有1个儿子的结点)

这棵树共有N个结点(叶子点或者树枝分叉点),编号为1-N,树根编号一定是1。

我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有4个树枝的树

2 5 \ / 3 4 \ / 1 现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。

给定需要保留的树枝数量,求出最多能留住多少苹果。

输入输出格式

输入格式:

第1行2个数,N和Q(1<=Q<= N,1<N<=100)。

N表示树的结点数,Q表示要保留的树枝数量。接下来N-1行描述树枝的信息。

每行3个整数,前两个是它连接的结点的编号。第3个数是这根树枝上苹果的数量。

每根树枝上的苹果不超过30000个。

输出格式:

一个数,最多能留住的苹果的数量。

输入输出样例

输入样例#1:
5 2
1 3 1
1 4 10
2 3 20
3 5 20
输出样例#1:
21

要使得一棵树割掉k个节点后整棵树权值和最大。这个题目挺恶心人的处理起来也麻烦。因为虽然这题目保证是二叉树,但是他输入时并没有给我们两个点的先后顺序。也就是他
输入u,v,w,你不知道u是v的父亲还是v是u的父亲。网上有很多人写这个题解时为了方便就乱搞,判断什么如果那个点左儿子为空就成为左儿子否则成为右儿子的什么鬼,一般
都是水得过cv水不过luogu或者反过来的偏解。正确的姿势应该是用边表存储。像我的做法就是拿边表存储之后弄一个Find_father函数找出每一个节点的父亲。关于这个函数
待会我会单独拉出来讲怎么实现。我还多做了一个其实可以不用这么做的无用功:我又跑了一遍枚举找出每个节点的左右儿子。前面麻烦的事干完,接下来我们就可以考虑dp了。
由于题目给的权值都是边权,我们就可以进行一个骚操作:把每条边的权值都下移到那条边通向的儿子节点上,然后给根节点的权值置0。因为我们给了根节点1一个0的权值,所
以我们要保留的节点数就从q变成q+1了!然后我们可以继续进行骚操作,用dp(i,j)表示保留j个节点,于是我们一开始就dp根节点:dp(1,q+1)。那么这个dp的函数该怎么写
呢?
我们设某一个子树要保留j个节点,而根节点是一定要保存的,因为如果你一开始就把根节点剪掉了就啥都没了嘛。所以根节点的以两个儿子为根的子树就总共要保存j-1个节点。
有了思路,我们就可以设左子树保存k个节点,然后我们来枚举这个k,于是右子树就可以推出来是保存j-k-1个节点啦。然后我们就来找状态转移方程和最大值。
首先我们来想,当j=0时,也就是某棵子树上取0个点时。取0个点答案当然是0。然后我们可以按照刚才的思路,给左儿子分配可以保留k个节点,给右儿子分配可以保留j-k-1个
节点,于是我们就枚举k,然后记忆化搜索一波:如果f[某个子节点][k]==0就说明这个节点还没访问过,答案还没有更新,我们就来把这个节点和这个k值dp一遍,作为待会dp
时需要的条件(dp前必须把需要的东西准备好,这就是在准备东西。)然后就可以推出状态转移方程为:
f[i][j]=max(f[i][j],f[son[i][0]][k]+f[son[i][1]][j-k-1]+dis[i]);
f[i][j]=max(f[i][j],儿子节点分配后的最大值加上i节点的权值。
我还是拉几段代码出来详细讲讲:
---------------

void Find_father (int x)//找出每个节点的父亲的函数。
{
  for (int i=head[x]; i; i=edge[i].next) {//枚举x为入点的每一条边
    if (book[edge[i].to]==0) {//如果i边的出点还没被标记过
      father[edge[i].to]=x;
      book[edge[i].to]=1;//标记
      Find_father(edge[i].to);//递归寻找儿子的儿子
    }
  }
}

可能有人要问:为什么需要一个book数组来标记这些点
实际上,还是输入的锅。输入不告诉我们到底谁是父亲,我们就只能从根节点一个一个顺下去找儿子。根节点就是自己的父亲,所以给根节点标记为1.
每个点都连向几个点:一个点是它的父亲,其他的点都是它的儿子。为了避免我们把父亲当做自己的儿子,我们就把父亲标记上,这样子没有标记过的点就是自己的儿子啦。
----------------
for (int i=1; i<=n; i++) {//这是找每个节点的左右儿子的过程
  v=-1;
  for (int j=head[i]; j; j=edge[j].next)//枚举从i点出发的每一条边
    if (father[edge[j].to]==i) {//如果i是edge[j].to点的爸爸的话
      son[i][++v]=edge[j].to;
      dis[edge[j].to]=edge[j].dis;//edge[j].to点的权值变成边j的权值
      if (v==1) break;//v==1时说明i节点的两个儿子都找到了,所以就直接break掉减少枚举量
    }
}
----------------

void DP(int i,int j)//树形DP的函数
{
  if (j==0) f[i][j]=0;//如果要保留0个点,最大值当然是0了。
  else if (son[i][0]==0 && son[i][1]==0) f[i][j]=dis[i];//如果没有儿子节点,最大值就是它本身的价值。
  else {
    for (int k=0; k<j; k++) {//k不能=j,否则j-k-1就会变成负数,是没有意义的,注意边界。
      if (f[son[i][0]][k]==0) DP(son[i][0],k);
      if (f[son[i][1]][j-k-1]==0) DP(son[i][1],j-k-1);
      f[i][j]=max(f[i][j],f[son[i][0]][k]+f[son[i][1]][j-k-1]+dis[i]);//状态转移方程
    }
  }
}



 #include<cstdio>
#include<iostream>
#include<cstring>
#include<cstdlib>
using namespace std;
int read()
{
int f=,x=; char c=getchar();
while (c>''||c<'') {if (c=='-') f=-; c=getchar();}
while (c>='' && c<='') {x=x*+c-''; c=getchar();}
return x*f;
}
int num_edge,head[],n,q,u,v,w,father[],son[][],dis[];
int f[][];
bool book[];
struct Edge
{
int next;
int to;
int dis;
}edge[];
void Add_edge(int from,int to,int dis)
{
edge[++num_edge].next=head[from];
edge[num_edge].to=to;
edge[num_edge].dis=dis;
head[from]=num_edge;
}
void Find_father (int x)
{
for (int i=head[x]; i; i=edge[i].next) {
if (book[edge[i].to]==) {
father[edge[i].to]=x;
book[edge[i].to]=;
Find_father(edge[i].to);
}
}
}
void DP(int i,int j)
{
if (j==) f[i][j]=;
else if (son[i][]== && son[i][]==) f[i][j]=dis[i];
else {
for (int k=; k<j; k++) {
if (f[son[i][]][k]==) DP(son[i][],k);
if (f[son[i][]][j-k-]==) DP(son[i][],j-k-);
f[i][j]=max(f[i][j],f[son[i][]][k]+f[son[i][]][j-k-]+dis[i]);
}
}
}
int main()
{
n=read(); q=read();
for (int i=; i<=n-; i++) {
u=read(); v=read(); w=read();
Add_edge(u,v,w);
Add_edge(v,u,w);
}
book[]=; Find_father();
for (int i=; i<=n; i++) {
v=-;
for (int j=head[i]; j; j=edge[j].next)
if (father[edge[j].to]==i) {
son[i][++v]=edge[j].to;
dis[edge[j].to]=edge[j].dis;
if (v==) break;
}
}
DP(,q+);
printf("%d\n",f[][q+]);
return ;
}

二叉苹果树


nbc姐姐说我的代码有些部分不大好,比如树形DP应该把情况合并在一起处理而不要搞分支。

贴一下nbc姐姐的代码:

 #include <cstdio>
#include <algorithm>
int f[][], N, Q, E, head[], next[], to[], len[], fa[], q[];
void DP(int p)
{
int son1 = , son2 = , len1 = , len2 = ;
for (int e = head[p]; e; e = next[e])
if (to[e] != fa[p])
{
if (son1)
{
son2 = to[e];
len2 = len[e];
}
else
{
son1 = to[e];
len1 = len[e];
}
}
if (!son1 && !son2)
{
for (int i = ; i <= Q; i++)
f[p][i] = ;
return;
}
DP(son1);
DP(son2);
for (int i = ; i <= Q; i++)
f[p][i] = std::max(f[son1][i - ] + len1, f[son2][i - ] + len2);
for (int i = ; i <= Q - ; i++)
for (int j = ; i + j <= Q - ; j++)
f[p][i + j + ] = std::max(f[p][i + j + ], f[son1][i] + len1 + f[son2][j] + len2);
}
int main()
{
scanf("%d%d", &N, &Q);
Q = std::min(Q, N - );
for (int i = , u, v, l; i < N; i++)
{
scanf("%d%d%d", &u, &v, &l);
next[++E] = head[u], to[E] = v, len[E] = l, head[u] = E;
next[++E] = head[v], to[E] = u, len[E] = l, head[v] = E;
}
int H = , T = , u;
q[] = ;
while (H < T)
for (int e = head[u = q[++H]]; e; e = next[e])
if (to[e] != fa[u])
fa[q[++T] = to[e]] = u;
DP();
printf("%d\n", f[][Q]);
return ;
}

NiroBC

f[i][j]表示以i为根的子树最多保存j个节点的最大值。nbc姐姐的写法没有像我一样把边权下移,而是直接处理。

void DP(int p)//树形DP函数的过程
{
int son1 = 0, son2 = 0, len1 = 0, len2 = 0;
for (int e = head[p]; e; e = next[e])//枚举每一条以p为入点的边
if (to[e] != fa[p])//如果e边的出点不是p点的父亲:即to[e]点为p点的儿子
{
if (son1)//如果p点的左儿子不为空:即to[e]点为p点的右儿子
{
son2 = to[e];
len2 = len[e];
}
else//to[e]点为p点的左儿子
{
son1 = to[e];
len1 = len[e];
}
}
if (!son1 && !son2)//p点如果没有左右儿子
{
for (int i = 0; i <= Q; i++)
f[p][i] = 0;//没有左右儿子可以通向,也是没有边,所以保存多少个点的值都为0
return;
}
DP(son1);
DP(son2);
for (int i = 1; i <= Q; i++)
f[p][i] = std::max(f[son1][i - 1] + len1, f[son2][i - 1] + len2);//只保留一棵子树的情况。
for (int i = 0; i <= Q - 2; i++)
for (int j = 0; i + j <= Q - 2; j++)
f[p][i + j + 2] = std::max(f[p][i + j + 2], f[son1][i] + len1 + f[son2][j] + len2);//保留两棵子树的情况
}
以上是nbc姐姐的代码解析。

有问题可以直接在评论里面提问,有需要转载的请得到我的允许,否则按侵权处理。

Elena loves NiroBC forever!