树形DP(1)

树形DP也即在树上做DP,而遍历一颗树往往需要DFS(深度优先搜索)算法,所以树形DP往往是DP和DFS的结合。
先引入一道例题 没有上司的舞会
我们把题目分为3个模块:构造树,遍历树,DP

1 构造树

这里我们引入一个图论中的算法,链式前向星算法,这个算法的作用是用父结点检索它的所有子结点。
例:假设结点的序数就是权值,父结点 1 1 1具有 234 2 3 4 234三个子节点,那么我们先确立以 1 1 1为起点, 2 2 2为终点的一条有向边 1 → 2 1\rightarrow2 12,并把这条边的序号记为1,我们记 f i r s t [ 1 ] first[1] first[1]是以1为父结点的第一条边的序号,所以 f i r s t [ 1 ] = 1 first[1]=1 first[1]=1,记 t o [ i ] to[i] to[i]是序号为 i i i的边的中点,显然 t o [ 1 ] = 2 to[1]=2 to[1]=2,当我们需要加上 1 → 3 1\rightarrow3 13(这条边的序号定为2)的时候我们只需要令 f i r s t [ 1 ] = 2 first[1]=2 first[1]=2 t o [ 2 ] = 3 to[2]=3 to[2]=3即可。
那么序号为 1 , 2 1,2 1,2的两条边该如何联系?我们仿造链表使得 1 → 3 → 2 1\rightarrow3\rightarrow2 132,引入一个 n e x t [ i ] next[i] next[i]表示序号为 i i i的下一条边的序号。那么 n e x t [ 2 ] = 1 next[2]=1 next[2]=1
由此可见,我们只需每次使得新加上边的序号 i i i, n e x t [ i ] next[i] next[i]等于原来的 f i r s t [ 1 ] first[1] first[1],更新 f [ 1 ] f[1] f[1]的值为新加上边的序号 i i i,最后加上子节点的权值 t o [ i ] to[i] to[i]即可。
代码:
void add(int a,int b){
next[++cnt]=first[a];
first[a]=cnt;
to[cnt]=b;
}
注意 c n t cnt cnt f i r s t first first数组内的所有元素初始值为0,可以自己举实例模拟一下代码(多个父结点的实例,比如 1 → 4 → 3 → 2 1\rightarrow4\rightarrow3\rightarrow2 1432 5 → 8 → 7 → 6 5\rightarrow8\rightarrow7\rightarrow6 5876同时存在,该算法可以保证边的序号不重复)
同时我们发现每一条链的最后一条边的序号 j j j一定满足 n e x t [ j ] = 0 next[j]=0 next[j]=0,因为首先根据该算法的特性最后一条边一定是第一个加入的,我们将 f i r s t first first数组清零后,插入第一条边时, f i r s h [ a ] firsh[a] firsh[a]此时的值为 0 0 0并将其赋给了 n e x t [ + + c n t ] next[++cnt] next[++cnt],如此一来,我们遍历完某一个父结点的终止条件便是 n e x t [ j ] = 0 next[j]=0 next[j]=0

2 遍历

有了上述讨论,我们可以很清晰的遍历一颗我们用链式前向星算法构造的一颗树
对每一个结点作为父结点(权值为 a a a)讨论的循环为
for(int i=first[a];i>0;i=next[i]) i = 0 i=0 i=0便是我们如上讨论的终止条件,该循环遍历以 a a a为父结点的所有子结点)
嵌套进DFS递归则有
void dfs(int a){
for(int i=first[a];i>0;i=next[i])
dfs[to[i]];
}

3 DP

现在正式讨论问题本身,由于子结点和父结点不能同时出现,我们用 d p [ n ] [ 1 ] dp[n][1] dp[n][1]表示取了编号为 n n n的这个点所能达到的快乐值总和的最大值, d p [ n ] [ 0 ] dp[n][0] dp[n][0]表示不取编号为 n n n的点, 那么 d p [ n ] [ 0 ] = ∑ m a x ( d p [ x ] [ 0 ] , d p [ x ] [ 1 ] ) dp[n][0]=\sum max(dp[x][0],dp[x][1]) dp[n][0]=max(dp[x][0],dp[x][1]) x x x的取值是 n n n的所有子节点,因为父结点不取,所以子节点可以取,也可以不取,选取这两种情况中最大的一种,并对每个子节点进行如此讨论,最后求和。另一个状态转移方程为 d p [ n ] [ 1 ] = ∑ d p [ x ] [ 0 ] dp[n][1]=\sum dp[x][0] dp[n][1]=dp[x][0],我们将DP和DFS结合
代码:
void dfs(int a){
for(int i=first[a];i>0;i=next[i]){
dfs[to[i]];
dp[a][1]+=dp[to[i]][0];
dp[a][0]+=max(dp[to[i]][1],dp[to[i]][0])
}
dp[a][1]+=r[a]//如果选取这个点那么加上这个点的权值
}
至于DP的起点是什么,我们可以创建一个 b o o l bool bool类型的 r o o t root root数组,每输入一对 a a a b b b的父结点时,便使得 r o o t [ b ] = f a l s e root[b]=false root[b]=false,有父结点的一定不是根结点。最后判断谁是 r o o t , d f s ( r o o t ) root,dfs(root) rootdfsroot即可。

4 例题解答

代码如下:

#include<bits/stdc++.h>
using namespace std;
int first[6005];//first[u]表示已u为起点第一条边的编号
int next[6005];//next[cnt]表示编号为cnt的下一条边的编号
int to[6005];//to[cnt]表示编号为cnt的边的终点
int cnt=0;
int r[6005];//快乐指数
int a[6005],b[6005];//a[i]是b[i]的直接上司
bool root[6005];//用来找根结点,给dfs一个起点
int dp[6005][2];
void add(int a,int b){//链式前向星算法用于构建树
next[++cnt]=first[a];
first[a]=cnt;
to[cnt]=b;
}
void dfs(int n){//遍历以n为根结点的树
for(int i=first[n];i>0;i=next[i]){
dfs(to[i]);
dp[n][0]+=max(dp[to[i]][1],dp[to[i]][0]);
dp[n][1]+=dp[to[i]][0];
}
dp[n][1]+=r[n];
}
int main(){
memset(dp,0,sizeof(dp));
memset(root,true,sizeof(root));//初始化
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>r[i];
}
for(int i=1;i<=n-1;i++){
cin>>b[i]>>a[i];
add(a[i],b[i]);//加一条以a[i]为起点,b[i]为终点的的边
root[b[i]]=false;//b不是根节点
}
for(int i=1;i<=n;i++){
if(root[i]){
dfs(i);
cout<<max(dp[i][0],dp[i][1]);
break;
}
}
return 0;
}

  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值