树形DP

 树形DP即在树上的DP

主要是把儿子的最优解传给父亲

与普通DP一样,也分几步

1.确定状态->第一维是节点,根据实际情况考虑加不加维

2.状态转移

3.边界条件->边界一般是叶子节点吧

4.答案dp[根][...]

以下部分来源于大佬Faithfully_lyl的博客


类型1:分左右两个儿子情况 

二叉苹果树

有一棵苹果树,它是一棵二叉树,共N个节点,树节点编号为1~N,编号为1的节点为树的根,边可理解为树的分枝,每个分支都长着若干个苹果,现在要减去若干个分支,保留M个分支,使得这M个分支中的苹果数量最多。

 

 样例输入:

5 2 
1 3 1 
1 4 10 
2 3 20 
3 5 20

样例输出

21

分析

我们发现,可以分为以下3种情况

1.树根的左子树为空,全部保留右子树,右子树中保留j-1
个结点;
2.树根的右子树为空,全部保留左子树,左子树中保留j-1个
结点;
3.树根的两棵子树非空,设左子树保留k个结点,则右子树
保留j-k-1个结点。

我们取这3种情况的最大值

保留k条边就是k+1个节点

于是定义状态f[节点][呃...]

经过我多年的观察和考证

一般第一行有几个变量,状态就是几维(数位dp除外),例如本题,状态定义应该还有一个保留节点的个数

于是答案就是f[1][k+1]

在例如背包,第一行一个总容量,一个总物品量,于是答案就是f[总容量][种物品量]

而这道题,确实这两个变量就足够了

转移方程很容易写出

f[i,j]=max{f[ch[i,0],k]+f[ch[i,1],j-k-1]+a[i]}(0<=k<=j-1)

边界条件,若为叶子节点,f[i][j]=a[i]

#include<bits/stdc++.h>
#define N 105
using namespace std;
int child[N][2],father[N],a[N],f[N][N];
int first[N],next[N],to[N],w[N],tot;
int n,m;
void add(int x,int y,int z)
{
	next[++tot]=first[x];
	first[x]=tot;
	to[tot]=y;
	w[tot]=z;
}
void dfs(int cur,int fa)
{
	for(int i=first[cur];i;i=next[i]){
		int t=to[i];
		if(t==fa) continue;
		a[t]=w[i],father[t]=cur;
		if(child[cur][0]) child[cur][1]=t;
		else child[cur][0]=t;
		dfs(t,cur);
	}
}
int dp(int i,int j)
{
	if(i==0||j==0) return 0;
	if(f[i][j]) return f[i][j];
	if(!child[i][0]) return a[i];
	for(int k=0;k<=j-1;k++)
	   f[i][j]=max(f[i][j],dp(child[i][0],k)+dp(child[i][1],j-k-1)+a[i]);
	return f[i][j];
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n-1;i++){
		int x,y,z;
		cin>>x>>y>>z;
		add(x,y,z);
	}
	dfs(1,0);
	cout<<dp(1,m+1);
	return 0;
}

类型二:普通树上的dp

普通树和二叉树的区别是啥子嘞,

就是说二叉树里一个结点下最多只有两个儿子结点,所以状态比较好转移(从儿子向上走)。但普通树就不止两个了,可能有多个,那就不好玩了╭(╯^╰)╮。可是,我们可以把普通树转成二叉树啊,一样的dp,熟悉的感觉~

转化的口诀(原则):左儿子,右兄弟

就是说当前结点的第一个儿子,放在它的左子树,其余的儿子挂在第一个儿子的右子树上。

那么对于每个结点而言,它左边的才是它真正的儿子,右边都是它兄弟(同一个父亲)

(大家凑合看看,然后自己试着转转)

如何检验是否转对了?

1.每个结点的信息没有改变(比如其父亲结点,儿子结点,还有的就因题而异了)

2.可以从二叉树再转回去(基本上这条如果做到了,就没什么大问题了)

 

普通树已经转成二叉树了,接下来就好办多了

代码

for(int i=1;i<=n;i++){
    	int fa=father[i];
    	if(!son[fa]) child[fa][0]=i;//如果父亲还没有儿子,他的左儿子是i
    	else child[son[fa]][1]=i;//否则他的其他儿子接在他儿子的右儿子上
    	son[fa]=i;//更新他的最后一个儿子
}

例题

选课

题目描述

学校实行学分制。每门的必修课都有固定的学分,同时还必须获得相应的选修课程学分。学校开设了 N(N<300)门的选修课程,每个学生可选课程的数量 M 是给定的。学生选修了这M门课并考核通过就能获得相应的学分。
在选修课程中,有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其它的一些课程的基础上才能选修。例如《Frontpage》必须在选修了《Windows操作基础》之后才能选修。我们称《Windows操作基础》是《Frontpage》的先修课。每门课的直接先修课最多只有一门。两门课也可能存在相同的先修课。每门课都有一个课号,依次为1,2,3,…。 例如:

课号先修课号学分
11
211
323
43
524

表中 1 是 2 的先修课,2 是 3、4 的先修课。如果要选 3,那么 1 和 2 都一定已被选修过。

你的任务是为自己确定一个选课方案,使得你能得到的学分最多,并且必须满足先修课优先的原则。假定课程之间不存在时间上的冲突。

输入格式

输入文件的第一行包括两个整数 N、M(中间用一个空格隔开)其中 1≤N≤300,1≤M≤N。
以下N行每行代表一门课。课号依次为 1,2,…,N。每行有两个数(用一个空格隔开),第一个数为这门课先修课的课号(若不存在先修课则该项为0),第二个数为这门课的学分。学分是不超过 10 的正整数。

输出格式

输出文件只有一个数,实际所选课程的学分总数。

样例输入

7 4 
2 2 
0 1 
0 4 
2 1 
7 1 
7 6 
2 2

样例输出

13

“每门课的直接先修课最多只有一门。”这句话告诉我们每门科只会对应一个父亲

“两门课也可能存在相同的先修课。"这是在说明这是一棵普通树,每个节点下不一定只有两个儿子

这种无先修课的就是根节点啦,而无先修课的课不止一门,所以这是一个森林~(但和普通树转二叉树没什么区别,直接选中一棵树的根节点作为森林的根节点,然后左儿子,右兄弟)

然后对于每个节点而言,如果选他的左儿子,那么该节点必选,但如果选他的右儿子,就不一定要选根了(因为他们是兄弟关系呗)根据这个来状态转移,就和上一题类似了

#include<bits/stdc++.h>
#define N 305
using namespace std;
int n,m,child[N][2],father[N],son[N],f[N][N];
int first[N],next[N],to[N],tot,a[N];
bool bj=false;
void add(int x,int y)
{
	next[++tot]=first[x];
	first[x]=tot;
	to[tot]=y;
}
void dfs(int cur,int fa)
{
	for(int i=first[cur];i;i=next[i]){
		int t=to[i];
		if(t==fa) continue;
		father[t]=cur;
		dfs(t,cur);
	}
}
int dp(int i,int j)
{
	if(f[i][j]) return f[i][j];
	if((i==0&&bj)||j==0) return 0;
	if(!child[i][0]&&!child[i][1]) return a[i];
	bj=true;
	if(child[i][1]) f[i][j]=dp(child[i][1],j);
	for(int k=0;k<=j-1;k++)
		f[i][j]=max(f[i][j],dp(child[i][0],k)+dp(child[i][1],j-k-1)+a[i]);
	return f[i][j];
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++){
	   int y,z;
	   cin>>y>>z;
	   a[i]=z,add(y,i);
    }
    dfs(0,-1);
    for(int i=1;i<=n;i++){
    	int fa=father[i];
    	if(!son[fa]) child[fa][0]=i;
    	else child[son[fa]][1]=i;
    	son[fa]=i;
    }
    cout<<dp(0,m+1);
    return 0;
}

类型3:树的直径

描述

树的直径,即这棵树中距离最远的两个结点的距离。每两个相邻的结点的距离为1,即父亲结点与儿子结点或儿子结点与父子结点之间的距离为1.有趣的是,从树的任意一个结点a出发,走到距离最远的结点b,再从结点b出发,能够走的最远距离,就是树的直径。树中相邻两个结点的距离为1。你的任务是:给定一棵树,求这棵树中距离最远的两个结点的距离。

输入

输入共n行 第一行是一个正整数n,表示这棵树的结点数 接下来的n-1行,每行三个正整数a,b,w。表示结点a和结点b之间有一条边,长度为w 数据保证一定是一棵树,不必判错。

输出

输出共一行 第一行仅一个数,表示这棵树的最远距离

样例输入

4
1 2 10
1 3 12
1 4 15

样例输出

27

100%的数据满足1<=n<=10000 1<=a,b<=n 1<=w<=10000

还是和我们之前一样,分析每一个点,发现最长链与它的关系那么我们状态就好定义了,而且我们也知道,某子树的直径是由它的最长链和次长链组成的。那么我们就用d1[i]表示以i为根的子树中最长链的长度,d2[i]表示以i为根的子树中次长链的长度。而对于1号节点而言,最长链要么经过它,要么不经过它。但反正求的ans总是取的max,所以不用单独弄出来搞

#include<bits/stdc++.h>
#define N 10005
using namespace std;
int n,a,b,w;
int ans=-1;
struct node{int v,len;};
vector<node> f[N];
int l1[N],l2[N];
void dfs(int u,int fa){
	int i,j,k;
	for(i=0;i<f[u].size();++i){//从0开始!!!
		int v=f[u][i].v,len=f[u][i].len;
		if(v==fa) continue;
		dfs(v,u);
		if(l1[v]+len>l1[u]) l2[u]=l1[u],l1[u]=l1[v]+len;
		else if(l1[v]+len>l2[u]) l2[u]=l1[v]+len;
	}
	ans=max(ans,l1[u]+l2[u]);
}
int main(){
	scanf("%d",&n);
	int i,j,k;
	for(i=1;i<n;++i)
	{
		scanf("%d%d%d",&a,&b,&w);
		f[a].push_back((node){b,w});
		f[b].push_back((node){a,w});
	}
	dfs(1,0);
	printf("%d",ans);
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FSYo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值