SDSC整理(Day5 树形dp)

树形dp

我才不会告诉你这是第六天讲的

\(pre-knowledge\)

没啥前置知识,递归算吗?存图算吗?简单dp算吗?

\(description\)

树形\(dp\),顾名思义,就是在树上进行\(dp\)

因为树的定义本身就具有递归性,而且树有很明显的层次性,必须求出所有的子节点之后才可以转移到父节点。

所以树形 \(dp\) 一般采用递归来实现。

而且树形 \(dp\) 的第一维通常都是子树的规模,算是比较套路的一点。

下面讲 \(4\) 个树形 \(dp\) 的典型例子。

1.最大独立集问题

例题:P1352 没有上司的舞会

很明显,职员与上司之间形成了一棵有根树,规则是选了儿子就不能选父亲,选了父亲就不能选儿子,要求选的点的权值和最大,这类问题被称为最大独立集问题。

我们设 \(dp[i][0/1]\) 表示以 \(i\) 为根的子树中所能选的最大值, \(0\) 代表该节点选, \(1\) 代表该节点不选。

如果我们选了父亲,那么我们就一定不能选儿子。

如果我们没有选父亲,那么儿子可选可不选,取最大值。

所以我们有:

\[dp[fa][0]=\sum\max(dp[son][1],dp[son][0]) \]

\[dp[fa][1]=\sum dp[son][0] \]

\(dfs\) 到叶子节点之后,在递归回退的时候从下往上转移。

/*
	树形dp(求最大独立集问题)
	date:2022.7.31
	worked by respect_lowsmile 
*/
#include<iostream>
#include<algorithm>
using namespace std;
const int N=6e3+5;
struct node
{
	int to,next;
};
node edge[N<<2];
int dp[N][2],val[N],fa[N],head[N];
int n,r=1,num;
void add(int u,int v)
{
	num++;
	edge[num].to=v;
	edge[num].next=head[u];
	head[u]=num;
}
void dfs(int now)
{
	dp[now][0]=0,dp[now][1]=val[now];
	for(int i=head[now];i;i=edge[i].next)
	{
		int v=edge[i].to;
		if(v)  dfs(v);
		dp[now][0]+=max(dp[v][1],dp[v][0]);
		dp[now][1]+=dp[v][0];
	}
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
		scanf("%d",&val[i]);
	for(int i=1;i<n;++i)
	{
		int u,v;
		scanf("%d %d",&u,&v);
		add(v,u),fa[u]=v;
	}
	while(fa[r])  r=fa[r];
	dfs(r);
	printf("%d",max(dp[r][0],dp[r][1]));
	return 0;
}

2.最大独立集问题+唯一性判断。

例题:UVA1220 Hali-Bula的晚会 Party at Hali-Bula

这题我写题解了,详情见题解

link

我再不要脸地要个赞

3.背包类树形 \(dp\) (点权)

例题:P2014 [CTSC1997] 选课

简单描述就是一共选 \(m\) 个点,选择顺序满足树的层次性,要求选的点的点权和最大。

我们设 \(dp[i][j]\) 表示以 \(i\) 为根的子树中选 \(j\) 个节点所能得到的最大得分。

如果一棵子树能选 \(j\) 个点,一个孩子选了 \(k\) 个点,那么这个子树内只能再选 \(j-k\) 个点,其中 \(0 \le j \le m\)\(0 \le k \le j-1\)

所以我们有状态转移方程:

\[dp[fa][j]=\max(dp[fa][j-k],dp[son][k]) \ (0 \le j \le m,0 \le k \le j-1) \]

我们枚举 \(j\)\(k\) ,转移即可。

注意,因为每个节点只能选一次,所以我们要保证转移唯一,所以枚举 \(j\) 的时候要倒着枚举。

是不是像极了 \(01\) 背包???

tips

因为每一个子树的节点个数都有限,有可能枚举的 \(j\)\(k\) 会大于当前子树的节点数,

特别是 \(j\) 或者 \(k\) 很大的时候,会产生很多没有必要的枚举,

所以我们可以处理一个 \(siz[]\) 来处理每棵子树的节点数,然后枚举的边界设为 \(min(siz[now],m)\)\(min(siz[son],k)\)

/*
	树形dp
	date:2022.8.1
	worked by respect_lowsmile
*/
#include<iostream>
#include<vector>
using namespace std;
const int N=305;
int dp[N][N],val[N],siz[N];
vector<int> E[N];
int n,m;
void dfs(int now)
{
	siz[now]=1,dp[now][1]=val[now];
	for(int i=0;i<E[now].size();++i)
	{
		int v=E[now][i];
		dfs(v);
		siz[now]+=siz[v];
		for(int j=min(siz[now],m);j>=1;--j)
			for(int k=0;k<=min(siz[v],j-1);++k)
				dp[now][j]=max(dp[now][j],dp[v][k]+dp[now][j-k]);
	}
}
int main()
{
	scanf("%d %d",&n,&m);
	m++;
	for(int i=1;i<=n;++i)
	{
		int k;
		scanf("%d %d",&k,&val[i]);
		E[k].push_back(i);
	}
	dfs(0);
	printf("%d",dp[0][m]);
	return 0;
}

4.背包类树形dp(边权)

例题:P2015 二叉苹果树

其实和点权的差不多。

我们用\(dp[i][j]\)表示以\(i\)为根的子树中选\(j\)条边能获得的最大值。

和上面的那个题一个套路,如果一棵子树能选 \(j\) 条边,一个孩子选了 \(k\) 条边,那么这个子树内只能再选 \(j-k-1\) 条边,因为该节点和它的儿子之间也连了一条边,其中 \(0 \le j \le m\)\(0 \le k \le j-1\)

我们有状态转移方程:

\[dp[fa][j]=\max(dp[fa][j],dp[son][k]+dp[fa][j-k-1]+w[fa,son])\ (0 \le j \le m,0 \le k \le j-1) \]

/*
	树形dp(最近在练vector,所以用vector存图比较多)
	date:2022.8.1
	worked by respect_lowsmile
*/
#include<iostream>
#include<vector>
using namespace std;
const int N=105;
struct node
{
	int to,w;
};
int dp[N][N],siz[N];
int n,m;
vector<node> E[N];
void dfs(int now,int fa)
{
	siz[now]=1;
	for(int i=0;i<E[now].size();++i)
	{
		int v=E[now][i].to,w=E[now][i].w;
		if(v==fa)  continue;
		dfs(v,now);
		siz[now]+=siz[v];
		for(int j=min(siz[now],m);j>=1;j--)
			for(int k=0;k<=min(j-1,siz[v]);++k)
				dp[now][j]=max(dp[now][j],dp[now][j-k-1]+dp[v][k]+w);
	}
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<n;++i)
	{
		int u,v,w;
		scanf("%d %d %d",&u,&v,&w);
		E[u].push_back(node{v,w});
		E[v].push_back(node{u,w});
	}
	dfs(1,0);
	printf("%d",dp[1][m]);
	return 0;
}

换根法

不会。。。

留坑

posted @ 2022-08-08 20:54  respect_lowsmile  阅读(38)  评论(0编辑  收藏  举报