树形dp
顾名思义,即在树上进行dp,涵盖普通dp(如例题一、三)、各种背包dp(如例题二、四)等。由于树的特殊递归结构,所以与普通dp不同的是,树形dp一般都是递归进行的。
例题
一、洛谷P1352 没有上司的舞会
题目大意
有\(n\)各职员,编号\(1-n\),他们的关系就像一棵树,父亲节点是儿子节点的直接上司,如果一个职员的直接上司来参加舞会了,那么他无论如何也不回来参加舞会,对于每个职员\(i\)都有一个快乐指数\(val[i]\),求来参加舞会的职员的快乐指数的和的最大值.
设计状态
设\(dp[i][0/1]\)表示以第\(i\)个职员为子树的快乐指数的最大值,其中\(dp[i][0]\)表示第\(i\)个职员不来参加舞会的最大快乐指数,\(dp[i][1]\)表示第\(i\)个职员来参加舞会的最大快乐指数.
状态转移
设职员\(y\)是职员\(x\)的儿子,那么显然
因为\(x\)来参加舞会了,\(y\)就不能参加舞会.
因为\(x\)没来参加舞会,\(y\)参加不参加都可以.
初始\(dp[x][0]=0\),\(dp[x][1]=val[x],\)在\(dfs\)返回上一层时记录\(x\)的答案.
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn=6e3+5;
ll n,u,v,rt,val[maxn],dp[maxn][2],ru[maxn];
vector<ll> e[maxn];
void dfs(ll x,ll fa){
dp[x][0]=0;
dp[x][1]=val[x];
for(auto y:e[x]){
if(y!=fa){
dfs(y,x);
dp[x][1]+=dp[y][0];
dp[x][0]+=max(dp[y][0],dp[y][1]);
}
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n;
for(ll i=1;i<=n;i++){
cin>>val[i];
}
for(ll i=1;i<n;i++){
cin>>u>>v;
e[v].push_back(u);
ru[u]++;
}
for(ll i=1;i<=n;i++){
if(ru[i]==0){
rt=i;
break;
}
}
dfs(rt,0);
cout<<max(dp[rt][0],dp[rt][1]);
return 0;
}
二、洛谷P2014 选课
题目大意
有\(n\)门课,每一门课都有不同的学分,也都有一门或没有直接先修课(若课程\(a\)是课程\(b\)的先修课即只有学完了课程\(a\),才能学习课程\(b\)).一个学生要在这些课程里选\(m\)门课程学习,他最大能获得多少学分.
审题
根据每门课只有一门或没有先修课的特点,可以联想到有根树中一个点最多只有一个父亲节点的特点,而根据本题描述,我们可以构造一个森林,为了便于搜索,我们设\(0\)节点为整个森林的根.
设计状态
设\(dp[x][i][j]\)表示以\(x\)为根的子树,已经遍历了\(x\)号节点的前\(i\)棵子树,选了\(j\)门课程的最大学分.
状态转移
设\(x\)的儿子个数为\(sum_x\),以\(x\)为根的子树大小为\(siz_x\),那么
不难发现,\(dp\)数组的第二维类似背包\(dp\),可以用滚动数组省略掉,此时枚举时要倒序枚举\(j\)的值.
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll n,m,k,s[305],dp[305][305];
vector<ll> e[305];
void dfs(ll x){
for(auto y:e[x]){
dfs(y);
for(ll t=m;t>=0;t--){
for(ll j=0;j<=t;j++){
dp[x][t]=max(dp[x][t],dp[x][t-j]+dp[y][j]);
}
}
}
if(x!=0){
for(ll t=m;t>=1;t--){
dp[x][t]=dp[x][t-1]+s[x];
}
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>m;
for(ll i=1;i<=n;i++){
cin>>k>>s[i];
e[k].push_back(i);
}
dfs(0);
cout<<dp[0][m];
return 0;
}
三、洛谷P3574 FAR-FarmCraft
题目大意
有\(n\)个节点,\(n-1\)条边,构成一棵以\(1\)为根的树,从根节点开始遍历整棵树,并给每一个节点记录下遍历到其的时间,同时每一个节点又有一个自己的倒计时,当遍历到其的时候就开始计时,我们需要求出所有节点中倒计时结束最慢的那一个的结束时间,并最小化之.即找到最小化节点权重与到达时间之和的最大值.
设计状态
设\(dp[x]\)表示到第\(x\)个人后,以\(x\)为根的所有人完成的最短时间;\(siz[x]\)表示遍历过\(x\)所在的子树之前已经经过的所有子树的大小之和再\(*2\),即在遍历完整个子树后\(siz[x]\)会更新为以\(x\)为根的子树的大小\(*2\).
状态转移
对于节点\(x\)的子树\(y\),可得:
可以发现,\(dp[x]\)最终的大小和遍历子树的顺序有关,所以要对遍历子树的顺序进行排序.
设节点\(x\)的两棵子树为\(y\)、\(z\).
假设先遍历子树\(y\)再遍历子树\(z\),那么
假设交换遍历顺序,先遍历子树\(z\)再遍历子树\(y\),那么
再假设交换后的方案比交换前的更优,即
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn=5e5+5;
ll n,val[maxn],dp[maxn],siz[maxn];
vector<ll> e[maxn];
bool cmp(ll y,ll z){
return dp[y]-siz[y]>dp[z]-siz[z];
}
void dfs(ll x,ll fa){
if(x!=1){
dp[x]=val[x];
}
if(!e[x].empty()){
for(auto y:e[x]){
if(y!=fa){
dfs(y,x);
}
}
sort(e[x].begin(),e[x].end(),cmp);
for(auto y:e[x]){
if(y!=fa){
dp[x]=max(dp[x],dp[y]+siz[x]+1);
siz[x]+=siz[y]+2;
}
}
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n;
for(ll i=1;i<=n;i++){
cin>>val[i];
}
for(ll i=1;i<n;i++){
ll u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1,0);
cout<<max(dp[1],siz[1]+val[1]);
return 0;
}
四、洛谷P4516 潜入行动
题目大意
一棵\(n\)个点、\(n-1\)条边的无向树,在其节点\(u\)上安装监听设备,对于每一条边\((u,v)\),节点\(v\)都会被监听,需要注意的是,放置在节点\(u\)的监听设备并不监听\(u\)本身.现在一共有\(k\)个监听设备,求有多少种不同的放置监听设备的方法(每个节点至多只能安装一个监听设备,且监听设备必须被用完),能够使得所有节点都被监听?
设计状态
设\(dp[x][i+j][0/1][0/1]\)表示以\(x\)号节点为根的子树已经放置了\(i+j\)个监听器、\(x\)号节点放没放、\(x\)号节点能不能被其子节点监听到的方案数,其中\(i\)是以\(x\)为根的子树中已经遍历的所有节点上放置的监听装备数,\(j\)是以当前\(x\)的孩子为根的子树上放置的监听装备数,设\(siz[x]\)表示到目前为止已经遍历的以\(x\)为根的子树的大小,\(siz[x]\)在遍历完以\(x\)为根的子树后就是这棵子树的完整大小.
状态转移
这里的状态转移比较复杂,类似背包,一共有四个状态,分别对应:\(dp[x][i+j][0][0]\)、\(dp[x][i+j][0][1]\)、\(dp[x][i+j][1][0]\)和\(dp[x][i+j][1][1]\).
首先初始每个点的\(dp[x][0][0][0]\)\(dp[x][1][1][0]\)都为1,即以\(x\)为根的子树没装监听设备,或装了一个(在\(x\)节点装的,所以\(x\)不能被监听到)的方案数是\(1\).
下面设\(x\)的子节点为\(y\).
1.对于\(dp[x][i+j][0][0]\),由于\(x\)既没放监听设备,又没有被其子节点监听,所以节点\(y\)一定没放监听设备,且必须已被监听,即
2.对于\(dp[x][i+j][0][1]\),\(x\)被监听了但没放监听设备,要分两种情况:
①对于\(dp[x][i][0][0]\),\(x\)还没被已遍历过的子节点监听,那么监听\(x\)的任务就交给了\(y\),而\(y\)有没有被监听无所谓;
②对于\(dp[x][i][0][1]\),\(x\)已经被其他子节点监听了,所以\(y\)放不放都无所谓,但要保证\(y\)是被监听的;
即
3.对于\(dp[x][i+j][1][0]\),
\(x\)没被其子节点监听,但放了监听设备,所以\(y\)有没有被监听无所谓,但一定放了监听设备,即
4.对于\(dp[x][i+j][1][1]\),\(x\)放了监听设备且被其子节点监听了,这时候也要分两种情况:
①对于\(dp[x][i][1][0]\),\(x\)还没被已遍历过的子节点监听,那么监听\(x\)的任务就交给了\(y\),而\(y\)有没有被监听无所谓;
②对于\(dp[x][i][1][1]\),\(x\)已被监听,那么\(y\)有没有放监听设备,有没有被监听都无所谓;
即
注意点
1.由于\(1\leq n\leq 10^5\),\(1\leq k\leq min(n,100)\),所以dp数组不能开\(long\) \(long\)(不然会\(MLE\)),但计算时又会爆\(int\),所以要单开一个\(long\) \(long\)数组\(tmp\)转一下.
2.注意循环边界,遍历\(i\)时从\(0\)到\(min(k,siz[x])\),\(j\)从\(0\)到\(min(k-i,siz[y])\).
3.输出时是\(dp[1][k][0][1]+dp[1][k][1][1]\),即刚好用完了\(k\)个监听设备,根节点放或没放监听设备都可以,但一定被监听的方案数.
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn=1e5+5,mod=1e9+7;
ll n,k,tmp[105][2][2],siz[100005];
int dp[100005][105][2][2];//dp[i][j][0/1][0/1]表示以i为根的子树已经放置了j个监听器,i放没放,i能不能被监听到的方案数
vector<ll> e[maxn];
void dfs(ll x,ll fa){
siz[x]=dp[x][0][0][0]=dp[x][1][1][0]=1;
for(auto y:e[x]){
if(y!=fa){
dfs(y,x);
for(ll i=0;i<=min(k,siz[x]);i++){
tmp[i][0][0]=dp[x][i][0][0];
tmp[i][0][1]=dp[x][i][0][1];
tmp[i][1][0]=dp[x][i][1][0];
tmp[i][1][1]=dp[x][i][1][1];
dp[x][i][0][0]=0;
dp[x][i][0][1]=0;
dp[x][i][1][0]=0;
dp[x][i][1][1]=0;
}
for(ll i=0;i<=min(k,siz[x]);i++){
for(ll j=0;j<=min(k-i,siz[y]);j++){
ll a=dp[x][i+j][0][0],b=dp[x][i+j][0][1],c=dp[x][i+j][1][0],d=dp[x][i+j][1][1];
ll e=dp[y][j][0][0],f=dp[y][j][0][1],g=dp[y][j][1][0],h=dp[y][j][1][1];
dp[x][i+j][0][0]=(a+tmp[i][0][0]*f%mod)%mod;
dp[x][i+j][1][0]=(c+tmp[i][1][0]*(e+f)%mod)%mod;
dp[x][i+j][0][1]=(b+tmp[i][0][1]*(f+h)%mod+tmp[i][0][0]*h%mod)%mod;
dp[x][i+j][1][1]=(d+tmp[i][1][0]*(g+h)%mod+tmp[i][1][1]*(e+f+g+h)%mod)%mod;
}
}
siz[x]+=siz[y];
}
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>k;
for(ll i=1;i<n;i++){
ll u,v;
cin>>u>>v;
e[u].push_back(v);
e[v].push_back(u);
}
dfs(1,0);
cout<<(dp[1][k][0][1]+dp[1][k][1][1])%mod;
return 0;
}

浙公网安备 33010602011771号