树形dp,就是根据题意需要建一颗树并且在上面使用dp。树形dp一般采用递归的形式,因为树形dp一般需要用深度深的节点的状态来更新深度浅的的节点的状态。
基础树形dp
那么接下来就结合P1352 没有上司的舞会来讲解一下基础树形dp。
这个题目的意思还是比较清晰的,无疑采用树形dp求解。首先建立一颗树,将上司的节点连一条有向边到下属的节点,因为树形dp是从上而下的。点权另外存入一个数组中。
然后我们设 f ( u , 1 ) f(u,1) f(u,1)表示 u 职员来的情况下,在以ta为根的子树中最大快乐指数; f ( u , 0 ) f(u,0) f(u,0)表示 u 职员不来的情况下,在以ta为跟的子树中最大快乐指数。
根据题意:如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了
, 我们可以很容易地得出状态转移方程:
f
(
u
,
1
)
f(u,1)
f(u,1) = ∑
f
(
v
,
0
)
f(v,0)
f(v,0),
f
(
u
,
0
f(u,0
f(u,0 = ∑max{
f
(
v
,
0
)
f(v,0)
f(v,0),
f
(
v
,
1
)
f(v,1)
f(v,1)}(
v
v
v表示
u
u
u的子节点)。
解释一下,当 u 要来的时候,v 一定来不了,因为题意说了这个职员来了那么ta的直接下属不会来;那么当 u 不来的时候, 就可以取 v 来或者不来的最大值。
要注意这题需要找根节点,从根节点开始向下递归。
code:
#include <bits/stdc++.h>
using namespace std;
const int N = 7e3+10;
int e[N<<1],nex[N<<1],h[N],idx;
int f[N][2],n,a[N];
bool isf[N] , vis[N];
void add(int a,int b){
e[++idx] = b;
nex[idx] = h[a];
h[a] = idx;
}
void tree_dp(int u){
for(int i=h[u];i;i=nex[i]){
int v = e[i];
tree_dp(v);
f[u][0] += max(f[v][0] , f[v][1]);
f[u][1] += f[v][0];
}
}
int main(){
cin >> n;
for(int i=1;i<=n;i++) cin >> f[i][1];// 直接存入当某个职员来的时候的初始值
for(int i=1;i<n;i++){
int a,b;
cin >> a >> b;
isf[a] = 1;// a 不可能是根节点
add(b , a);
}
int root;
for(int i=1;i<=n;i++){
if(!isf[i]) {//找根
root = i;
break;
}
}
tree_dp(root);
cout << max(f[root][1] , f[root][0]);// 最后要取校长来或者不来的最大值
return 0;
}
相关练习:
(1)P1122 最大子树和
这个题目还是比较水的。
首先我们设 f ( u ) f(u) f(u)表示以 u 为根的子树中经过“修剪”后的美丽指数最大值。
根据题意我们很容易得出状态转移方程: f [ u ] = ∑ f [ v ] ( f [ v ] ≥ 0 ) f[u] = ∑f[v](f[v]\ge0) f[u]=∑f[v](f[v]≥0)。
ps:此处为了区分与小括号使用中括号
因为题意中有负数,所以很明显我们只需要使得算上这颗子树后和更大即可取这颗子树,否则直接减去不要。
(2)P2016 战略游戏
这个题目也算比较水。
我们设 f ( u , 1 ) f(u,1) f(u,1)表示在以 u u u点为根的子树上,在 u u u点上放置一个士兵时所放置士兵的最小数, f ( u , 0 ) f(u,0) f(u,0)表示在以 u u u点为根的子树上,在 u u u点上不放置士兵时所放置士兵的最小数。
可能有点绕但是仔细阅读理解一下
根据题目意思我们可以很容易地得出状态转移方程: f ( u , 1 ) = ∑ m i n f ( v , 1 ) , f ( v , 0 ) f(u,1) = ∑min{f(v,1),f(v,0)} f(u,1)=∑minf(v,1),f(v,0), f ( u , 0 ) = ∑ f ( v , 1 ) f(u,0) = ∑f(v,1) f(u,0)=∑f(v,1)。
注意 f ( u , 1 ) f(u,1) f(u,1)要初始化为 1 1 1 ,不然全是 0 0 0。
至于原因就是当 u u u节点有士兵的时候和它相邻的边可以有士兵也可以没有士兵,但是如果 u u u节点上没有士兵,那么它的儿子节点 v v v必须有士兵。
这个题目比前面两题难亿点(
刚看完题目,你可能会觉得这就是个有权的战略游戏,但是码完之后发现假了。其实原因就在于战略游戏那题的士兵看的时边
,而这题的保安看的是点
。
这就导致了很大的区别,如果在战略游戏中我们最多只能做到放 不放 放
而在这题中可以放 不放 不放 放
所以我们的设的状态会复杂一点: f ( u , 0 ) f(u,0) f(u,0)表示为点 u u u被父节点看守, f ( u , 1 ) f(u,1) f(u,1)表示为点 u u u被子节点看守, f ( u , 2 ) f(u,2) f(u,2)表示为点 u u u被自己看守。
那么我们很容易得出状态转移方程:
f
(
u
,
0
)
=
∑
m
i
n
f
(
v
,
1
)
,
f
(
v
,
2
)
f(u,0)=∑min{f(v,1),f(v,2)}
f(u,0)=∑minf(v,1),f(v,2)
当
u
u
u被父亲节点看守的时候,它的儿子
v
v
v只要不被
u
u
u看守都没有问题。
f ( u , 1 ) = ∑ m i n f ( v , 1 ) , f ( v , 2 ) f(u,1)=∑min{f(v,1),f(v,2)} f(u,1)=∑minf(v,1),f(v,2) 但是保证有一个 v v v必须是自己看守自己
可以抽象为: f ( u , 1 ) = ∑ m i n f ( v , 1 ) , f ( v , 2 ) − m i n f ( v , 1 ) , f ( v , 2 ) + f ( v , 2 ) f(u,1)=∑min{f(v,1),f(v,2)}−min{f(v,1),f(v,2)}+f(v,2) f(u,1)=∑minf(v,1),f(v,2)−minf(v,1),f(v,2)+f(v,2)
可能会有一点难理解,其实就是因为这个节点要被其中一个 v v v看守,所以必须保证取其中一个 v v v的状态为 f ( v , 2 ) f(v,2) f(v,2)即这个 v v v上有保安。之所以这么弄是重新遍历一次,保证一定有一次是取上了 f ( v , 2 ) f(v,2) f(v,2)。
f ( u , 2 ) = ∑ m i n f ( v , 0 ) , f ( v , 1 ) , f ( v , 2 ) + v a l [ u ] f(u,2)=∑min{f(v,0),f(v,1),f(v,2)}+val[u] f(u,2)=∑minf(v,0),f(v,1),f(v,2)+val[u]
当 u u u被自己看守的时候,它的儿子 v v v可以是任意状态。但是注意最后要加上这个点的费用 v a l val val。
dp函数部分代码:
void dp(int u){
f[u][2] += w[u];
int sum = 0;
for(int i = h[u];i ;i = nex[i]){
int v = e[i];
dp(v);
f[u][2] += min(f[v][2] , min(f[v][1] , f[v][0]));
sum += min(f[v][1] , f[v][2]) ;
}
f[u][0] += sum;
f[u][1] = INF;
for(int i = h[u];i ;i = nex[i]){
int v = e[i];
f[u][1] = min(f[u][1] , sum - min(f[v][1] , f[v][2]) + f[v][2]);
}
}
解释一下第二个循环部分:第一个循环中我们求出了 ∑ m i n f ( v , 1 ) , f ( v , 2 ) ∑min{f(v,1),f(v,2)} ∑minf(v,1),f(v,2)存入 s u m sum sum,那么在第二个循环中直接遍历所有 v v v,然后找出当 v v v一定放置保安时和其他 v v v的状态和,这样的最小值就是 f ( u , 1 ) f(u,1) f(u,1)的结果。