【动态规划 & 树形dp】Tree DP ~~~详解
一、简单介绍 (Tree DP)
-
树形动态规划 ( Tree DP ) 是一种常用的动态规划技巧,跟普通的线性动态规划不同,此算法将DP建立在树状结构或图的基础上,是一种 DP 的思想。
-
以下是 树形 DP 的一般步骤:
①、定义状态:根据问题的特点,定义每个节点需要保存的状态。可以是最大值、最小值、累加和、路径长度等等。
②、设计转移方程:根据问题的要求,定义每个节点状态之间的转移关系。这个转移关系通常由树的拓扑结构决定,先在每个节点处定义状态,再利用已经计算出的节点状态来计算当前节点的状态。
③、遍历计算:使用深度优先搜索 (DFS)或广度优先搜索(BFS)遍历整个树,按照定义的转移方程计算每个节点的状态。
④、求解最终结果:根据最终状态的定义,从中选择所需的结果,比如最大值、最小值等。
二、代码实现
树形dp 之 选择节点类
例题引入
题目
描述
某大学有
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。
输入格式
输入的第一行是一个整数
第 2 到第
第
输出格式
输出一行一个整数代表最大的快乐指数。
输入/输出样例
输入 #1
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
输出 #1
5
说明/提示
数据规模与约定
对于
思路
先浅浅地说说选择节点类的一般转移方程:
选择节点式的题首先前提条件是整个数据是由树形结构存储的,或者应该用树形结构存,运行的效率更高,而且相邻的节点是不能同时存在的,最后要求取最大最小值 。
I.建立树
用链式前向星建树,这里是储存一棵树,所以每两个结点之间只需要建立一条边,即等同于有向无环图。
我们还需要统计每个结点的入度,如果当前的结点的入度为
II. dfs 遍历整棵树
借助链式前项星遍历整棵树。
此处用一个数组
状态转移方程如下:
①.
②.
最后找到根节点,
Code
#include<bits/stdc++.h>
using namespace std;
const int N=6001;
int n,h[N],l,k,root;
struct Edge {
int v,nxt;
}edge[N<<1];
int head[N],idx;
int f[N][3],degree[N];
void add(int x,int y) { //邻接表建图
edge[++idx].v=y;
edge[idx].nxt=head[x];
head[x]=idx;
}
void dfs(int u,int fa) { //进行树形dp
f[u][0]=0,f[u][1]=h[u];
for(int i=head[u];i;i=edge[i].nxt) { //遍历当前结点u的子节点v
int v=edge[i].v;
dfs(v,u); //继续向下dfs遍历
f[u][0]+=max(f[v][1],f[v][0]); //父节点由子节点转移过来
f[u][1]+=f[v][0]; //若上司不去,下属可以选择去不去;反之,上司去,下属就不去
}
}
int main() {
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&h[i]);
for(int i=1;i<n;i++) {
scanf("%d%d",&l,&k);
add(k,l); //有向边构图,有直属关系,
degree[l]++; //记录入度
}
for(int i=1;i<=n;i++) { //寻找根节点
if(degree[i]==0) {
root=i;
break;
}
}
dfs(root,0);
printf("%d\n",max(f[root][0],f[root][1])); //取最大值
return 0;
}
类似的选择节点类的经典例题还有 洛谷 P2016 战略游戏 等
树形dp 之 树的直径类(有些超纲)【图论•算法】
- 树的直径,(Diameter of a Tree),又称树的最长链,定义为一棵树上最远的两个节点的路径,即树上一条不重复经过某一条边的最长的路径。树的直径也可以代指这条路径的长度,树是以邻接表存无向边的形式储存的。
在树上寻找 树的直径(最长链) 的方法:
前提:此做法只有在权值为正数的情况下才能使用。
1). 对于任意一点,假设这个结点为
2). 再对
3). 最后,从
注意:一棵树的所有直径的长度(边权值总和) 都一样。
对于此处,大家可能会放出疑问,或者你已经理解了,但你只是模糊的明白,还不能很好的证明,本蒟蒻也是学得一脸蒙,最后只能搬大佬的详细证明,再结合自我理解,最通俗地讲述给大家。
F1 证明(反证大法)
假设此树的最长路径是从
假设搜到的点是
1、结点
2、结点
如图所示:
此时
则
3、节点
综上所述,经过两次的 dfs 求出最长路径的两个端点和其的路径长度便是树的直径。
F2 证明(交换论证大法):
!!大佬做法!!
经典模板题 洛谷转载自SPOJ PT07Z - Longest path in a tree
题目描述
给你一个无权无向的树。编写程序以输出该树中最长路径(从一个节点到另一个节点)的长度。在这种情况下,路径的长度是我们从开始到目的地的遍历边数。
输入格式
输入文件的第一行包含一个整数
接下来
输出格式
输出最长路径的长度。
输入/输出样例
输入 #1
3
1 2
2 3
输出 #1
2
说明/提示
对于
Code(权值无负,全正)
如果权值全部都为不为负数,即任意结点
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
int n,x,y;
struct Edge {
int v,nxt;
}edge[N<<1];
int head[N],idx;
int dist[N];
void add(int x,int y) {
e[++idx].v=y;
e[idx].nxt=head[x];
head[x]=idx;
}
void dfs(int u,int step) {
if(dist[u]!=0) return ;
dist[u]=step;
for(int i=head[u];i;i=edge[i].nxt) {
int v=edge[i].v;
dfs(v,step+1);
}
}
int main() {
scanf("%d",&n);
for(int i=1;i<n;i++) {
scanf("%d%d",&x,&y);
add(x,y),add(y,x);
}
dfs(1,1);
int maxx=0,k=0;
for(int i=1;i<=n;i++)
if(dist[i]>maxx) maxx=dis[i],k=i;
memset(dist,0,sizeof(dist));
dfs(k,1);
maxx=0;
for(int i=1;i<=n;i++)
maxx=max(dist[i],maxx);
printf("%d\n",maxx-1);
return 0;
}
题目描述
给定一棵树,树中包含
现在请你找到树中的一条最长路径。
换句话说,要找到一条路径,使得路径两端的点的距离最远。
注意:路径中可以只包含一个点。
输入格式
第一行包含整数
接下来
输出格式
输出一个整数,表示树的最长路径的长度。
输入/输出样例
输入 #1
6
5 1 6
1 4 5
6 3 9
2 6 8
6 1 7
输出 #1
22
说明/提示
数据规模与约定
对于
Code (权值有负)
此处蒟蒻一开始并没有用树形dp去做,思想是对于树上的任意结点,都对其找最远
为什么此处不能直接二次
#include<bits/stdc++.h>
using namespace std;
const int N=1e6;
int n,a,b,c,ans;
struct Edge {
int v,nxt;
int w;
}edge[N<<1];
int head[N],idx;
void add(int x,int y,int z) {
edge[++idx].v=y;
edge[idx].nxt=head[x];
edge[idx].w=z;
head[x]=idx;
}
int dfs(int u,int fa) {
int d1=0,d2=0,dist=0;
for(int i=head[u];i;i=edge[i].nxt) {
int v=edge[i].v;
int w=edge[i].w;
if(v==fa) continue;
int d=dfs(v,u)+w;
dist=max(d,dist);
if(d>d1) {
d2=d1;
d1=d;
} else if(d>d2) d2=d;
}
ans=max(d1+d2,ans);
return dist;
}
int main() {
scanf("%d",&n);
for(int i=1;i<n;i++) {
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
add(b,a,c);
}
dfs(1,0);
printf("%d\n",ans);
return 0;
}
但为了凸显我们信息学的举一反三的优良学习精神,我们就大方的开个数组。
其实也没什么用,反正空间爆不了。
下面是改进的代码 (啥也不是) :
#include<bits/stdc++.h>
using namespace std;
const int N=1e6;
int n,h[N],a,b,c,ans;
struct Edge {
int v,nxt;
int w;
}edge[N<<1];
int head[N],idx;
int f[N];
void add(int x,int y,int z) {
edge[++idx].v=y;
edge[idx].nxt=head[x];
edge[idx].w=z;
head[x]=idx;
}
int dfs(int u,int fa) {
int d1=0,d2=0;
for(int i=head[u];i;i=edge[i].nxt) {
int v=edge[i].v;
int w=edge[i].w;
if(v==fa) continue;
int d=dfs(v,u)+w;
f[u]=max(d,f[u]);
if(d>d1) {
d2=d1;
d1=d;
} else {
if(d>d2)
d2=d;
}
}
ans=max(d1+d2,ans);
return f[u];
}
int main() {
scanf("%d",&n);
for(int i=1;i<n;i++) {
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
add(b,a,c);
}
dfs(1,0);
printf("%d\n",ans);
return 0;
}
拓展
树的中心为“到所有点的最大距离“最小的节点。而定义树的“几何中心”,除了在节点上,还可以处于某条边中。
那么有性质:
当边权为正时,所有直径都经过(几何)中心。
证明:
显然,几何中心可以把树分为多棵子树,而一条直径经过几何中心当且仅当这条直径的两端点在不同子树中。容易看出,若一条直径的两端在同一子树中,那么将此时的“伪几何中心”向这课子树方向移动显然只会让这个最大距离进一步变小。
如图,显然有
显然,若所有直径经过几何中心,则所有直径要么都经过某条边及其两端点(其中至少一个为中心),要么经过中心,证毕。
树形dp 之 树形背包类
请让蒟蒻说说树形背包类的一般转移方程:
树形背包,就是背包加上条件,在树上进行背包选择,一个物品只有选择了它的主件(父亲结点) 才能选择。
题目来源 洛谷P2015 苹果树
题目描述
有一棵苹果树,如果树枝有分叉,一定是分二叉(就是说没有只有一个儿子的结点)
这棵树共有
我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有
2 5
\ /
3 4
\ /
1
现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。
给定需要保留的树枝数量,求出最多能留住多少苹果。
输入格式
第一行
接下来
输出格式
一个数,最多能留住的苹果的数量。
说明/提示
解题思路
经分析,本题需要在树上进行动态规划,故使用树形dp。
I.建立树
任何相邻的两个节点没有优先级关系,所以用邻接表储存树,如果把这棵树看做一个图,即是无向无环连通图,此处不用多讲,比较简单实现。
II.dp 随着 dfs 动规整棵树
此题已明确给出了根节点为
状态转移方程:
当前状态可从以子节点
注意:
Code
#include<bits/stdc++.h>
using namespace std;
const int N=1001;
int n,q,a,b,c;
struct Edge {
int v,w,nxt;
}edge[N<<1];
int head[N],idx;
int f[N][N];
void add(int x,int y,int z) {
edge[++idx].v=y;
edge[idx].nxt=head[x];
edge[idx].w=z;
head[x]=idx;
}
void dfs(int u,int fa) {
for(int i=head[u];i;i=edge[i].nxt) {
int v=edge[i].v;
int w=edge[i].w;
if(v==fa) continue;
dfs(v,u);
for(int j=q;j>=0;j--) {
for(int k=0;k<j;k++)
f[u][j]=max(f[u][j],f[u][j-k-1]+f[v][k]+w);
}
}
}
int main() {
scanf("%d%d",&n,&q);
for(int i=1;i<n;i++) {
scanf("%d%d%d",&a,&b,&c);
add(a,b,c);
add(b,a,c);
}
dfs(1,0);
printf("%d\n",f[1][q]);
return 0;
}
树形dp的 n^2 优化
本题如果需要优化时间的话,需要在进行动态规划的循环进行上下界限优化,
(
我在做题一开始没意识到可以优化,所以code程序没有进行优化,大家可以自行修改。
再拿 洛谷P2014 选课 这道经典的树形dp例题来讲,抛开题目,如果学某个课程前不只需要学其他的一个课程,而是一些课程(课程数目
首先上两个都可以过本题的代码,以方便作比较。
Code
#include<bits/stdc++.h>
using namespace std;
const int N=301;
int n,m,a,b[N];
struct Edge {
int v,nxt;
}edge[N<<1];
int head[N],idx;
int f[N][N];
void add(int x,int y) {
edge[++idx].v=y;
edge[idx].nxt=head[x];
head[x]=idx;
}
void dfs(int u) {
f[u][1]=b[u];
for(int i=head[u];i;i=edge[i].nxt) {
int v=edge[i].v;
dfs(v);
for(int j=m+1;j>=0;j--) {
for(int k=0;k<j;k++)
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]);
}
}
}
int main() {
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) {
scanf("%d%d",&a,&b[i]);
add(a,i);
}
dfs(0);
printf("%d\n",f[0][m+1]);
return 0;
}
Code 2 (优化)
#include<bits/stdc++.h>
using namespace std;
const int N=301;
int n,m,a,b[N];
struct Edge {
int v,nxt;
}edge[N<<1];
int head[N],idx;
int f[N][N],sz[N];
void add(int x,int y) {
edge[++idx].v=y;
edge[idx].nxt=head[x];
head[x]=idx;
}
void dfs(int u) {
f[u][1]=b[u];
sz[u]=1;
for(int i=head[u];i;i=edge[i].nxt) {
int v=edge[i].v;
dfs(v);
for(int j=min(m+1,sz[u]+sz[v]);j>=0;j--) {
for(int k=0;k<=min(sz[v],j-1);k++)
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]);
}
sz[u]+=sz[v];
}
}
int main() {
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) {
scanf("%d%d",&a,&b[i]);
add(a,i);
}
dfs(0);
printf("%d\n",f[0][m+1]);
return 0;
}
小总结
通过对比观察两次的数据,可以发现两个代码在用时上有差距,而且这是在
从程序上来看最主要的差别在于进行dp操作的两个循环的范围 :
程序 1 的 main part
for(int j=m+1;j>=0;j--) {
for(int k=0;k<j;k++)
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]);
}
程序 2 的 main part
for(int j=min(m+1,sum[u]+sum[v]);j>=0;j--) {
for(int k=0;k<=min(sum[v],j-1);k++)
f[u][j]=max(f[u][j],f[u][j-k]+f[v][k]);
}
这乍一看,好像没啥差别。
可是你不能小看了这步操作,他可起了很大的作用,光表面说是没用的。
但如果用大佬的一大堆数学结论方法证明,我们这些小蒟蒻是看不懂的 。
那就用最通俗易懂 的方法来解释吧。
我们把这两个循环做的事看作是两组数进行乘法原理的匹配,
此处的
而每次匹配操作都会出现和以前不一样的数对。
当访问完所有结点后,所得出来的全部数对都不重复。
这样时间复杂度就可以转换为了一个组合问题:
再加上原来省略的常数,就接近
大佬的严格证明,感兴趣的数学大佬可以尝试去理解,本蒟蒻就不参与了。
这里的优化省略了常数,其实当 (虽然还是比 。
所以在做树形dp的题型时,我们尽量做到能优化就优化,尽量想办法把时间复杂度从
总结
给定一棵有N个节点的树(通常是无根树,也就是有
在树上设计动态规划算法时,一般就以节点从深到浅(子树从小到大)的顺序作为dp的“阶段”。在状态表示中,第一维通常是节点编号(代表以该节点为根的子树)。
大多数时候,我们采用递归的方式实现树形动态规划。对于每个节点
最主要的题型主要用于:
①.选择节点类
②.树形背包类
③.树的直径类
题库
可以先浅浅练练基础 ,建议可以参考一下我的学习题单 洛谷【树形dp】 ID:970993
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!