一文学懂 树形 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];
}

练习题:

posted @ 2023-06-10 23:22  Telsif  阅读(49)  评论(0)    收藏  举报