树形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
1→2,并把这条边的序号记为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
1→3(这条边的序号定为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
1→3→2,引入一个
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
1→4→3→2和
5
→
8
→
7
→
6
5\rightarrow8\rightarrow7\rightarrow6
5→8→7→6同时存在,该算法可以保证边的序号不重复)
同时我们发现每一条链的最后一条边的序号
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)
root,dfs(root)即可。
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;
}