【总结】树形DP

本文介绍了树形动态规划(Tree DP)的概念,并详细讲解了三个经典例题:最大独立子集、树的重心和树的直径。对于最大独立子集,通过递归实现DP状态转移;树的重心通过DFS计算子树大小判断;树的直径则通过两次DFS找到最长路径。最后讨论了拓展的两次DFS求直径的方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

树形DP是WHAT?

就是树上的DP呗 :-p

HOW TO DP?

给定一棵有N个节点的树(通常是无根树,也就是有N-1条无向边),我们可以任选一个节点为根节点,从而定义出每个节点的深度和每棵子树的根。

在树上设计动态规划算法时,一般就以节点从深到浅(子树从小到大)的顺序作为DP的“阶段”。DP的状态表示中,第一维通常是节点编号(代表以该节点为根的子树)。大多数时候,我们采用递归的方式实现树形动态规划。对于每个节点x,先递归在它的每个子节点上进行DP,在回溯时,从子节点向节点x进行状态转移。

经典例题

1、最大独立子集:

最大独立子集的定义是,对于一个树形结构,所有的孩子和他们的父亲存在排斥,也就是如果选取了某个节点,那么会导致不能选取这个节点的所有孩子节点。一般询问是要求给出当前这颗树的最大独立子集的大小(被选择的节点个数)。

例题:没有上司的舞会

this

我们设 d p [ i ] [ 1 ] dp[i][1] dp[i][1]表示 i i i要来的情况下以 i i i为根节点的子树的最大快乐值
d p [ i ] [ 0 ] dp[i][0] dp[i][0]表示 i i i不来的情况下以 i i i为根节点的子树的最大快乐值

i i i要来,那么他的每一个子节点都只能不来, d p [ i ] [ 1 ] = ∑ d p [ j ] [ 0 ] , j 为 i 的 子 节 点 dp[i][1]=\sum dp[j][0],j为i的子节点 dp[i][1]=dp[j][0]ji

如果ta不来,那么他的每个子节点就可来可不来, d p [ i ] [ 0 ] = ∑ m a x ( d p [ j ] [ 0 ] , d p [ j ] [ 1 ] ) , j 为 i 的 子 节 点 dp[i][0]=\sum max(dp[j][0],dp[j][1]),j为i的子节点 dp[i][0]=max(dp[j][0],dp[j][1]),ji

我们需要从每个叶子节点向上不断跟新dp,直到求到根节点,这一步我们可以用递归的方式,对于每一个节点,先递归他的每一个子节点,再在回溯时执行更新操作

最终的答案就是看根节点是去还是不去, m a x ( d p [ r o o t ] [ 1 ] , d p [ r o o t ] [ 0 ] ) max(dp[root][1],dp[root][0]) max(dp[root][1],dp[root][0])

Code:

#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
const int MAXN=6005;
int h[MAXN];
vector<int> son[MAXN];
bool flag[MAXN];
int dp[MAXN][5]; 
int Max(int a,int b){
	return a<b?b:a;
}
int Min(int a,int b){
	return a<b?a:b;
}
void dfs(int x){
	dp[x][0]=0;
	dp[x][1]=h[x];
	for(int i=0;i<son[x].size();i++){
		int y=son[x][i];
		dfs(y);
		dp[x][0]+=Max(dp[y][0],dp[y][1]);
		dp[x][1]+=dp[y][0];
	}
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&h[i]);
	}
	for(int i=1;i<=n-1;i++){
		int l,k;
		scanf("%d %d",&l,&k);
		son[k].push_back(l);
		flag[l]=1;
	}
	int root;
	for(int i=1;i<=n;i++){
		if(!flag[i]){
			root=i;
			break;
		}
	}
	dfs(root);
	printf("%d",Max(dp[root][1],dp[root][0]));
return 0;
}

2、树的重心

this
树的重心定义为,当把节点x去掉后,其最大子树的节点个数最少(或者说成最大连通块的节点数最少),那么节点x就是树的重心。
通俗的理解:这个点去掉后,剩下的联通块尽量平均

算法:

树上任选一结点 u u u 开始 DFS,沿路统计其所有子树的大小和以它为根的子树的大小,这样也就可以得到一个点所有子树的大小及的大小

还有一个问题,如果这个点不是根节点,那么删掉后剩下的连通块不止有他的子树,还有他上面的部分,这部分可以由总结点减去他自己的子树大小得到

我们知道一个性质:删除重心后所得的所有子树,节点数不超过原树的1/2,那么我们使用这个性质来判断一个点是否为重心即可

代码:

#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
const int MAXN=105;
vector<int> g[MAXN];
int dp[MAXN];
bool vis[MAXN];
int ans[MAXN];
int len=0;
int n;
void dfs(int x,int to){
	dp[x]=1;
	bool flag=true;
	for(int i=0;i<g[x].size();i++){
		int y=g[x][i];
		if(!vis[y]){
			vis[y]=true;
			dfs(y,x);
			dp[x]+=dp[y];
			if(dp[y]>n/2){
				flag=false;
			}
		}
	}
	if(n-dp[x]>n/2){
		flag=false;
	}
	if(flag){
		ans[++len]=x;
	}
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n-1;i++){
		int u,v;
		scanf("%d %d",&u,&v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	dfs(1,-1);
	printf("%d\n",len);//有多少个重心
	
	for(int i=1;i<=len;i++){
		printf("%d\n",ans[i]);//输出每一个重心
	}
return 0;
}

3、树的直径

this
给定一棵树,树中每条边都有一个权值,树中两点之间的距离定义为连接两点的路径边权之和。树中最远的两个节点(两个节点肯定都是叶子节点)之间的距离被称为树的直径,连接这两点的路径被称为树的最长链。后者通常也可称为直径。

算法:
不妨设1号节点为根,“N个节点,N-1条边的无向图”就可以看作“有根树”。设d[x]表示从节点x出发走向以x为根的子树,能够到达最远节点的距离。设x的子节点为y1,y2,……,yt,edge(x, y)表示边权,显然有:

d[x] = max{d[yi] + edge(x, yi)} (1≤i≤t)

接下来,我们可以考虑对每个节点x求出“经过节点x的最长链的长度”ans[x],整棵树的直径就是:

max{ans[x]} (1≤x≤n)

那么,如何求ans[x]呢?我们用链式前向星存图,依次遍历以x为起点的所有连边,用已经更新过的d[x]与没有枚举到的x的连边之和来更新ans[x]:

ans[x] = max(ans[x], d[x] + d[yi] + edge(x, yi))

然后再不断更新

d[x] = max(d[x], d[yi] + edge(x, yi))

注意ans[i]存的是经过i节点的最长链,最后还要便利一遍ans才能求到真正的直径
而我们的ans数组其实可以用一个整型数ans代替,就不需要后面再更新一遍了

ans = max(ans, d[x] + d[yi] + edge(x, yi))

关键代码:

void dp(int x){
	vis[x]=1;
	for(int i=head[x];i;i=edge[i].next){
		int y=edge[i].to;
		if(vis[y]) continue;
		dp(y);
		ans=max(ans,d[x]+d[y]+edge[i].dis);
		d[x]=max(d[x],d[y]+edge[i].dis);
	}
} 

拓展:两次DFS求直径

在树上任选一节点 u u u ,通过搜索求得距离它最远的点 x x x ,再从点 x x x 出发通过搜索得到距离它最远的点 y y y x x x y y y 的路径即为这棵树的直径。

#include<cstdio>
#include<iostream>
#include<vector>
using namespace std;
const int MAXN=100005;
vector<int> g[MAXN];
int point,maxn;
void dfs(int now,int fa,int step){
	if(step>maxn){
		maxn=step;
		point=now;
	}
	for(int i=0;i<g[now].size();i++){
		int v=g[now][i];
		if(v!=fa){
			dfs(v,now,step+1);
		} 
	}
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n-1;i++){
		int u,v;
		scanf("%d %d",&u,&v);
		g[u].push_back(v);
		g[v].push_back(u);
	}
	maxn=-1;
	dfs(1,0,0);
	maxn=-1;
	dfs(point,0,0);
	printf("%d",maxn);
return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值