树形DP

树形DP没有固定的模板,见多了题,就知道该如何分析,如何设计状态了。所以该篇博客以例题来进行讲解。

树的直径

本题是一道树的直径板子题。
求树的直径有两种方法:
(1)两遍 dfs。(2)树形DP。
时间复杂度都是O(N),但两遍dfs需要满足树中不存在负边。

这里我们用树形DP来解决该题。

思路

首先,选择任意一点为根。
如果一个点是直径上的一点,那树的直径等于从该点出发的最长链+从该点出发的次长链。

设计状态

dp[i][0/1] 分别表示以 i 点为根的子树中从 i 出发的最长链和次长链长度。

状态转移

树形DP一般就是递归由子孙节点的信息去更新祖先节点的信息。
sonj 表示 i 节点的儿子。
因为最长链和次长链不可以重合,所以我们用最长链去更新次长链。
①若 dp[i][0]<dp[sonj][0]+1,则dp[i][1]=dp[i][0]dp[i][0]=dp[sonj][0]+1
②否则,若dp[i][1]<dp[sonj][0]+1,则 dp[i][1]=dp[sonj][0]+1
用代码解释就是:

点击查看代码
if(dp[x][0]<dp[v][0]+1){
    dp[x][1]=dp[x][0];
    dp[x][0]=dp[v][0]+1;
}else if(dp[x][1]<dp[v][0]+1){
    dp[x][1]=dp[v][0]+1;
}

初始化

都为0。

代码

点击查看代码
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
inline int read(){
	int w=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-1') f=-1;
		ch=getchar(); 
	}
	while(ch>='0'&&ch<='9'){
		w=w*10+ch-'0';
		ch=getchar();
	}
	return w*f;
}
int n;
int dp[10005][2],ans;
vector<int> g[10005];
void dfs(int x,int fa){
	for(int i=0;i<g[x].size();i++){
		int v=g[x][i];
		if(v==fa) continue;
		dfs(v,x);
		if(dp[x][0]<dp[v][0]+1){
			dp[x][1]=dp[x][0];
			dp[x][0]=dp[v][0]+1;
		}else if(dp[x][1]<dp[v][0]+1){
			dp[x][1]=dp[v][0]+1;
		}
	} 
	ans=max(ans,dp[x][0]+dp[x][1]);
}
int main(){
	n=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read();
		g[u].push_back(v);
		g[v].push_back(u); 
	}
	dfs(1,0);
	cout<<ans;
	return 0;
}

树的中心

这个蒟蒻没有权限看这道题
题意:一棵树,边有权,找出一个点,使得这个点距离其他点的最大距离最小。

思路

首先,选择任意一点为根。
一个点的最远点的位置有两种情况。
(1)以该点为根的子树内;
(2)以该点为根的子树外。
子树内的好求,和上面那题一模一样。
字数外呢?新设一个变量。

设计状态

dp[i][0/1] 分别表示以 i 点为根的子树中从 i 出发的最长链和次长链长度。
c[i][0/1] 分别表示 dp[i][0/1] 从哪个点转移过来。
u[i] 表示以 i 为根的子树外的距离 i 最远的点。

状态转移

首先一遍树形DP求出 dp[i][0/1] 以及 c[i][0/1]
同上题。
然后,设 pre[i]=xi 的父亲。
①若 c[x][0]!=i,则 u[i]=max(u[x],dp[x][0])+w;
②否则,若 c[x][1]!=i,则 u[i]=max(u[x],dp[x][1]])+w

初始化

都为0。

代码

点击查看代码
#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
inline int read(){
	int w=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		w=w*10+ch-'0';
		ch=getchar();
	}
	return w*f;
}
int n;
int dp[10005][2],c[10005][2],out[10005],ans=0x7fffffff;
vector<pair<int,int> > g[10005];
void dfs1(int x,int fa){
	for(int i=0;i<g[x].size();i++){
		int v=g[x][i].second;
		int w=g[x][i].first;
		if(v==fa) continue;
		dfs1(v,x);
		if(dp[x][0]<dp[v][0]+w){
			c[x][1]=c[x][0];
			dp[x][1]=dp[x][0];
			c[x][0]=v;
			dp[x][0]=dp[v][0]+w;
		}else if(dp[x][1]<dp[v][0]+w){
			c[x][1]=v;
			dp[x][1]=dp[v][0]+w;
		}
	}
} 
void dfs2(int x,int fa){
	for(int i=0;i<g[x].size();i++){
		int v=g[x][i].second;
		int w=g[x][i].first;
		if(v==fa) continue;
		if(c[x][0]!=v) out[v]=max(out[x],dp[x][0])+w;
		else if(c[x][1]!=v) out[v]=max(out[x],dp[x][1])+w;
		dfs2(v,x);
	} 
}
int main(){
	n=read();
	for(int i=1;i<n;i++){
		int u=read(),v=read(),w=read();
		g[u].push_back(make_pair(w,v));
		g[v].push_back(make_pair(w,u));
	}
	dfs1(1,0);
	dfs2(1,0);
	for(int i=1;i<=n;i++) ans=min(ans,max(out[i],dp[i][0]));
	cout<<ans;
	return 0;
} 

没有上司的舞会

思路

先找到根,简化一下题意就是父亲和儿子不能同时被选,每个点只有两种状态,选或不选,且仅影响自己的父亲与儿子。树形DP。其实这里解释的很草率,但这就是个树形DP。可能做多了就有这种感觉了吧

设计状态

dp[i][0/1] 表示分别表示该点不选和选,对于以 i 为根的子树的最大权值。

状态转移

sonji 点的儿子。
i 不选,那么儿子可选可不选:dp[i][0]+=max(dp[sonj][0],dp[sonj][1])
i 选,那么儿子绝对不能选:dp[i][1]+=dp[sonj][0]
最后答案就是 max(dp[root][0],dp[root][1])

初始化

dp[i][0]=0dp[i][1]=w[i]

代码

点击查看代码
#include<iostream>
#include<cstdio>
#include<vector>
#include<cmath>
using namespace std;
inline int read(){
	int w=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		w=w*10+ch-'0';
		ch=getchar();
	}
	return w*f;
}
int n,ha[6005];
bool ind[6005];
vector<int> g[6005];
int f[6005][2];
int roott;
void inp(){
	n=read();
	for(int i=1;i<=n;i++){
		ha[i]=read();
	}
	//每个人的快乐值 
	for(int i=1;i<n;i++){
		int upp,loww;
		loww=read(),upp=read();
		g[upp].push_back(loww);
		ind[loww]=1;
	}
	//建树,ind为入度,用来找根(最大的上司) 
}
void findroott(){
	for(int i=1;i<=n;i++){
		if(!ind[i]){
			roott=i;
			break;
		}
	}
}
//找根 
void recursion(int x){
	f[x][0]=0;
	f[x][1]=ha[x];
	int k=g[x].size(); 
	for(int i=0;i<k;i++){
		int v=g[x][i]; 
		recursion(v);
		f[x][0]+=max(f[v][0],f[v][1]);//自己不去,和自己相连的人可以去也可以不去,取最大值 
		f[x][1]+=f[v][0];//自己去,自己的儿子就不能去 
	}
}
//递归求值 
int main(){
	inp();
	findroott();
	recursion(roott);
	printf("%d",max(f[roott][0],f[roott][1]));
	//输出根去的价值大,还是不去的价值大 
	return 0;
}

保安站岗

思路

以点被谁覆盖进行分类:(1)被自己覆盖;(2)被父亲覆盖;(3)被儿子覆盖。

设计状态

  1. dp[i][0]表示以i为根的整棵子树被覆盖,i被自己覆盖的最小代价
  2. dp[i][1]表示以i为根的整棵子树被覆盖,i被父亲覆盖的最小代价
  3. dp[i][2]表示以i为根的整棵子树被覆盖,i被儿子覆盖的最小代价

状态转移方程

  1. dp[i][0]=son[i]min{dp[son[i]][0],dp[son[i]][1],dp[son[i]][2]}+val[i]

这个比较好想,自己控制了自己,儿子可以被爹,自己,儿子控制,三种情况取最小。最后再加上控制自己的代价。
2. dp[i][1]=son[i]min{dp[son[i]][0],dp[son[i]][2]}

自己被父亲控制,所以儿子要么被自己控制,要么被儿子控制。这里不需要加上自己被父亲控制的代价,我们看看哪里用到了dp[i][1],是不是只有转移dp[i][0]时,但dp[i][0]已经加上了val[i]如果这里也加上自己被父亲控制的代价,就重复计算了。
3. dp[i][2]=son[i]min{dp[son[i]][0],dp[son[i]][2]}+minn

这里就需要注意了,自己被儿子控制(只要有一个儿子控制自己就行),儿子可能自己控制自己,也可能被自己的儿子控制。两者取最小。这里又分出两种情况

  • 如果有任意一个儿子,是自己控制自己的,也就满足了该点可以被儿子控制。不用加minn
  • 如果所有的儿子都是被自己的儿子控制,就无法满足该点被儿子控制,所以至少要找出一个儿子来自己控制自己,为了使答案更小,所以就找minn=dp[son[i]][0]dp[son[i]][2]最小的那个儿子,加上这个值,就满足了该点被其中一个儿子控制了。
    最后答案:
    根节点没有父亲,所以答案就取min{dp[1][0],dp[1][2]}

代码

点击查看代码
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<cstring>
using namespace std;
inline int read(){
	int w=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-') f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		w=w*10+ch-'0';
		ch=getchar();
	}
	return w*f;
}
int n,w[1505],dp[1505][3];
vector<int> g[1505];
void dfs(int x,int fa){
	dp[x][0]=w[x];
	bool mark=0;
	int minn=0x7fffffff;
	for(int i=0;i<g[x].size();i++){
		int v=g[x][i];
		if(v==fa) continue;
		dfs(v,x);
		dp[x][0]+=min(dp[v][1],min(dp[v][2],dp[v][0]));
		dp[x][1]+=min(dp[v][0],dp[v][2]);
		if(dp[v][2]<dp[v][0]){
			dp[x][2]+=dp[v][2];
			minn=min(dp[v][0]-dp[v][2],minn);
		}else{
			dp[x][2]+=dp[v][0];
			mark=1;
		}
	}
	if(mark==0) dp[x][2]+=minn;
}
int main(){
	n=read();
	for(int i=1;i<=n;i++){
		int u=read();
		w[u]=read();
		int x=read();
		while(x>0){
			x--;
			int v=read();
			g[u].push_back(v);
			g[v].push_back(u);
		}
	}
	dfs(1,0);
	printf("%d",min(dp[1][0],dp[1][2]));
	return 0;
}
posted @   Travller  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示