树形DP入门
一· 引入
作为一个DP学的很渣的人,树形DP一开始对我很不友好(我连线性DP都没掌)。握更别说树形DP了
所以,为了造福自己,奉献上此篇水文(不喜可以在下方评论)。
我对树形DP的理解(勿喷):给定了一棵树,要求在其上以最少(最大)的代价(收益)完成给定的操作。所以,对于在树上进行状态转移就显得尤为的重要。
因为树本身是由树和它的子树构成的,所以,我们可以在树上进行递归。递归到叶子节点之后,可以一步一步返回其状态更新根节点。
反过来,因为搜索基本上都可以概括成一个状态的转移,由此,我们也可以在树上进行DP。不同的条件有着不同的状态转移方程式。
接下来,我们通过一道例题来讲解树形DP。
二·例题
例题1 二叉苹果树 洛谷P2015
题面
题目描述
有一棵苹果树,如果树枝有分叉,一定是分二叉(就是说没有只有一个儿子的结点)
这棵树共有 N N N 个结点(叶子点或者树枝分叉点),编号为 1 ∼ N 1 \sim N 1∼N,树根编号一定是 1 1 1。
我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有 4 4 4 个树枝的树:
2 5
\ /
3 4
\ /
1
现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。
给定需要保留的树枝数量,求出最多能留住多少苹果。
输入格式
第一行 2 2 2 个整数 N N N 和 Q Q Q,分别表示表示树的结点数,和要保留的树枝数量。
接下来 N − 1 N-1 N−1 行,每行 3 3 3 个整数,描述一根树枝的信息:前 2 2 2 个数是它连接的结点的编号,第 3 3 3 个数是这根树枝上苹果的数量。
输出格式
一个数,最多能留住的苹果的数量。
样例 #1
样例输入 #1
5 2
1 3 1
1 4 10
2 3 20
3 5 20
样例输出 #1
21
提示
1 ⩽ Q < N ⩽ 100 1 \leqslant Q < N \leqslant 100 1⩽Q<N⩽100,每根树枝上的苹果 ⩽ 3 × 1 0 4 \leqslant 3 \times 10^4 ⩽3×104。
分析
题目大家都应该理解了,我们接下来逐步分析。
这道题的第一个考点是考察我们对于树的存储。对于树的存储有两种方法(目前我只知道两种),一种是用vector实现的邻接表,一种是链式前向星。(本蒟蒻太菜了,不会链式前向星,只能用邻接表存QWQ)。
接下来,我们开始状态转移。
我们定义一个 d p [ x ] [ j ] dp[x][j] dp[x][j] 数组,来表示以 x x x 为根的儿子上留 j j j 条边时最多的苹果数量。由此,我们可以得出,最终的答案是 d p [ 1 ] [ q ] dp[1][q] dp[1][q]。
状态转移代码如下,等会我们来分析。
for(int j=sum[x];j;j--){
for(int k=0;k<j;k++){
dp[x][j]=max(dp[x][j],dp[x][j-k-1]+dp[v][k]+w);
}
}
其中, v v v 是 x x x 的一个儿子结点,然后, d p [ x ] [ j ] dp[x][j] dp[x][j] 的计算方法有两部分。
1) d p [ v ] [ k ] dp[v][k] dp[v][k] 就是在以 v v v 为根结点的子树上留 k k k 条边
2) d p [ x ] [ j − k − 1 ] dp[x][j-k-1] dp[x][j−k−1] 就是在除了以 x x x 为根结点的子树上的 k k k 条边和 [ u , v ] [u,v] [u,v] 这一条边,一共是 k + 1 k+1 k+1 条边。这时,以 x x x 为根节点的子树上只有 j − k − 1 j-k-1 j−k−1 条边了。
接下来放出DP部分代码,带注释,好理解
void dfs(int x,int fath){
for(int i=0;i<v[x].size();i++){//遍历x的所有子节点
int vv=v[x][i].v;//x的子节点
int ww=v[x][i].w;//该边的权
if(vv==fath)continue;//就不用回去搜父节点啦,避免循环重复(一开始我就是没有注意到这个才导致我MLE)
dfs(vv,x);//递归到最深的叶子结点,然后返回一步一步更新信息直到到根结点
sume[x]+=sume[vv]+1;//sume[i]是来记录以i为根的子树的总边数,这里是累计子树上的总边数
for(int j=sume[x];j;j--){
for(int k=0;k<j;k++){
dp[x][j]=max(dp[x][j],dp[x][j-k-1]+dp[vv][k]+ww);//状态转移,解释过啦,没什么好说的
}
}
}
}
其实上面我们给的是多叉树的状态转移的方法,二叉树有其独特的转移方法,但是我们学了多叉树之后还有必要学二叉树吗(doge)。
上面一题我们也使用多叉树的方法处理的二叉树,那两部分计算方式大家都应该了解了吧。
其实上面的代码最关键的便是 d f s ( ) dfs() dfs() 中的 j j j 循环方向,不知道循环方向,我们就无法做出此题。 j j j 应该是从 s u m e [ x ] sume[x] sume[x] 开始慢慢递减,而不是从 0 0 0 开始递增。因为它此时的状态应该是以前用 x x x 的子树计算之后得来的结果,即排除当前的 v v v 的这个子树的计算结果。
而 k k k 的循环顺序是无所谓的,递增递减都可以 。
该代码的时间复杂度小于 O ( n 3 ) O(n^3) O(n3)。
例题2.没有上司的舞会 洛谷P1352
题面
题目描述
某大学有 n n n 个职员,编号为 1 … n 1\ldots n 1…n。
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 r i r_i ri,但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
输入格式
输入的第一行是一个整数 n n n。
第 2 2 2 到第 ( n + 1 ) (n + 1) (n+1) 行,每行一个整数,第 ( i + 1 ) (i+1) (i+1) 行的整数表示 i i i 号职员的快乐指数 r i r_i ri。
第 ( n + 2 ) (n + 2) (n+2) 到第 2 n 2n 2n 行,每行输入一对整数 l , k l, k l,k,代表 k k k 是 l l l 的直接上司。
输出格式
输出一行一个整数代表最大的快乐指数。
样例 #1
样例输入 #1
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
样例输出 #1
5
提示
数据规模与约定
对于 100 % 100\% 100% 的数据,保证 1 ≤ n ≤ 6 × 1 0 3 1\leq n \leq 6 \times 10^3 1≤n≤6×103, − 128 ≤ r i ≤ 127 -128 \leq r_i\leq 127 −128≤ri≤127, 1 ≤ l , k ≤ n 1 \leq l, k \leq n 1≤l,k≤n,且给出的关系一定是一棵树。
分析
本题是树形DP的经典例题!!!
如题目所说,一个职员就是一个节点,如果一个节点参加宴会,那么它的子节点就不能参加宴会。如果这个节点不参加宴会,那么它的子节点参不参加宴会都可以。
状态转移分析:
我们定义一个 d p [ i ] [ 0 ] dp[i][0] dp[i][0] 数组表示不选择当前的节点,也就是不参加宴会的的最优解。
反之 d p [ i ] [ 1 ] dp[i][1] dp[i][1] 表示选择当前节点,也就是参加宴会的最优解。
状态转移也有两种情况,我们逐个分析
1)如果我们不选择当前的节点,那么它的子节点就是可以选可以不选,我们取其中的最大值,也就是 d p [ x ] [ 0 ] + = m a x ( d p [ x ] [ 0 ] , d p [ v ] [ 1 ] ) dp[x][0]+=max(dp[x][0],dp[v][1]) dp[x][0]+=max(dp[x][0],dp[v][1])。
2)如果我们选择当前这个节点,那么它的子节点都不可以选了。也就是 d p [ x ] [ 1 ] + = d p [ v ] [ 0 ] dp[x][1]+=dp[v][0] dp[x][1]+=dp[v][0]。
下面给出本题代码
#include<bits/stdc++.h>
using namespace std;
int a[6005],fath[6005],dp[6005][2],n,r;
vector<int>s[6005];
void dfs(int x){
dp[x][0]=0;//不参加宴会初始化
dp[x][1]=a[x];//参加宴会初始化
for(auto v:s[x]){
dfs(v);//递归子节点
dp[x][1]+=dp[v][0];//选父节点,子节点不可以选
dp[x][0]+=max(dp[v][0],dp[v][1]);//不选父节点,子节点可以选可以不选
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];//快乐指数
}
for(int i=1,x,y;i<n;i++){
cin>>x>>y;
s[y].push_back(x);//vector邻接表建树
fath[x]=1;//标记,证明它不是根结点
}
for(int i=1;i<=n;i++){
if(!fath[i]){
r=i;
break;
}
}//此处的for在查找树的根结点
dfs(r);//r为此树的根结点,从根结点开始遍历这棵树
cout<<max(dp[r][0],dp[r][1]);//选与不选的最大值
return 0;
}
本代码的时间复杂度为 O ( n ) O(n) O(n)。
以上就是我对树形DP的初步理解(终于理解DP的含义了)