一、树形DP思想介绍
在树这种数据结构上做DP很常见:给出一棵树,要求以最少代价(或最大收益)完成给定的操作。
并且在树上做DP显得非常自然,因为树本身有“子结构”性质(树和子树)。
基于树的解题步骤一般是先把树转换为有根树(如果是几棵互不连通的树,就加一个虚拟根,它连接所有孤立的树),然后在树上做DFS,递归到最底层的叶子节点,再一层层返回信息更新至根节点。显然,树上DP所操作的就是这一层层返回的信息。不同的题目需要灵活设计不同的DP状态和转移方程。
二、树形DP经典例题
2.1 二叉苹果树
题目描述
有一棵苹果树,如果树枝有分叉,一定是分二叉(就是说没有只有一个儿子的结点)
这棵树共有 N 个结点(叶子点或者树枝分叉点),编号为 1∼N,树根编号一定是 1。
我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有 4 个树枝的树:
2 5
\ /
3 4
\ /
1
现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。
给定需要保留的树枝数量,求出最多能留住多少苹果。
输入格式
第一行 2 个整数 N 和 Q,分别表示表示树的结点数,和要保留的树枝数量。
接下来 N−1 行,每行 3 个整数,描述一根树枝的信息:前 2 个数是它连接的结点的编号,第 3 个数是这根树枝上苹果的数量。
输出格式
一个数,最多能留住的苹果的数量。
解题步骤
-
状态表示
f[x][i]表示以结点x为根的子树保留i个树枝所获得的最大边权值和。 -
状态计算
树的父结点的状态由子结点转移而来,设以结点x为根的子树有一颗以结点u为根的子树,则状态转移方程为:
f [ x ] [ i ] = m a x ( f [ x ] [ i ] , f [ x ] [ i − j − 1 ] + f [ u ] [ j ] + w [ i ] ) f[x][i] = max(f[x][i], f[x][i - j - 1] + f[u][j] + w[i]) f[x][i]=max(f[x][i],f[x][i−j−1]+f[u][j]+w[i])
i − j − 1 i-j-1 i−j−1多减了1的原因是结点与子树连接需要用掉一根树枝,w[i]为该树枝的权值。 -
初始化
根据本题题意,一颗树若没有树枝该树留住的苹果数量为0。 -
实现细节
我们需要统计以x为根的子树的树枝总数sz[x],为了方便进行状态转移时的边界控制,确保在枚举保留的树枝数量时不超过子树的实际大小
。
第一层遍历的时候为什么要从大到小枚举树枝数?
与01背包问题dp优化的原理类似。我们希望状态转移时f[x][i-j-1]代表的是为与当前枚举的子树u没有连接的树x在拥有i-j-1个树枝保留的最多苹果数。若我们从小到大枚举树枝,则f[x][i-j-1]可能已经被更新了,相当于同一颗子树被考虑连接两次,不符合题意。
代码实现
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
const int N = 300, M = 2 * N;
int h[N], e[M], ne[M], w[M], idx;
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
int f[N][N];
int n, m;
int sz[N];
void dfs(int x, int father)
{
for(int i = h[x]; ~i; i = ne[i])
{
int j = e[i];
if(j == father)continue;
dfs(j, x);
sz[x] += sz[j] + 1;
for(int size_x = min(n-1 , sz[x]); size_x > 0; size_x --)
for(int k = min(size_x-1, sz[j]); k >= 0; k --)
{
f[x][size_x] = max(f[x][size_x], f[x][size_x - k - 1] + f[j][k] + w[i]);
}
}
}
int main()
{
scanf("%d %d", &n, &m);
memset(h, -1, sizeof h);
for(int i = 0; i < n - 1; i ++)
{
int a, b, c;
scanf("%d %d %d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
dfs(1, 0);
printf("%d", f[1][m]);
return 0;
}
2.2 没有上司的舞会
题目描述
某大学有 n 个职员,编号为 1…n。
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 r i r_i ri,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
解题步骤
-
状态表示
f[x][0]表示以结点x为根的子树不选择该结点获得的最大happy值,f[x][1]表示以结点x为根的子树选择该结点获得的最大happy值. -
状态计算
代码实现
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
const int N = 6010;
int f[N][2];
int h[N], ne[N], e[N], idx;
int happy[N], has_fa[N];
int n;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void dfs(int u)
{
f[u][1] = happy[u];
for(int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
dfs(j);
f[u][0] += max(f[j][0], f[j][1]);
f[u][1] += f[j][0];
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++)
scanf("%d", &happy[i]);
memset(h, -1, sizeof(h));
for(int i = 0; i < n - 1; i ++)
{
int a, b;
scanf("%d %d", &a, &b);
add(b, a);
has_fa[a] = true;
}
int root = 1;
while(has_fa[root])root++;
dfs(root);
printf("%d", max(f[root][0], f[root][1]));
}