树形dp是一类在树上作状态转移的问题。一般情况下,如果题目求的是最大值/最小值/数量等可以用dp解决的问题且明确给出一棵树,都可以考虑尝试树形dp解法。
核心思想:
1. 利用子节点信息更新当前节点。一般先进行dfs,此时所有子节点信息都已更新完毕,之后利用子节点状态dp[son]更新状态dp[now]。
2. 利用当前节点信息更新子节点。一般先用dp[now]更新dp[son],在更新前son的父节点信息都已更新完毕,之后dfs下去遍历整棵树。
可以发现,两种思想都是从根开始向叶子节点遍历,只是更新状态的时刻不同罢了。第一种思想是先dfs递归到底部,之后在向上回溯的过程中更新当前节点,而第二种思想是先更新子节点,然后dfs向下遍历,当递归到底部时这一支就已经更新完毕,这时的dfs更像是一种向叶子遍历的工具。
相关例题(题目顺序由易到难):
1. Anniversary party POJ2342
题目大意,给出一棵关系树,每个人都不想和自己的上级一起聚会,且每人有一个快乐值,求最大快乐值。这题是个树形dp模板题,首先考虑状态设计,令dp[i][0]表示在以i为根的子树中且i不去的情况下最大快乐值,dp[i][1]表示在以i为根的子树中且i去的情况下最大快乐值。设计好状态后,转移方程也就呼之欲出了,考虑选当前节点,其子节点必然都不可选,即dp[now][1]=sum(dp[son][0]),若不选当前节点,其子节点可选可不选,我们必然取最优的情况,即dp[now][0]=sum(max(dp[son][1],dp[son][0]))。最终答案即为max(dp[root][0],dp[root][1])。
代码如下:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
//不可以贪心地去想,每层选取状态10101或01010,因为有可能10010获得的快乐值更大
int n, cnt, head[6005], hpy[6005], in[6005];//in数组用来找到根节点
int dp[6005][2];//dp[i][1]表示i来时,他和他的下属们能获得的最大快乐值,dp[i][0]表示i不来时...
struct edges
{
int to, next;
}edge[200005];
void init()
{
cnt = 0;
for(int i = 1; i <= n; i++)
head[i] = in[i] = 0;
memset(dp, 0, sizeof dp);
}
void add(int u, int v)
{
edge[++cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt;
}
void dfs(int now)//有向树,不可能往回遍历
{
dp[now][0] = 0, dp[now][1] = hpy[now];
for(int i = head[now]; i; i = edge[i].next)
{
dfs(edge[i].to);
dp[now][1] += dp[edge[i].to][0];
dp[now][0] += max(dp[edge[i].to][0], dp[edge[i].to][1]);//此时下属们来不来都可以,谁的贡献大谁来
}
}
signed main()
{
while(cin >> n)
{
if(n == 0)
break;
init();
for(int i = 1; i <= n; i++)
scanf("%d", &hpy[i]);
int a, b;
for(int i = 1; i <= n-1; i++)
{
scanf("%d%d", &a, &b);
add(b, a);
in[a]++;
}
int s;
for(int i = 1; i <= n; i++)
if(in[i] == 0)
{
s = i;
break;
}
dfs(s);
cout << max(dp[s][0], dp[s][1]) << endl;
}
return 0;
}
2. Strategic game POJ1463
题目大意,给出一个城市地图,各城市之间构成一棵树,现在需要派士兵守卫每一条边,一个士兵可以守卫其周围的所有边,求最少派兵数。与上题类似,也是树形dp模板题,状态设计类似,考虑如果当前点不派兵守卫,则子节点一定要派兵守卫,而当前点派兵守卫后,其子节点是否派兵其实无所谓,根据情况最优选择即可,故状态转移方程为dp[now][1]=sum(max(dp[son][1],dp[son][0])),dp[now][0]=sum(dp[son][1])。答案即为max(dp[root][0],dp[root][1])。
代码如下:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;
int n, dp[1505][2];//dp[i][0]表示以i为根(实际根不变)i处不放士兵的子树最少士兵数,dp[i][1]表示i处放士兵...
vector<int> a[1505];
void dfs(int now, int fa)
{
dp[now][1] = 1;
dp[now][0] = 0;
for(int i = 0; i < a[now].size(); i++)
{
if(a[now][i] == fa)
continue;
dfs(a[now][i], now);
dp[now][1] += min(dp[a[now][i]][1], dp[a[now][i]][0]);
dp[now][0] += dp[a[now][i]][1];
}
}
signed main()
{
while(cin >> n)
{
int u, t, v;
for(int i = 0; i <= n-1; i++)
a[i].clear();
for(int i = 0; i <= n-1; i++)
{
scanf("%d:(%d)", &u, &t);
for(int j = 1; j <= t; j++)
{
scanf("%d", &v);
a[u].push_back(v);
a[v].push_back(u);
}
}
memset(dp, 0x3f, sizeof dp);
dfs(0, -1);
cout << min(dp[0][0], dp[0][1]) << endl;
}
return 0;
}
另外需要注意的一点是,本题给出的是一棵树,树中不含奇圈,即树是二分图。根据konig定理:二分图中的最大匹配数等于这个图中的最小点覆盖数。而最小点覆盖数即以最少的点覆盖所有相邻的边,正是题目所求。因此本题还可转化为求二分图中最大匹配数,用匈牙利算法解决即可。
代码有空再补......
3. Average distance HDU2376
题目大意,给定一棵带权树,对每两对点之间的距离求和,并除以点的对数。本题有两种解法,第一种就是朴素树形dp,第二种是考虑每条边的贡献。首先是第一种解法,状态设计为令dp[i][0]表示以i为根的子树中i到各点的距离和,dp[i][1]表示除去以i为根的子树的部分中i到各点的距离和。