树形 dp
在树上运行的 dp 叫做树形 dp。
树形 dp 入门问题
例 1.1:没有上司的舞会
我们发现,对于一个节点,要么选或不选,且题目中要求求出权值最大的方案,不妨分别设
那么可以得到
因为我们在转移
void dfs(int x){
dp[x][1]=w[x];
for(int i=ver[x];i;i=nxt[i]){
dfs(to[i]);
dp[x][1]+=dp[to[i]][0];
dp[x][0]+=max(dp[to[i]][0],dp[to[i]][1]);
}
return;
}
该算法时间复杂度
树形 dp 经典模型
树上背包
例 2.1:[CTSC1997] 选课
由于是最值,以及存在「费用」和「价值」两个方面,很自然联想到背包问题,设
但这个方程不好转移,直接枚举会达到指数阶,我们仿照线性 dp。在状态中添上一维:另
其中
空间复杂度显而易见
int sum=0;dp[u][0]=0;
for(int i=ver[u];i;i=nxt[i]){
int v=to[i],son;
if(v==fa)continue;
son=dfs(v,u);
sum+=son;
for(int j=sum;j>0;j--){
for(int k=1;k<=min(j,son);k++){
dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]-val[i]);
}
}
}
//这并非P2014正确代码,仅做树形背包的模型参考
换根 dp
例 2.2:[POI2008] STA-Station
我们发现,对于一棵无根树,以任意一个节点为根,树上所有节点的深度和有可能不一样的。所以一个朴素的办法是枚举任意一个根节点,遍历整棵树。时间复杂度
我们可以利用已经求过的信息来优化这个过程。我们另
假设
- 以
为根时, 的子树的深度和(就是 ) - 其他,也就是说
除了儿子 以外的部分的深度和。
当以
综上可得:
本题中的
例 2.3:[USACO12FEB] Nearby Cows G
以下均将原树成为以
显然
-
原树中
子树中的部分,即 -
其他部分,也就是距离其父节点距离不超过
,且不包括 子树的部分。
转移方程
void dfs1(int x,int fa){
for(int i=0;i<=k;i++)d[x][i]=w[x];
for(int i=ver[x];i;i=nxt[i]){
if(to[i]==fa)continue;
dfs1(to[i],x);
for(int j=1;j<=k;j++)d[x][j]+=d[to[i]][j-1];
}
return;
}
void dfs2(int x,int fa){
for(int i=ver[x];i;i=nxt[i]){
if(to[i]==fa)continue;
f[to[i]][1]=d[to[i]][1]+w[x];
for(int j=2;j<=k;j++)f[to[i]][j]=d[to[i]][j]+f[x][j-1]-d[to[i]][j-2];
dfs2(to[i],x);
}
return;
}
时间复杂度
小结
可以看出来,这种方法一般会进行两次 DFS。每次的时间复杂度
基环树 dp
定义
基环树:
基环树森林:
解题思路:找环,转换为树形 dp 或环形 dp。
找环方法
- 并查集
在读入边的时候动态维护连通块,如果此时加入一条边
该方法代码量少,适合在连通图基环树上找环,同时也可以维护一些信息
- DFS
开一个
例题 2.4:城市环路
基环树中的边最小支配集问题。我们可以任意去掉基环树上环的任意一条边
问题在于,这里其实还有一条边
scanf("%lld\n",&n);
for(int i=0;i<n;i++)scanf("%lld",w+i),f[i]=i;
for(int i=1,u,v;i<=n;i++){
scanf("%d %d",&u,&v);
if(found(u)!=found(v))f[found(u)]=found(v),add(u,v),add(v,u);
else a=u,b=v;
}
scanf("%lf",&k);
dfs(a,-1),ans=dp[a][0];
dfs(b,-1),ans=max(ans,dp[b][0]);
printf("%.1lf\n",ans*1.0*k);
树形 dp 拓展问题
主要是一些有意思的题目
例 3.1:[SDOI2006] 保安站岗
同样是一道最小支配集问题,和「没有上司的舞会」相像,只不过这次要求相邻两点必须选一个。
举个例子,加入树中有一条链 1-2-3-4
。如果选择
我们需要进一步考虑:
:以 为根的子树中,选择点 的最小权值和。
如果点
:以 为根的子树中,不选择点 ,点 的父亲被选择的情况下子树的最小权值和。
因为点
:以 为根的子树中,不选择点 ,点 的儿子被选择的情况下子树的最小权值和。
因为点
void dfs(int x,int fa){
dp[x][0]=w[x];int f=0,flag=0,minn=1e9;
for(int i=ver[x];i;i=nxt[i]){
if(to[i]==fa)continue;
dfs(to[i],x);
dp[x][0]+=min(min(dp[to[i]][0],dp[to[i]][1]),dp[to[i]][2]);//自己控制
dp[x][1]+=min(dp[to[i]][0],dp[to[i]][2]);//父亲控制
if(dp[to[i]][0]<dp[to[i]][2])dp[x][2]+=dp[to[i]][0],flag=1;//儿子控制
else dp[x][2]+=dp[to[i]][2];
f=1,minn=min(minn,dp[to[i]][0]-dp[to[i]][2]);
}
if(!f)dp[x][2]=1e9;
if(x==1)dp[x][1]=1e9;
if(f&&!flag)dp[x][2]+=minn;
return;
}
最小支配集
例 3.2:[ZJOI2006] 三色二叉树
树上染色,经典问题,下文以绿色节点的最大数为例。
设
没有儿子
有一个儿子
如果
反之如果不然绿色,染成颜色
- 如果有两个儿子
由于三个儿子都不一样。情况就很简单了,可以开一个数组来代表情况。
void dfs(int x){
dp[x][0][1]=dp[x][1][1]=dp[x][2][1]=1e6;
if(!ls[x]){
for(int i=0;i<=2;i++)dp[x][i][0]=dp[x][i][1]=0;
dp[x][0][0]=1,dp[x][0][1]=1;
}else if(!rs[x]){
dfs(ls[x]);
for(int i=1;i<=2;i++)dp[x][0][0]=max(dp[x][0][0],dp[ls[x]][i][0]+1),dp[x][0][1]=min(dp[x][0][1],dp[ls[x]][i][1]+1);
for(int i=1;i<=2;i++){
for(int j=0;j<=2;j++){
if(i==j)continue;
dp[x][i][0]=max(dp[x][i][0],dp[ls[x]][j][0]),dp[x][i][1]=min(dp[x][i][1],dp[ls[x]][j][1]);
}
}
}else{
dfs(ls[x]),dfs(rs[x]);
for(int i=0;i<=2;i++){
int a=other[i][0],b=other[i][1];
dp[x][i][0]=max(dp[x][i][0],max(dp[ls[x]][a][0]+dp[rs[x]][b][0],dp[ls[x]][b][0]+dp[rs[x]][a][0])+(i==0?1:0));
dp[x][i][1]=min(dp[x][i][1],min(dp[ls[x]][a][1]+dp[rs[x]][b][1],dp[ls[x]][b][1]+dp[rs[x]][a][1])+(i==0?1:0));
}
}
}
这种树上染色的状态很好设计,但对于状态的转移则考验分类讨论能力。
例 3.3:[ZJOI2007] 时态同步
我们另
我们发现,每次操作只能将传导速度后延,而不能加速。也就是说
而
void dfs(int x,int fa){
dp[x]=0;ll maxn=-1;
for(int i=ver[x];i;i=nxt[i]){
if(to[i]==fa)continue;
dfs(to[i],x);
maxn=max(maxn,d[to[i]]+val[i]);
}
for(int i=ver[x];i;i=nxt[i]){
if(to[i]==fa)continue;
dp[x]+=dp[to[i]]+maxn-d[to[i]]-val[i];
}
d[x]=(maxn==-1?0:maxn);
return;
}
例 3.4:[HNOI/AHOI2018] 道路
注意到给出的树是一棵满二叉树,且
特殊的,如果这个节点是村庄,那么
最后的结果就是
void dfs(ll x,ll l,ll r){
if(x>=n){
for(int i=0;i<=l;i++)
for(int j=0;j<=r;j++)
dp[x][i][j]=c[x]*(a[x]+i)*(b[x]+j);
}else{
dfs(ls[x],l+1,r),dfs(rs[x],l,r+1);
for(int i=0;i<=l;i++)
for(int j=0;j<=r;j++)
dp[x][i][j]=min(dp[ls[x]][i][j]+dp[rs[x]][i][j+1],dp[ls[x]][i+1][j]+dp[rs[x]][i][j]);
}
return;
}
例 3.5:[HAOI2009] 毛毛虫
我们写这道题之前,要理解毛毛虫本质。
-
从树中抽出一条链
-
抽出与链上节点相连的所有点
毛毛虫只有两种情况,一是单独存在于某个节点的子树中,而是经过这个节点,我们可以把它看成带权的树的直径,树形 dp 求解就行
void dfs(int x,int fa){
int cnt=0;
for(int i=ver[x];i;i=nxt[i],cnt++){
if(to[i]!=fa)dfs(to[i],x);
}
d[x]=max(1,cnt);
for(int i=ver[x];i;i=nxt[i]){
if(to[i]==fa)continue;
ans=max(ans,d[x]+d[to[i]]);
d[x]=max(d[to[i]]+cnt-1,d[x]);
}
}
//这种写法要特判n=1的情况
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!