什么是树形dp?
树形dp是一种应用于树形结构的动态规划算法。在树形结构中,每个节点可以有零个或多个子节点,但没有环(即无向图中没有回路)。树形动态规划通常用于解决与树相关的问题,例如路径问题、最大独立集、最小路径覆盖等。
选择节点类
例1洛谷p1352
题意:有n个职员,每邀请到一个职员会增加快乐指数ri,如果某个职员的直接上司来参加了,那么这个职员就不来了。求最大快乐指数。
分析:
假设1号来了,所求的最大值就是2号没来的最大值加3号没来时的最大值最后加上1的权值;
假设1号没来,所求最大值就是max(2号来,2号不来)+max(3号来,3号不来);就得到了节点1的最大值,所有节点都可以根据这个方程求得该状态的最大值;主要的实现形式是dp[i][j][0/1],i是以i为根的子树,j是表示在以i为根的子树中选择𝑗j个子节点,0表示这个节点不选,1表示选择这个节点。有的时候j或0/1这一维可以压掉
设f[x][0]表示以x为根的子树,且x不参加舞会的最大快乐值
f[x][1]表示以x为根的子树,且x参加了舞会的最大快乐值
#include<bits/stdc++.h>
using namespace std;
const int N=6e3+10;
int a[N],ne[N],edge[N],dp[N][2],last[N],cnt=1;
bool yf[N];
void add(int a,int b){
edge[cnt]=b;
ne[cnt]=last[a];
last[a]=cnt++;
}
void dfs(int x){
dp[x][0]=0;//不叫x去
dp[x][1]=a[x];//叫了x去
for(int i=last[x];i>=1;i=ne[i]){//遍历x之后的所有子节点
int j=edge[i];//j是i的直接子节点
dfs(j);
dp[x][0]+=max(dp[j][0],dp[j][1]);
dp[x][1]+=dp[j][0];
}
}
int main(){
int n;cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
int x,y;
for(int i=1;i<n;i++){
cin>>x>>y;
add(y,x);//y是x的父亲
yf[x]=true;//x有父亲
}
int v=1;
while(yf[v])v++;//找到没有父亲的点
dfs(v);
cout<<max(dp[v][1],dp[v][0])<<endl;
}
//for(int i=1;i<=7;i++){
// cout<<edge[i]<<" "<<ne[i]<<" "<<last[i]<<endl;
// }
//1 0 0
//2 1 0
//6 0 2
//7 3 4
//4 0 6
//3 5 0
//0 0 0
树形背包类
例2洛谷p2014
题意:有n门课程,要从中学m门课程,使得获得总学分最大化。并且在学某些课程之前要先学完某些先修课。求最大总学分。
分析:用dfs扫描整个数,然后对树上每个节点进行背包。先让当前节点加上父节点的权值,对于剩下可以选的几次,更新节点状态,调用dfs进行搜索,然后再更新父节点的最大权值。
#include<bits/stdc++.h>
using namespace std;
int val[1000],edge[1000],ne[1000],last[1000],f[1000][1000],cnt=1;
void add(int a,int b){
edge[cnt]=b;
ne[cnt]=last[a];
last[a]=cnt++;
}
void dfs(int x,int y){
if(y==0)return;//已经选了m个了
for(int i=last[x];i>=1;i=ne[i]){
int j=edge[i];//j是i的儿子;
f[j][0]=f[x][0]+val[j];//初始状态
for(int k=1;k<=y;k++){//将权值加上之前的值
f[j][k]=f[x][k]+val[j];
}
dfs(j,y-1);//进入下一层搜索
for(int k=1;k<=y;k++){//搜完啦可以更新了
f[x][k]=max(f[x][k],f[j][k-1]);
}
}
}
int main(){
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++){
int a;cin>>a>>val[i];
add(a,i);//a是i的父亲节点
}
dfs(0,m);
cout<<f[0][m];
}
例题
例3洛谷p2016
题意:所有士兵放在一棵树上,在这棵树的结点上放置最少数目的士兵,使得这些士兵能瞭望到所有的路。某个士兵在一个结点上时,与该结点相连的所有边将都可以被瞭望到。求最少的士兵数
分析:求最少士兵数,判断此题为选择节点类。判断其中一个士兵的状态
dp[x][0]+=dp[j][1];
dp[x][1]+=min(dp[j][1],dp[j][0])+1;
x为当前士兵,j为x的子节点。如果x不去,那它的子节点j就一定得去,如果x去,选择子节点去还是不去取最小值。
#include<bits/stdc++.h>
using namespace std;
const int N=2e4+10;
int a[N],ne[N],edge[N],dp[N][4],last[N],cnt=1;
bool yf[N];
void add(int a,int b){
edge[cnt]=b;
ne[cnt]=last[a];
last[a]=cnt++;
}
void dfs(int x,int fa){
dp[x][0]=0;//x不去
dp[x][1]=1;//x去
for(int i=last[x];i;i=ne[i]){//遍历x之后的所有子节点
int j=edge[i];//j是i的直接子节点
if(j==fa)continue;//x没有儿子节点
dfs(j,x);
dp[x][0]+=dp[j][1];//不去就只能让儿子去了
dp[x][1]+=min(dp[j][1],dp[j][0]);//去的话,看儿子去还是不去能取最小值
}
}
int main(){
int n;cin>>n;
int x,k,y;
for(int i=1;i<=n;i++){
cin>>x>>k;
x++;
while(k--){
cin>>y;
y++;
add(y,x);
add(x,y);
}
}
dfs(1,0);
cout<<min(dp[1][1],dp[1][0])<<endl;
}