ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

树形DP

2022-08-17 21:31:49  阅读:100  来源: 互联网

标签:DP int st 树形 dp 节点 职员


树形DP,顾名思义,就是在树上设计动态规划。

一般树形DP的DP数组的第一维表示节点编号,代表以此节点为根的子树作为的阶段。除此之外,可能的第二维乃至第三维与题目具体要求相关。
在树上进行动态规划时,一般先用深度优先搜索(\(DFS\))来遍历该树,定义出每个节点的深度与以该节点为根的子树。
而此类动态规划,一般是以节点由深到浅(即由“小子树”到“大子树”)的顺序作为DP阶段的顺序。所以,一般需待该节点的子树遍历完成(即“小子树”完成DP)后,回溯时,进行该节点的状态转移。

举个例子。

某大学有 \(n\) 个职员,编号为 \(1\ldots n\)。
他们之间有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。
现在有个周年庆宴会,宴会每邀请来一个职员都会增加一定的快乐指数 \(r_i\),但是呢,如果某个职员的直接上司来参加舞会了,那么这个职员就无论如何也不肯来参加舞会了。
所以,请你编程计算,邀请哪些职员可以使快乐指数最大,求最大的快乐指数。

题目中说明各职员之间的关系类似于一棵树,那么,就确保所有节点都在同一棵树上,并且代表只有一个节点(即“校长”)没有父节点(即“上司”)。
因此,我们可以在输入时记录每个节点的父节点,之后循环找到没有父节点的点作为根进行遍历,所以输入时可以像有向图一样只记录单向边(父->子)。

遍历解决了,那重点的DP数组该如何定义呢?
这里我们采用二维数组,第一维自然是表示编号,第二维只有1或0两种情况,用于表示该名职员来或不来的情况。
那么DP数组代表的含义也就很明显了:分别在该名员工来与不来的情况下以该节点为根的子树的“快乐指数”的最大值。
为什么要这么定义呢?因为一名职员的来与不来的情况与其上司来或不来有着很大关系,在上司来的情况下,该职员一定不来,而上司不来的情况下,该职员有着来或不来两种选择。
如果只记录当该职员来的情况下以该节点为根的子树的“快乐指数”的最大值,就不能很好由其子节点转移最优解。
接下来,如何转移呢?

若以u代表当前节点,v代表子节点:

  1. dp[u][1]+=dp[v][0](当上司来了,只能由其下属不来的情况转移)
  2. dp[u][0]+=max(dp[v][0],dp[v][1])(当上司不来,可以由其下属来或不来两种情况转移)

最后,只要注意一些细节,这道模板题就解决了。

下面上代码

#include<bits/stdc++.h>
using namespace std;
const int N=6e3+5;
int a[N],fa[N],n,dp[N][2],s;
vector<int>G[N];
void dfs(int x){
	dp[x][1]=a[x];
	for(int i=0;i<G[x].size();i++){
		dfs(G[x][i]);
		dp[x][1]+=dp[G[x][i]][0];
		dp[x][0]+=max(dp[G[x][i]][1],dp[G[x][i]][0]);
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<n;i++){
		int x,y;
		cin>>x>>y;
		fa[x]=y;
		G[y].push_back(x);
	}
	for(int i=1;i<=n;i++)
		if(!fa[i]){dfs(i);cout<<max(dp[i][1],dp[i][0]);break;}
	return 0;
}

除了这种简单的问题外,树形DP还有一些与分组背包有关的题。

上题!!!

有一棵苹果树,如果树枝有分叉,一定是分二叉(就是说没有只有一个儿子的结点)
这棵树共有 \(N\) 个结点(叶子点或者树枝分叉点),编号为 \(1 \sim N\),树根编号一定是 \(1\)。
我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有 \(4\) 个树枝的树:

2   5
 \ / 
  3   4
   \ /
    1

现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。
给定需要保留的树枝数量,求出最多能留住多少苹果。

我们直接上代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e2+5;
int n,q,dp[N][N],fa[N],st[N];
struct node{
	int v,w;
	node(int v=0,int w=0):v(v),w(w){}
};
vector<node>G[N];
void dfs(int x){
	st[x]++;
	for(int i=0;i<G[x].size();i++){
		int v=G[x][i].v,w=G[x][i].w;
		if(fa[x]==v) continue;
		fa[v]=x;
		dfs(v);
		st[x]+=st[v];
		for(int i=st[x]-1;i>=0;i--){
			for(int j=st[v]-1;j>=0;j--){
				if(i>=j+1)
				dp[x][i]=max(dp[x][i],dp[x][i-j-1]+dp[v][j]+w);
			}
		}
	}
}
int main(){
	cin>>n>>q;
	for(int i=1;i<n;i++){
		int x,y,s;
		cin>>x>>y>>s;
		G[x].push_back(node(y,s));
		G[y].push_back(node(x,s));
	}
	dfs(1);
	cout<<dp[1][q];
	return 0;
}

可以看到,其他部分与上题类似,只是普通的\(DFS\)。(顺带一提,fa数组记录父节点,st数组记录子树大小)

直接看到重点

for(int i=st[x]-1;i>=0;i--){
	for(int j=st[v]-1;j>=0;j--){
		if(i>=j+1)
		dp[x][i]=max(dp[x][i],dp[x][i-j-1]+dp[v][j]+w);
	}
}

外层相当于背包中的“体积”,内层相当于分组背包中每组的物品,而分组背包的最外层“种类”,则由\(DFS\)来“代替”。
另外,因为“先有根才有子树”这一隐藏条件,DP数组的每一次转移都需注意只能从子树转移所需体积-1的情况(需留1体积来加上子节点与根之间的边),从而确保“先有根才有子树”。

树形DP的基础基本就是这些,至于“二次扫描与换根法”什么的,待笔者学会后再更新吧。其实就是不会且懒得学就想这么水完

标签:DP,int,st,树形,dp,节点,职员
来源: https://www.cnblogs.com/hh--/p/16589287.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有