一文学懂 树形 dp
树型 dp
树型 dp 一般先算子树然后进行合并,遍历完子树后把子树的值合并给父亲,所以一般 dp 数组会与数编号 \(i\) 有关。
本章部分定义参考 https://oi-wiki.org,部分思路、sol 参考雨巨的 ppt。
子树大小
- 给你一棵 \(n\) 个点的树(\(1\) 号点为根节点),求以点 \(i\) 为根的子树的大小。
考虑设计状态,\(f_i\) 代表以 \(i\) 为根的子树点个数,那么转移方程为 \(f_u=1+\sum\limits_{v\in u}^{}f_v\)。
NC15033 小 G 有一个大树
- 求树的重心。
树的重心是什么?树的重心是指在树中找到一个点,其所有的子树中最大的子树节点数最少。
显然你需要在算的过程中知道子树大小,可以用前面讲过的求法求,设 \(s_i\) 为以 \(i\) 为根的子树大小。
再设 \(f_i\) 为以将点 \(i\) 删掉后最大连通块的大小,那么现在就比较显然的得出转移方程了:\(f_u=n-s_u,f_u=\mathop{\max}\limits_{v\in u}\{s_v\}
\)
最后答案为 \(\mathop{\min}\limits_{u\in T}\{f_u\}\) 的编号。
没有上司的舞会
- 最大独立集
最大独立集是指,对于图 \(G=(V,E)\),若 \(V'\subseteq V\) 且 \(V'\) 中任意两点在图 \(G\) 中不相邻,称 \(V'\) 为独立集,\(i\) 点有权值 \(h_i\),选出独立集中点权之和最大的称为最大独立集。
显然贪心是不对的,因为父亲不选儿子不选可能是更优的。所以普通的染色统计法是错误的,可以知道问题复杂在 \(i\) 选与不选会影响子树结果,那么不妨就直接在 dp 数组中额外开一维空间讨论 \(i\) 是否被选。
用 \(f_{i,0}\) 表示不选择 \(i\) 时,\(i\) 及其子树选出的最大权值和,同用 \(f_{i,1}\) 表示选择 \(i\) 时,\(i\) 及其子树选出的最大权值和。
\(f_{i,0}=\sum\limits_{}^{}\mathop{\max}\limits_{j\in i}\{f_{j,0},f_{j,1}\}\)
$f_{i,1}=h_i+\sum\limits_{j\in i}^{}f_{j,0} $
结果为 \(\max(f_{\texttt{root},0},f_{\texttt{root},1})\)。
#include<bits/stdc++.h>
#define N 6005
using namespace std;
int n,m,a[N],f[N][2];
vector<int> e[N];
void dfs(int u,int fa){
f[u][1]=a[u],f[u][0]=0;//边界
for(auto x:e[u]){//遍历儿子
if(x==fa) continue;
dfs(x,u);//递归更新
f[u][1]+=f[x][0];
f[u][0]+=max(f[x][0],f[x][1]);
}
}
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<n;i++){
int x,y;
cin>>x>>y;
e[x].push_back(y);
e[y].push_back(x);
}
dfs(1,0);
cout<<max(f[1][0],f[1][1]);
}
P2016 战略游戏
- 树的最小点覆盖
树的最小点覆盖是指,定义覆盖一个点后,所有与这个点相邻的边被瞭望到,求所有边被瞭望到所需要的最小点覆盖数量。
通过仔细观察题目可以发现,诶其实这题与上题(最大独立集)好像没啥区别?你看既然所有覆盖一个点会使所有相邻的边被瞭望,不就是选了 \(i\) 就不能选 \(i\) 的父亲与儿子吗,就少了点权而已!通过这点恍然大悟,那状态设计肯定也跟上题是一样滴!
设 \(f_{i,0}\) 表示以 \(i\) 为根的子树在 \(i\) 上放置士兵,最少所需的士兵数量。
又 \(f_{i,1}\) 表示以 \(i\) 为根的子树不在 \(i\) 上放置士兵,最少需要的士兵数量。
$f_{i,0}=\sum\limits_{j\in i}^{}f_{j,1} $
\(f_{i,1}=1+\sum\limits_{}^{}\mathop{\min}\limits_{j\in i}\{f_{j,0},f_{j,1}\}\)
树的直径
树的直径是指树上最远两点(叶子结点)的距离。
可以证明,两次 dfs 可以求出树的直径,想看证明可以网上搜搜。步骤是,第一次任意一点求出最远的点 \(P\),然则从 \(P\) 点求出离它最远的点 \(Q\),\(PQ\) 则是树的直径。
但是两次 dfs 无法跑负权,原因是第一步中任意一点会导致无法得到最优解的情况,所以如果想要跑负权,可以用树形 dp 的求法。
记录当 \(1\) 为树的根时,每个节点作为子树的根向下,所能延伸的最长路径长度 \(d\) 与次长路径(与最长路径无公共边)长度 \(d'\),那么直径就是对于每一个点,该点 \(d+d'\) 能取到的值中的最大值。
值得注意的是,虽然树形 dp 仍然可以记录直径节点,但远远会比 dfs 麻烦,若没有负权边,本人建议还是写易懂的两次 dfs。
dfs 代码略,下面给出树形 dp 的代码:
#include<bits/stdc++.h>
#define N 100005
using namespace std;
int n,h[N],idx,ans;
struct node{
int u,v,w;
}e[N];
void add(int a,int b,int c){
e[++idx]={b,h[a],c};
h[a]=idx;
}
int dfs(int u,int fa){
int dist=0;
int d1=0,d2=0;
for(int i=h[u];~i;i=e[i].v){
int j=e[i].u;
if(j==fa) continue;
int d=dfs(j,u)+e[i].w;
dist=max(dist,d);
if(d>=d1){
d2=d1;
d1=d;
}
else if(d>d2) d2=d;
}
ans=max(ans,d1+d2);
return dist;
}
int main(){
cin>>n;
memset(h,-1,sizeof(h));
for(int i=1;i<n;i++){
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
add(y,x,z);
}
dfs(1,-1);
cout<<ans<<endl;
}
Acwing 1075 数字转换
如果一个数 \(x\) 的约数之和 \(y\)(不包括他本身)比他本身小,那么 \(x\) 可以变成 \(y\),\(y\) 也可以变成 \(x\)。限定所有数字变换在不超过 \(n\) 的正整数范围内进行,求不断进行数字变换且不出现重复数字的最多变换步数。
\(x\) 的约数之和是唯一的,且只有约数之和小于自身才能转换。它朝小于自己的数转换的边至多一条,其实就是 \(x\) 的约数之和 \(x'\),把它转换到图里,连边 \(x'\rightarrow x\),可以发现本题就是求连边后森林中最大树的直径。
代码:
#include<bits/stdc++.h>
#define N 1000005
using namespace std;
int n,m,ans,idx,h[N],sum[N];
struct node{
int u,v;
}e[N];
void add(int x,int y){
e[++idx]={y,h[x]};
h[x]=idx;
}
int dfs(int u){
int dist=0;
int d1=0,d2=0;
for(int i=h[u];~i;i=e[i].v){
int j=e[i].u;
int d=dfs(j);
dist=max(dist,d);
if(d>=d1){
d2=d1;
d1=d;
}
else if(d>d2){
d2=d;
}
}
ans=max(ans,d1+d2);
return dist+1;
}
int main(){
cin>>n;
memset(h,-1,sizeof(h));
for(int i=1;i<=n;i++){
for(int j=2;j<=n/i;j++){
sum[i*j]+=i;
}
}
for(int i=2;i<=n;i++){
if(sum[i]<i){
add(sum[i],i);
}
}
dfs(1);
cout<<ans<<endl;
}
P2899 [USACO08JAN]Cell Phone Network G
- 树的最小支配集
对于无向图 \(G=(V, E)\),若 \(V'\subseteq V\) 且 \(\forall v\in(V\setminus V')\) 存在边 \((u, v)\in E\) 满足 \(u\in V'\),则 \(V'\) 是图 \(G\) 的一个 支配集,本题要求求出最小支配集。
通俗易懂的讲法,定义覆盖一个点后,所有与这个点相邻的点被瞭望到,求所有点被瞭望到所需要的最小点覆盖数量。
我们可以先考虑一个节点能被谁给染色,无非三种情况:被自己,被儿子,被父亲。那么直接根据这三种情况设状态。
设 \(f_{i,0}\) 为选点 \(i\),且以点 \(i\) 为根的子树均被覆盖的最小点数。\(f_{i,1}\) 为不选点 \(i\),\(i\) 被其儿子覆盖,且以点 \(i\) 为根的子树均被覆盖的最小点数。\(f_{i,2}\) 为不选点 \(i\),\(i\) 被其父亲覆盖,且以点 \(i\) 为根的子树均被覆盖的最小点数(儿子可选可不选)。
那么显然能得到 \(2\) 个状态转移方程:
\(f_{i,0}=1+\sum\limits_{}^{}\mathop{\min}\limits_{j\in i}\{f_{j,0},f_{j,1},f_{j,2}\}\)
$f_{i,2}=\sum\limits_{j\in i}^{}(f_{j,0}+f_{j,1}) $
对于 \(f_{i,1}\) 的转移方程细想发现不对劲,因为你必须要保证一定有一个儿子是选的,而你在转移中这样是错误的(最优解无法保证其必选),我们得先思考什么情况才会至少一个儿子是选的。首先不用考虑儿子中选自己更优的方案,考虑 \(i\) 所有儿子都选自己更劣,那么此时一定是要选出 \(\mathop{\min}\limits_{j\in i}\{f_{j,0}-f_{j,1}\}\) 的编号这个节点的,因为只有差值最小让它选才答案变化最小。
那么 \(f_{i,1}\) 的转移为:
- 当 \(i\) 为叶子节点(即没有子节点) \(f_{i,1}=\texttt{INF}\)
- 否则,\(f_{i,1}=\sum\limits_{}^{}\mathop{\min}\limits_{j\in i}\{f_{j,0},f_{j,1}\}+\texttt{inc}\)
对于 inc 有:
- \(\sum\limits_{}^{}\mathop{\min}\limits_{j\in i}\{f_{j,0},f_{j,1}\}\) 中包含某个 \(f_{j,0}\),则 \(\texttt{inc}\) 为 \(0\)。
- 否则,\(\texttt{inc}=\mathop{\min}\limits_{j\in i}\{f_{j,0}-f_{j,1}\}\)
因为一些特殊原因,给定代码的舍弃了部分分类讨论,请自行思考。
#include<bits/stdc++.h>
#define N 10005
using namespace std;
int n,dp[N][3];
vector<int> g[N];
void dfs(int u,int fa){
dp[u][0]=1;
int res=1e9;
for(auto x:g[u]){
if(x==fa) continue;
dfs(x,u);
dp[u][0]+=min(dp[x][0],min(dp[x][1],dp[x][2]));
dp[u][2]+=min(dp[x][0],dp[x][1]);
dp[u][1]+=min(dp[x][0],dp[x][1]);
res=min(res,dp[x][0]-dp[x][1]);
}
if(res<0) res=0;
dp[u][1]+=res;
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
int u,v;
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs(1,0);
cout<<min(dp[1][0],dp[1][1])<<endl;
}
P2015 二叉苹果树
- 树上背包
有一棵二叉苹果树,这棵树共有 \(N\) 个结点,编号为 \(1\sim N\),树根编号一定是 \(1\)。现在这颗树枝条太多了,需要剪枝,但是一些树枝上长有苹果。给定需要保留的树枝数量,求出最多能留住多少苹果。
设 \(f_{u,j}\) 表示以 \(u\) 为根的子树保留 \(j\) 个分支可以得到的最大苹果数量。
易得,\(f_{u,j}=\sum\limits_{k=0}^{j}\mathop{\max}\limits_{v\in u}\{f_{u,k}+f_{v,j-k-1}+W\}\)。其中 \(W\) 为权值,很像背包的思想,所以叫树上背包。
#include<bits/stdc++.h>
#define N 1005
using namespace std;
int n,m,f[N][N];
vector<pair<int,int>> g[N];
void dfs(int u,int fa){
for(auto [v,w]:g[u]){
if(v==fa) continue;
dfs(v,u);
for(int j=m;j;j--) for(int k=j-1;~k;k--) f[u][j]=max(f[u][j],f[u][j-k-1]+f[v][k]+w);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<n;i++){
int u,v,w;
cin>>u>>v>>w;
g[u].push_back({v,w});
g[v].push_back({u,w});
}
dfs(1,0);
cout<<f[1][m];
}
练习题:

浙公网安备 33010602011771号