树形DP·总结

18 篇文章 0 订阅

前言

最近做了些树形DP的基础题目,不是特别难,最难的是道紫题……
然后为了让自己以后更好地回忆,也是现在地一种梳理,所以我还是点开了“创作中心”,开始总结。


目录


首先,个人感觉树形DP题目常见的两大类,一种是关乎子树的,一种是背包。
感觉其一更好打一些,不过也不是没有难题。
背包的话,就是注意细节,将树上的DP慢慢推导成普通的几种背包即可。


子树类

以下来几道子树类的例题,一边记录,一边讲解总结。

没有上司的晚会

说到子树类的DP,就不得不说树形DP入门题,“没有上司的晚会
这道题的感觉给人焕然一新的感觉。(因为这是我第一次接触树形DP

我们可以将上下属的关系转化成一棵树:上属就是下属的父节点,这样就可以构成一棵简单的关系树了。

题目的解法也是很简单:我们只需要设 f i , 0 f_{i,0} fi,0为第i个人不去晚会的最大快乐值,反之 f i , 1 f_{i,1} fi,1为第i个人参加晚会的最大快乐值。然后在递归便利这棵树中实现,最终的答案就是 m a x ( f r o o t , 0 , f r o o t , 1 ) max(f_{root,0},f_{root,1}) max(froot,0,froot,1),其中(root为这棵树的根节点

所以,我们可以知道,根据题目描述:
一个人去,那他的下属就不能去。
一个人不去,那他的下属可以不去也可以去。

所以转移方程显而易见(其中 s o n i , j son_{i,j} soni,j表示i的第j个儿子:
f i , 0 f_{i,0} fi,0+= m a x ( f s o n i , j , 0 , f s o n i , j , 1 ) max(f_{son_{i,j},0},f_{son_{i,j},1}) max(fsoni,j,0,fsoni,j,1)
f i , 1 f_{i,1} fi,1+= f s o n i , j , 0 f_{son_{i,j},0} fsoni,j,0

CODE

#include<bits/stdc++.h>
using namespace std;
int n,x[6005],son[6005][6005],f[6005][2],p,q,root;
bool bz[6005];
int max(int a,int b)
{
	if(a>b) return a;
	else return b;
}
void dg(int k)
{
	f[k][0]=0,f[k][1]=x[k];
	for(int i=1;i<=son[k][0];++i)
	{
		dg(son[k][i]);
		f[k][0]+=max(f[son[k][i]][0],f[son[k][i]][1]);
		f[k][1]+=f[son[k][i]][0];
	}
	return ;
}
int main()
{
	memset(bz,false,sizeof(bz));
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
		scanf("%d",&x[i]);
	scanf("%d%d",&p,&q);
	while(p!=0&&q!=0) son[q][++son[q][0]]=p,bz[p]=true,scanf("%d%d",&p,&q);
	for(int i=1;i<=n;++i)
		if(bz[i]==false)
		{
			root=i;
			break;
		}
	dg(root);
	printf("%d",max(f[root][0],f[root][1]));
	return 0;
}
Tips 1
  1. 一般来说,建树的时候,如果没有特殊要求,祖先节点是那个没有上属(父节点)的节点。但有的时候,如果是双向边,那么一般考虑以 1 1 1为根节点。但是,还有可能出现多棵树DP的情况,这时候我们就要考虑建一个虚点,以做方便做DP。

最大子树和

最大子树和,这来起看一道也较为简单的树形DP,不过其所有的DP部分似乎就不是那么多了。

我们照样可以照着题意考虑。
题目只需要一种留下一株花的最大答案,并没有多余的限制。
所以,我们可以边做DP边更新我们的答案(是不是有点像记忆化搜索了?

我们顺水推舟设DP的状态:
f i f_{i} fi为以i为根,剩下花卉的美丽值之和。
但是,我们看到题目还要知道,如果一朵花太丑了以至于他的美丽值是个负数,我们也不能留他。这样做出的处理是,每次得到一个子树的DFS下的DP值,与0作比较。
所以,式子就是: f i f_{i} fi+= m a x ( f s o n i , 0 ) max(f_{son_{i}},0) max(fsoni,0),其中有着我们上面提到的处理细节。
在这道题,我们就要用到前面提到的对双向边的普通处理,将1默认为根节点
然后,每次做完一个节点的值,就要做出与最终答案ans的比较以更新答案。

CODE

#include<cstdio>
#include<cstring>
#define N 100005
using namespace std;
int n,u,v,ans=0;
int a[N],f[N];
int head[N],to[N],next[N],tot=0;
int max(int x,int y){if(x>y)return x;return y;}
void add(int x,int y)
{
	to[++tot]=y,next[tot]=head[x],head[x]=tot;
	to[++tot]=x,next[tot]=head[y],head[y]=tot;
}
void dfs(int x,int last)
{
	f[x]=a[x];
	for(int i=head[x];i;i=next[i])
		if(to[i]!=last) dfs(to[i],x),f[x]+=max(f[to[i]],0);
	ans=max(ans,f[x]);
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]);
	for(int i=1;i<n;++i) scanf("%d%d",&u,&v),add(u,v);
	dfs(1,0),printf("%d",ans);return 0;
}
Tips 2&3
  1. DP有时候就是一个辅助我们完成题目找到最终答案的一个过程,不要只是死磕硬撞拼命往DP想,如果一段时间没有结果,试着让自己的思路从DP中解脱出来。这样,可能会更快地AC。

  2. 链式前向星是真的香啊,速度快空间少,不过吐槽一个这道题的数据为什么我照着 n ≤ 16000 n\le16000 n16000来打,结果MLE 70。结果前向星数组开到100000就过了。这说明,前向星还是很重要的。数组还是要尽量开大点。


再搞最后一道子树类的例题。
这种题也是很常见的,那就是战略游戏

战略游戏

看到题目,可能会有点小紧张(我是在模拟赛中接触这道题的
因为这和我之前做的寥寥无几的树形DP不一样了。
所以要重新思考。

但稍微思考后,我们就可以发现,这题和前面的“没有上司的晚会”很像。
因为,如果一个点我放置了一个士兵,我的子节点可以放也可以不放。
反之,如果我不放,那么子节点就必须要放。

因为,对于一个节点所连接的一条边,和一个子节点连出的边是唯一的。通俗一点就是,对于一条边,只能由自己和自己的子节点来覆盖。
所以我们就可以设状态了:
f i , 1 f_{i,1} fi,1i这个节点放置士兵的最小答案。
f i , 0 f_{i,0} fi,0i这个节点不放置士兵的最小答案。
显然易得,转移方程:

f i , 1 f_{i,1} fi,1+= m i n ( f s o n i , j , 0 , f s o n i , j , 1 ) min(f_{son_{i,j},0},f_{son_{i,j},1}) min(fsoni,j,0,fsoni,j,1)
因为答案要求最小,所以这里取小值

f i , 0 f_{i,0} fi,0+= f s o n i , j , 0 f_{son_{i,j},0} fsoni,j,0
和“晚会”一样,我们这里要用到的是找根节点root,最终答案即是
m i n ( f r o o t , 0 , f r o o t , 1 ) min(f_{root,0},f_{root,1}) min(froot,0,froot,1)
CODE

#include<bits/stdc++.h>
using namespace std;
const int inf=9999;
int n,x,son[1505][1505],root,f[1505][2];
bool bz[1505];
void dg(int x)
{
	f[x][1]=f[x][0]=0;
	for(int i=1;i<=son[x][0];++i)
	{
		dg(son[x][i]);
		f[x][0]+=f[son[x][i]][1];
		f[x][1]+=min(f[son[x][i]][1],f[son[x][i]][0]);
	}
	f[x][1]++;
	return ;
}
int main()
{
	memset(bz,false,sizeof(bz));
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
	{
		scanf("%d",&x);
		scanf("%d",&son[x][0]);
		for(int j=1;j<=son[x][0];++j)
		{
			scanf("%d",&son[x][j]);
			bz[son[x][j]]=true;
		}
	}
	for(int i=0;i<n;++i)
		if(bz[i]==false)
		{
			root=i;
			break;
		}
	dg(root);
	printf("%d",min(f[root][0],f[root][1]));
	return 0;
}

Tips 4

在遇到不会的题目时,要融会贯通,多去思考。可以尝试去从平时做题的时候来寻找可能的答案。
这也告诉了自己,平时做题的时候要多多总结,好好消化,加深记忆,这样算法才能在比赛时更好地为己所用。


UP 2021.1.30 11:06
GDKOI成绩出了,等级出了。
然后压线优良……感觉自己真的考得差了。以后多多总结,努力消化。
直接开始总结背包树形DP


背包类

树上背包其实可以从一般背包转移过来,看到题目,抓住关键字,感觉是树。然后再看到题面,知道是DP以后,开始看是那种。
如果是背包,那就凭做题经历来判断是那种背包,并往上靠就好了。
还是来几道例题,易于总结理解。


选课

选课,一道入门的乱切题,然后我搞了3个小时。
理解消化一番后,感觉自己的树形DP终于初碰皮毛了。
按照前面的思路,我们可以判断出这题就是01背包。

然后照着设状态,
我们可以设 f i , j f_{i,j} fi,j为对于第i选了j节棵的最大学分。
那么,显然我们可以将原始的状态初始化为:
f s o n i , j f_{son_{i},j} fsoni,j= f i , j f_{i,j} fi,j+ v a l s o n i val_{son_{i}} valsoni
其中, v a l i val_{i} valii的学分。

所以,在完成这个初始化以后,我们就可以递归,求子树的解。
其中的状态转移方程为:
f i , j f_{i,j} fi,j= m a x ( f s o n i , j − 1 , f i , j ) max(f_{son_{i},j-1},f_{i,j}) max(fsoni,j1,fi,j)即可。
因为我们这里的节点可以没有先修课,然后在没有先修课的情况下输入是为0的。
我们就可以依照着这样设下去,直接将0为根节点即可。

CODE

#include<cstdio>
using namespace std;
int m,n;
int from,val[1005],f[1005][1005];
int to[1005],next[1005],head[1005],tot=0;
int max(int x,int y){if(x>y) return x;return y;}
int min(int x,int y){if(x>y) return y;return x;}
void add(int u,int v){to[++tot]=v,next[tot]=head[u],head[u]=tot;}
void dfs(int x,int y)
{
	if(y<=0) return ;
	for(int i=head[x];i;i=next[i])
	{
		for(int j=0;j<y;++j) f[to[i]][j]=f[x][j]+val[to[i]];
		dfs(to[i],y-1);
		for(int j=1;j<=y;++j) f[x][j]=max(f[x][j],f[to[i]][j-1]);
	}
}
int main()
{
	scanf("%d%d",&m,&n);
	for(int i=1;i<=m;++i)
	{
		scanf("%d%d",&from,&val[i]);
		add(from,i);
	}
	dfs(0,n);
	printf("%d",f[0][n]);
	return 0;
}

背包类就总结到这里(主要是题目的难易主要决定在背包的使用,反而没有树的味道了。

然后,就结束吧。
end 2021.1.30 11:28

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值