- 题目
- 问题描述
有一棵 n 个节点的树,树上每个节点都有一个正整数权值。如果一个点被选择了,那么在树上和它相邻的点都不能被选择。求选出的点的权值和最大是多少?
- 输入格式
第一行包含一个整数 n 。
接下来的一行包含 n 个正整数,第 i 个正整数代表点 i 的权值。
接下来一共 n-1 行,每行描述树上的一条边。
- 输出格式
输出一个整数,代表选出的点的权值和的最大值。
- 样例输入
5
1 2 3 4 5
1 2
1 3
2 4
2 5
- 样例输出
12
- 样例说明
选择3、4、5号点,权值和为 3+4+5 = 12 。
数据规模与约定
对于20%的数据, n <= 20。
对于50%的数据, n <= 1000。
对于100%的数据, n <= 100000。
权值均为不超过1000的正整数。
- 引言:
这是我第一次接触到的树形DP题,一开始完全没有思路。
后面看了看网上大佬的题解,颇有启发。
DP的关键就在于状态的定义以及找转移
首先要考虑清楚状态,状态要能够很好地并且完整地描述子问题
其次考虑最底层的状态,这些状态一般是最简单的情况或者是边界情况
再就是考虑某一个状态能从哪些子状态转移过来,同时还要考虑转移的顺序,确保子问题已经解决
树形DP很多时候就是通过子节点推父亲节点的状态
- 分析:
于是我们用一个f[i][k]表示到以i为根的子树中的最佳答案。
同时k=1时表示选这个节点,k=0时表示不选这个节点。
那么,如果用u表示父节点,v表示它的一个子节点时
容易得到
f[u][0]+=max(f[v][0],f[v][1]);
f[u][1]+=f[v][0];
也可以得知:
初始化时,f[i][0]=0;f[i][1]=w[i];
最终状态为max(f[1][0],f[1][1])//要么选根节点要么不选
可是怎么一步步推出此状态呢?
这就要我们先深搜到它每一个叶节点在一步步退回来。
同时节点数比较大,这就要求我们用邻接表存树。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
using namespace std;
int head[100100];//表头,head[i]代表起点是i的边的编号
int cnt;//代表边的编号
int f[1000][1000];
struct s
{
int u;//记录边的起点
int v;//记录边的终点
int next;//指向上一条边的编号
}edge[100010];
void add(int u,int v)//向所要连接的表中加入边
{
edge[cnt].u=u;
edge[cnt].v=v;
edge[cnt].next=head[u];
head[u]=cnt++;
}
void dfs(int u,int pre)//pre--父节点 u起点
{
for(int i=head[u];i!=-1;i=edge[i].next)
{
int v=edge[i].v;
if(pre==v) //若父节点与终点重合(即为叶节点)则不需DP
continue;
dfs(v,u); //以起点为父亲继续DP
f[u][1]+=f[v][0];
f[u][0]+=max(f[v][0],f[v][1]);
}
}
int main()
{
int n;
cin>>n;
memset(head,-1,sizeof(head));
memset(f,0,sizeof(f));
for(int i=1;i<=n;i++)
cin>>f[i][1];
for(int i=1;i<n;i++)
{
int a,b;
cin>>a>>b;
add(a,b);
add(b,a);
}
dfs(1,-1);//从第一个节点开始
cout<<max(f[1][1],f[1][0])<<endl;
return 0;
}
相信大家也注意到了。
if(pre==v)continue;//若父节点与终点重合(即为叶节点)则不需DP
如果是叶节点的话就不用递推。
- 总结
这算是最基本的树形DP了,不难理解却也让大家明白了树形DP与以往题目是有很大不同的。