树形 DP

15 篇文章 0 订阅
6 篇文章 0 订阅

简单入门题

P1352 没有上司的舞会

一、dfs方法

普通dp一般用两个for循环,但是树形dp的基本操作就是将一个结点所有子树的信息合到这个节点上,我们的for循环显然就不能满足这个要求。我们采取dfs的方法。

很容易发现树形dp它为什么一般会是dfs形式?因为树形dp的状态大多是一颗颗子树,它传递状态过程一般都是先求出最下层再往上更新。所以对于每一个点,我们在求解它的值的过程中,需要求出它每一个子节点的解。

#include <iostream>
#include <vector>
using namespace std;
const int MAX=6005;
int h[MAX]; 
int vis[MAX];
int dp[MAX][2];
vector<int> son[MAX];//son[x][i],值为x的节点的儿子们 
void dfs(int x){
	dp[x][0]=0;
	dp[x][1]=h[x];//x号职员去或不去的初始快乐值
	for(int i=0;i<son[x].size();i++){
		int s=son[x][i];
		dfs(s);
		dp[x][0]+=max(dp[s][0],dp[s][1]);//x不去,下属去或不去的最大值
		dp[x][1]+=dp[s][0]; 
	} 
	
}
int main(){
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>h[i];
	} 
	int x,y;//下属,上司 
	for(int i=1;i<=n-1;i++){//只有n-1个职员有公司 
		cin>>x>>y;
		son[y].push_back(x);
		vis[x]=1;//没有上司的是根节点 
	}
	int root;
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			root=i;
			break;
		}
	} 
	//判断根节点方法可多种了,但都是判断是否做别人儿子的演化
	//是否做别人儿子vis数组、是否有父亲fa【x]!=x、入度是否为0
	dfs(root);
	cout<<max(dp[root][0],dp[root][1]);
    return 0;
} 

链式前向星(head数组初始化为0好了,不用特意memset成-1,遍历时终止条件直接i省的~i

#include <iostream>
//#include <vector>
#include<string.h>
using namespace std;
const int MAX=6005;
int h[MAX]; 
int vis[MAX];
int dp[MAX][2];
//vector<int> son[MAX];//son[x][i],值为x的节点的儿子们 
struct edge{
	int to;
	int next;
//	int w;这里无所谓边的权重了 
}e[MAX];
int cnt;
int head[MAX];
void add(int u,int v){
	e[++cnt].to=v;
	e[cnt].next=head[u];
	head[u]=cnt;
}
void dfs(int x){
	dp[x][0]=0;
	dp[x][1]=h[x];//x号职员去或不去的初始快乐值
//	for(int i=0;i<son[x].size();i++){
//		int s=son[x][i];
	for(int i=head[x];~i;i=e[i].next){
		int s=e[i].to;
		dfs(s);
		dp[x][0]+=max(dp[s][0],dp[s][1]);//x不去,下属去或不去的最大值
		dp[x][1]+=dp[s][0]; 
	} 
	
}
int main(){
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>h[i];
	} 
	memset(head,-1,sizeof(head));
	int x,y;//下属,上司 
	for(int i=1;i<=n-1;i++){//只有n-1个职员有公司 
		cin>>x>>y;
//		son[y].push_back(x);
		add(y,x);
		vis[x]=1;//没有上司的是根节点 
	}
	int root;
	for(int i=1;i<=n;i++){
		if(!vis[i]){
			root=i;
			break;
		}
	} 
	dfs(root);
	cout<<max(dp[root][0],dp[root][1]);
    return 0;
} 

更周到的解法

洛谷题解页面大佬提供的思路👇
如果我们的人数相当多且是一条链的时候就容易造成爆栈,那这我们有如何解决呢?
方法有三
1、开一个数组手动实现栈。
2、bfs后用for循环
3、拓扑排序

第一个想必大家都会写,而且其与dfs相似,所以不再赘述。
那为啥会讲后两种呢?
因为有时候dfs并不好写,所以我们会把它转化为bfs+for或者拓扑,大家可以看一下,dfs和这两种写法的推导有的是不一样的。特别是和这题的拓扑写法,可以仔细看一下。

二、bfs+for循环(终于有点理解为什么说dfs能做到的bfs都可以做到了)从底部节点开始,节点由孩子信息推出(叶子节点孩子信息为0哪)

我们很容易发现树形dp它为什么一般会是dfs形式?因为树形dp的状态大多是一颗颗子树,它传递状态过程一般都是先求出最下层再往上更新。所以对于每一个点,我们在求解它的值的过程中,需要求出它每一个子节点的解
那有什么方法我们可以用数组和for循环实现这样的求解呢?没错,就是bfs过程中的队列。由于队列中的点都是先入的父亲节点后入的子节点,所以我们求解的时候只要把循环顺序反过来就可以了。

从根节点自顶而下就需要dfs掌握所有子树信息
那么试想自底而上,先掌握底部节点的信息,可用bfs将节点依据深度依次放入队列,当然了,先进先出,先放的应该是底部节点

#include <iostream>
#include <vector>
#include <queue>
using namespace std;
const int MAX=6005;
int h[MAX];
vector<int> son[MAX];
int fa[MAX];//以此判断根节点,感觉比vis是否有父亲难一点 
int dp[MAX][2];//dp[x][0]以x为根节点的子树上 在x上不妨士兵 共需士兵数
int vis[MAX];//节点是否入队的判断 
queue<int> Q;
int que[MAX];//到时候可逆序输出存储在队列中的节点
int cnt;//给que计数,到时候好逆序 
void bfs(int r){
	Q.push(r);
	que[cnt++]=r;
	vis[r]=1;
	while(!Q.empty()){
		int x=Q.front();Q.pop();
		for(int i=0;i<son[x].size();i++){
			int s=son[x][i];
			if(!vis[s]){
			Q.push(s);
			que[cnt++]=s;
			vis[s]=1;
			}
		}
	}
} 
int main(){
	int n;
	cin>>n;
for(int i=1;i<=n;i++){
		cin>>h[i];
		fa[i]=i;
	} 
	int x,y;//下属,上司 
	for(int i=1;i<=n-1;i++){//只有n-1个职员有公司 
		cin>>x>>y;
		son[y].push_back(x);
		fa[x]=y; 
	}
	int r=n;//随便找了个节点r,寻找她的终极祖先 我们的根节点 
	while(r!=fa[r]){
		r=fa[r];//r现在是当初r的父亲 父亲的父亲的父亲直到根节点 
	}
	bfs(r);
	for(int i=cnt-1;i>=0;i--){
		int x=que[i];
		for(int j=0;j<son[x].size();j++){
			int s=son[x][j];
			dp[x][0]+=max(dp[s][0],dp[s][1]);
			dp[x][1]+=dp[s][0];
		}
		dp[x][1]+=h[x];
	}
	cout<<max(dp[r][0],dp[r][1]);
    return 0;
} 

三、拓扑排序(节点fa[x]由节点x信息推出)

先回忆一下拓扑排序
在这里插入图片描述

拓扑排序,先将所有入度为0的点入队,列举队列中元素,逐一出队,出队时改变邻接结点的入度,如果邻接结点入读减为0就将其入队
现在从后往前,就看出度是否为0
这次就是真真切切的先存底部叶子节点再依次往上了

#include <iostream>
#include <vector>
#include <queue>
using namespace std;
const int MAX=6005;
int h[MAX];
vector<int> son[MAX];
int fa[MAX];//以此判断根节点,感觉比vis是否有父亲难一点 
int dp[MAX][2];//dp[x][0]以x为根节点的子树上 在x上不妨士兵 共需士兵数
int vis[MAX];//节点是否入队的判断 
queue<int> Q;
int degree[MAX];
int main(){
	int n;
	cin>>n;
for(int i=1;i<=n;i++){
		cin>>h[i];
	} 
	int x,y;//下属,上司 
	for(int i=1;i<=n-1;i++){//只有n-1个职员有公司 
		cin>>x>>y;
		son[y].push_back(x);
		fa[x]=y; 
		degree[y]++;//出度为0的点(没有孩子的点) 
	}
	for(int i=1;i<=n;i++){
		if(degree[i]==0)Q.push(i);
	} 
	int maxx=0;
	while(!Q.empty()){
		int x=Q.front();Q.pop();
		dp[x][1]+=h[x];
		maxx=max(maxx,dp[x][1]);
		maxx=max(maxx,dp[x][0]);
		degree[fa[x]]--;
		if(degree[fa[x]]==0)Q.push(fa[x]);
		dp[fa[x]][1]+=dp[x][0];
		dp[fa[x]][0]+=max(dp[x][0],dp[x][1]);
	}
	cout<<maxx;
    return 0;
} 

P2016 战略游戏

#include <iostream>
#include <vector>
using namespace std;
const int MAX=1500;
vector<int> son[MAX];
int vis[MAX];
int dp[MAX][2];//dp[x][0]以x为根节点的子树上 在x上不妨士兵 共需士兵数
void dfs(int x){
	dp[x][1]=1;
	dp[x][0]=0;//不放就只花0个士兵
//	if(son[x].size()==0)return;
	for(int i=0;i<son[x].size();i++){
		int s=son[x][i];
		dfs(s);
		dp[x][1]+=min(dp[s][1],dp[s][0]);
		dp[x][0]+=dp[s][1];
	} 
}
int main(){
	int n;
	cin>>n;
	int x,y,k;//x节点有k个节点与他相连(k个孩子 
	for(int i=0;i<n;i++){
		cin>>x>>k;
		for(int i=0;i<k;i++){
			cin>>y;
			son[x].push_back(y);
			vis[y]=1;
		} 
	}
	int root;
	for(int i=0;i<n;i++){
		if(!vis[i]){
			root=i;
			break;
		}
	}
	dfs(root);
	cout<<min(dp[root][1],dp[root][0]);
    return 0;
} 

链式前向星存储无向边 一要开两倍空间,二要

全都是有向边还好,有无向边就要判断即将dfs的节点x的终点是否是x的起点, 少了这个判断就死循环了,由于层层递归,x取决于to的情况,而以to为起点发出去的一条边终点又是x,像拓扑排序里形成了个圈,互为前提

#include <iostream>
//#include <vector>
using namespace std;
const int MAX=1500;
//vector<int> son[MAX];
int vis[MAX];
int dp[MAX<<1][2];//dp[x][0]以x为根节点的子树上 在x上不妨士兵 共需士兵数
struct edge{
	int to;
	int next;
}e[MAX<<1];
int head[MAX<<1];
int cnt;
void add(int u,int v){
	e[++cnt].to=v;
	e[cnt].next=head[u];
	head[u]=cnt;
}
void dfs(int x,int from){//全都是有向边还好,有无向边就要判断即将dfs的节点x的终点是否是x的起点, 少了这个判断就死循环了,由于层层递归,x取决于to的情况,而以to为起点发出去的一条边终点又是x,像拓扑排序里形成了个圈,互为前提
	dp[x][1]=1;
	dp[x][0]=0;//不放就只花0个士兵
//	if(son[x].size()==0)return;
//	for(int i=0;i<son[x].size();i++){
//		int s=son[x][i];
	for(int i=head[x];i;i=e[i].next){
		int s=e[i].to;
		if(s!=from){//由于视作了无向边a->b,b->a 
		dfs(s,x);
		dp[x][1]+=min(dp[s][1],dp[s][0]);
		dp[x][0]+=dp[s][1];
		}
	} 
}
int main(){
	int n;
	cin>>n;
	int x,y,k;//x节点有k个节点与他相连(k个孩子 
	for(int i=0;i<n;i++){
		cin>>x>>k;
		for(int i=0;i<k;i++){
			cin>>y;
//			son[x].push_back(y);
			add(x,y);
			add(y,x);
			vis[y]=1;
		} 
	}
	int root;
	for(int i=0;i<n;i++){
		if(!vis[i]){
			root=i;
			break;
		}
	}
	dfs(root,0);
	cout<<min(dp[root][1],dp[root][0]);
    return 0;
} 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值