( 动态规划专题 )【 树形dp 】
树形DP是一种DP的思想,只不过普通dp是在数组上进行的,而树形dp是在树上进行的。
直接看例题:P2015 二叉苹果树
题目链接:https://www.luogu.com.cn/problem/P2015
题目描述
有一棵苹果树,如果树枝有分叉,一定是分2叉(就是说没有只有1个儿子的结点)
这棵树共有N个结点(叶子点或者树枝分叉点),编号为1-N,树根编号一定是1。
我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有4个树枝的树
2 5
\ /
3 4
\ /
1
现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。
给定需要保留的树枝数量,求出最多能留住多少苹果。
输入格式
第1行2个数,N和Q(1<=Q<= N,1<N<=100)。
N表示树的结点数,Q表示要保留的树枝数量。接下来N-1行描述树枝的信息。
每行3个整数,前两个是它连接的结点的编号。第3个数是这根树枝上苹果的数量。
每根树枝上的苹果不超过30000个。
输出格式
一个数,最多能留住的苹果的数量。
输入输出样例
输入 #1
5 2
1 3 1
1 4 10
2 3 20
3 5 20
输出 #1
21
思路:
很明显,这是一个树状结构,我们可以从这点出发来考虑。这道题明确给出了根的位置,也就确定了父子节点的关系,我们会发现,对于每个父节点的状态,都是由它的子节点转移过来的,所以我们大概可以得出这里有一个由子节点转移到父节点的状态转移方程,又因为父节点子树上选择的边数完全取决于子节点的子树选择的边数。
f[u][i] = max(f[u][i], f[u][i−j−1] + f[v][j] + e[i].w) ( 1≤i≤min(q,sz[u]),0≤j≤min(sz[v],i−1) )
u表示当前节点,v是u的一个子节点,sz[u]表示u的子树上的边数,q就是题目中要求的最多保留边数
f[u][i] 表示以u为根节点的子树选择 i 条边权值和最大为多少,这实际上就是一个背包。为什么两个f数组的边数为i−1条呢?因为我们若想取一颗子节点的子树上的边,那就必须取父节点与子节点相连的那条边。
代码:
#include <bits/stdc++.h>
using namespace std;
int n,m;
int dp[105][105];
int head[105],cnt;
int sz[105];
struct node {
int to,nxt,w;
}e[105];
void addage( int u, int v, int w )
{
e[cnt].to = v;
e[cnt].w = w;
e[cnt].nxt = head[u];
head[u] = cnt++;
}
void dfs( int u, int fa )
{
for ( int i=head[u]; i!=-1; i=e[i].nxt ) {
int v = e[i].to;
if ( v==fa ) continue;
dfs(v,u);
sz[u] += sz[v] + 1;
// j,k要倒序枚举因为这是01背包
for ( int j=min(sz[u],m); j>=0; j-- ) { // 以点u为根节点,一共有多少条边。
for ( int k=min(sz[v],j-1); k>=0; k-- ) { // 以以点v为根节点,一共有多少条边。
dp[u][j] = max( dp[u][j], dp[u][j-k-1]+dp[v][k]+e[i].w );
/*首先,为什么是f[u][i−j−1]而不是f[u][i−j] ?
前文提到了,保留一条边必须保留从根节点到这条边路径上的所有边,
那么如果你想从u的子节点v的子树上留边的话,也要留下u,v之间的连边*/
}
}
}
}
int main()
{
cin >> n >> m;
memset(head,-1,sizeof(head)); cnt=0;
for ( int i=0; i<n-1; i++ ) {
int u,v,w;
cin >> u >> v >> w;
addage(u,v,w);
}
dfs(1,-1);
cout << dp[1][m] << endl;
return 0;
}