树形dp
给定一棵N
个节点的树(通常是无根树,也就是有N-1
条无向边),我们可以任选一个节点为根节点,从而定义出每个节点的深度 和 每棵子树的根。
在树上设计动态规划算法时,一般就以节点从深到浅(子树从小到大)的顺序作为DP
的“阶段”。
DP
的状态表示中,第一维通常是节点编号(代表以该节点为根的子树)。
大多数时候,我们采用递归的方式实现树形动态规划。对于每个节点x
,先递归在它的每个子节点上进行DP
,在回溯时,从子节点向节点x
进行状态转移。
例题:AcWing 285. 没有上司的舞会
其实本题是一个从一维扩展而来的题目(大盗阿福)
题意:
有N名职员,编号为1~N。他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数happy给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和他的直接上司一起参会。
在满足这个条件的前提下,选择部分职员参加宴会,使得所有参会职员的快乐指数happy总和最大,
求这个最大值。
思路:
首先,如果仅仅使用f[i]
表示i
这个子树最多能选的分值的话,我们发现状态无法进行转移,因为在分析每一棵子树的时候,我们无从得知当前子树的根节点是否选择,所以为了能正确区分,我们需要对状态进行进一步划分(这是一种简单的状态机模型)
状态表示
我们可以划分两种情况:
f
[
u
]
[
0
]
f[u][0]
f[u][0]:所有以 u
为根的子树中选择,并且不选 u
这个点的方案
f
[
u
]
[
1
]
f[u][1]
f[u][1]:所有以 u
为根的子树中选择,并且选 u
这个点的方案
属性:Max
状态计算
(1)当前 u
结点不选,子结点可选可不选
求解树形 dp
时我们是从根节点开始向下递归求解,当我们递归到 u
这个点是我们先将它的两个儿子的状态(设为s1
、s2
)处理好,即算出f[s1, 0]、f[s1, 1]、f[s2, 0]、f[s2, 1]
之后再来计算f[u, 0]
(所有以 u
为根的子树中选择,并且不选 u
这个点的最大值)。首先每个儿子都是独立的,若使得f[u, 0]
最大,就应该使u
每棵子树达到最大,它们的加和即为答案。
由于 u
没有选择,所以对于它的每棵子树(注意分析的对象是子树),既可以选择其根节点,也可以不选其根节点,比如对于某一棵子树 s
来说,则有:
f
[
u
,
0
]
+
=
m
a
x
(
f
[
s
,
0
]
,
f
[
s
,
1
]
)
f[u, 0] += max(f[s, 0], f[s, 1])
f[u,0]+=max(f[s,0],f[s,1])
推出最终的式子为: f [ u ] [ 0 ] = ∑ m a x ( f [ s i , 0 ] , f [ s i , 1 ] ) ( s i 表 示 u 的 各 棵 子 树 ) f[u][0]=∑max(f[si,0],f[si,1])(si表示u的各棵子树) f[u][0]=∑max(f[si,0],f[si,1])(si表示u的各棵子树)
(2)当前 u
结点选,子结点一定不能选
f
[
u
]
[
1
]
=
∑
(
f
[
s
i
,
0
]
)
(
s
i
表
示
u
的
各
棵
子
树
)
f[u][1]=∑(f[si,0])(si表示u的各棵子树)
f[u][1]=∑(f[si,0])(si表示u的各棵子树)(由于选择了 u
,所以对于它的每棵子树,不选其根节点)
时间复杂度分析:
首先一共有2n
个状态,每个状态计算的时候需要计算的是它所有的儿子,又树中所有节点的儿子数量即为树当中边的数量(n-1
条),所以计算所有状态时总共枚举的次数应当为:O(n-1)
。
最终分析的总时间复杂度为:O(n)
注意:
本题输入的是一棵有根树(指定了节点间的上下属关系),故我们需要先找出没有上司的节点root
作为根,
DP的目标为:max(f[root, 0], f[root, 1])
。
代码:
写法一:用vector
存图
#include<bits/stdc++.h>
using namespace std;
const int N = 6010;
int happy[N];
bool has_father[N];
int n;
int dp[N][N];
vector<int> g[N];
void dfs(int u)
{
dp[u][0] = 0, dp[u][1] = happy[u];
for(auto x : g[u])
{
dfs(x);
//回溯的时候更新答案
dp[u][0] += max(dp[x][0], dp[x][1]);
dp[u][1] += dp[x][0];
}
}
int main()
{
cin>>n;
for(int i=1; i<=n; ++i) cin>>happy[i];
for(int i=0; i<n-1; ++i)
{
int a, b;
cin>>a>>b;
has_father[a] = true, g[b].push_back(a);
}
int root = 1;
while(has_father[root]) root++;
dfs(root);
cout<<max(dp[root][0], dp[root][1])<<endl;
return 0;
}
写法二:用数组模拟邻接表存图
#include<bits/stdc++.h>
using namespace std;
const int N = 6010;
int n;
int happy[N];
bool has_father[N];
int h[N], e[N], ne[N], idx;
int dp[N][N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u)
{
dp[u][0] = 0, dp[u][1] = happy[u];
for(int i=h[u]; ~i; i=ne[i])
{
int j = e[i];
dfs(j);
dp[u][0] += max(dp[j][0], dp[j][1]);
dp[u][1] += dp[j][0];
}
}
int main()
{
cin>>n;
for(int i=1; i<=n; ++i) cin>>happy[i];
memset(h, -1, sizeof h);
for(int i=0; i<n-1; ++i)
{
int a, b;
cin>>a>>b;
add(b, a), has_father[a] = true;
}
int root = 1;
while(has_father[root]) root++;
dfs(root);
cout<<max(dp[root][0], dp[root][1])<<endl;
return 0;
}