树形DP简单总结

树的特征

1.N个点 只有N-1条边的无向图

2.无向图里 任意两点有且只有一条路

3.一个点只有一个前驱 但可以有多个后继

4.无向图没有环

树形DP

由于树有着天然的递归结构 父子结构 而且它作为一种特殊的图 可以描述许多复杂的信息 因此在树就成了一种很适合DP的框架

问题:给你一棵树 要求用最少的代价(最大的收益)完成给定的操作

树形DP 一般来说都是从叶子从而推出根 当然 从根推叶子的情况也有 不过很少(本蒟蒻还没有做到过~)

一般实现方式: DFS(包括记忆化搜索),递推等


例题

1.二叉苹果树

传送门

二叉树 很的一种dp结构 由于二叉树父亲节点只用管它的左右儿子 状态转移变得较为轻松

在遇到多叉树时 我们时常会考虑把多叉树转化为二叉树来做

而这道题直接是二叉树

首先考虑给DP数组下定义 一般来说树形DP的DP数组的第一维都是当前节点的编号

这道题光一维肯定是不够的  那么加维 发现dp[i][j]表示当前节点为i 保留j个节点的最大苹果数量比较ok

那么方程就显而易见了 dp[i][j]=max(dp[i][j],dp[i.lson][k]+dp[i.rson][j-k-1]+apple[i])

很明显 该问题具有很明显的最优子结构性质具备无后效性(每一步只与儿子有关系 而与爸爸之类的没有关系 )

另外 还可以在dfs时运用记忆化 可以大大提高速度 

再提一句 由于题目中给的权值在边上 让人特别难受 于是 我们把权值转化到儿子上会方便操作

//f[i][j] 当前在i点 保留j个节点 
//f[i][j]=max(f[i][j],f[tree[i].l][k]+f[tree[i].r][j-k-1]+tree[i].v);
#include<bits/stdc++.h>
using namespace std;
const int N=150;
int n,q,dp[N][N];
struct node
{
	int lson;
	int rson;
	int val;
}tree[N*20];
int dfs(int now,int point)
{
	if(now==0||point==0)
	{
		return 0;
	}
	if(tree[now].lson==0&&tree[now].rson==0)
	{
		return tree[now].val;
	}
	if(dp[now][point]>0)	return dp[now][point];//记忆化 
	for(int k=0;k<point;k++)	//枚举方程中的k 
	{
		dp[now][point]=max(dp[now][point],dfs(tree[now].lson,k)+dfs(tree[now].rson,point-k-1)+tree[now].val); 
	}
	return dp[now][point];
}
int main()
{	cin>>n>>q;
	for(int i=1;i<=n-1;i++)
	{
		int fa,son,v;
		cin>>fa>>son>>v;
		tree[son].val=v;
		if(tree[fa].lson==0) tree[fa].lson=son;
		else tree[fa].rson=son;
	}
	cout<<dfs(1,q+1);//注意是q+1 因为把边变成了点 
	return 0;
}
2.选课

传送门

这道题就是采用刚才提到过的 把多叉树转化为二叉树来做

关于如何把多叉树转化为二叉树 有个口诀 叫做左儿子不变 右儿子兄♂弟 

详细的不多说 可以去参考一下相关资料

等转化为二叉树了过后 让我们来琢磨一下

左儿子:原根节点的孩子

右儿子:原根节点的兄♂弟

也就是说 不能直接套用第一题的方程 但是可以对dp数组进行相同的定义

对于一个根节点 都可以 选 或者 不选

当给左儿子分配资源时 根节点必须选 而与右儿子无关

因此 方程就显而易见了 dp[i][j]=max(dp[i][j],dp[i.rson][j],dp[i.lson][k]+dp[i.rson][j-k-1]+val[i])   (0<=k<j)

//dp[i][j]: i的所有兄弟和i的所有儿子 和i自己 学j门课的最大学分总和。
//dp[i][j]=max(dp[rson][j],dp[lson][k]+dp[rson][j-k-1]+val[i]) 
#include<bits/stdc++.h>
using namespace std;
const int N=305;
int n,m,bigson[N],dp[N][N];

struct node
{
	int val,lson,rson;
}tree[N*4];
int dfs(int now,int point)
{
	if(!now||!point)	return 0;
	if(dp[now][point])	return dp[now][point];
	dp[now][point]=dfs(tree[now].rson,point);
	for(int k=0;k<point;k++)
	{
		dp[now][point]=max(dp[now][point],dfs(tree[now].lson,k)+dfs(tree[now].rson,point-k-1)+tree[now].val);
	}
	return dp[now][point];
}
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(NULL);
	cout.tie(NULL);
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		int k,s;	//爸爸 权值 
		cin>>k>>s;
		tree[i].val=s;
		if(bigson[k]==0)	tree[k].lson=i;	//如果k还没有其他儿子 那么i就是儿子了
		else tree[bigson[k]].rson=i;
		bigson[k]=i; 
	}
	cout<<dfs(tree[0].lson,m);
	return 0;
}

3.树的直径

传送门

这是解析...然而我觉得bfs或者dfs就够了 何苦dp


4.战略游戏

传送门

假设dp[i]表示以i为根的子树上需要安放的最少士兵 希望能从i的儿子推出i的情况

然而无法做到 考虑加维

由于每个节点可以选 或者不选 

如果选了的话  那他的儿子可选可不选

如果没选的话 那他的儿子就必须选

因此dp[i][0]表示选了节点i所需要安防的最少士兵 dp[i][1]表示不选

方程显而易见 dp[i][0]=sigma min(,dp[i.son][0],dp[i.son][1]) dp[i][1]=sigma min(dp[i][1],dp[i.son][0])

//dp[i][0] 选i dp[i][1] 不选i 的所需最小个数 
//如果选了i 意味着可以选或者不选他的儿子
//如果没选 意味着必须选所有的儿子 
//dp[i][1]=sigma(dp[i.son][0])
//dp[i][0]=sigma(min(dp[i.son][0],dp[i.son][1]))
#include<bits/stdc++.h>
using namespace std;
const int N=1505;
int n,dp[N][2],first[N],tot;
struct Edge
{
	int to,next;
}edge[N*4];
inline void addedge(int x,int y)
{
	edge[++tot].to=y;
	edge[tot].next=first[x];
	first[x]=tot;
}
inline void dfs(int now)
{
	dp[now][0]=1,dp[now][1]=0;
	for(int u=first[now];u;u=edge[u].next)
	{
		int vis=edge[u].to;
	//	if(vis==fa)	continue;
		dfs(vis);
		dp[now][1]+=dp[vis][0];
		dp[now][0]+=min(dp[vis][0],dp[vis][1]);
	}
}
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		int a,k;
		cin>>a>>k;
		for(int j=1;j<=k;j++)
		{
			int son;
			cin>>son;
			addedge(a,son);
		//	addedge(son,a);
		}
	}
	dfs(0);
	cout<<min(dp[0][1],dp[0][0])<<endl;
	return 0;
}

5.皇宫看守

网址实在没找到....我提交的地方是学校题库

(题面)



对于每个点 都有三种情况

1.自己放

2.父亲放(被父亲看到)

3.儿子放(被儿子看到)

这意味着什么呢?

对于一个i 

if 自己放了 也就是说儿子一定被父亲看到 也可以安排警卫 也可以被它的儿子看见

else

如果父亲放了 也就是说儿子可以安♂排 也可以被它的儿子看见

如果儿子放了 它的儿子必定有一个安排了的 否则被它的儿子看见 具体可以进行一些 特♂判


其实这道题很像上一道题的升级版

点到为止 不多说了(其实只是懒


6.消息传递

传送门

由于根是不一定的 所以需要遍历所有点 作为根

设dp[i]是以i为根的子树传遍它所有子树需要的最少时间

dp[i]取决于花费时间最多的那颗子树(当然还要加上每次一秒的传递时间) 不过也不是一定的 万一话费时间最多的和次多的只差了一秒之类的情况也会出现 所以需要遍历所有的儿子~

方程:dp[i]=max{dp[i.son]+i.son.number(传递时间)}

#include<bits/stdc++.h>
using namespace std;
const int N=3005;
int n,tot,first[N],dp[N],son[N],cnt,ans=0,num;
struct Edge
{
	int to,next;
}edge[N*10];
inline void addedge(int x,int y)
{
	tot++;
	edge[tot].to=y;
	edge[tot].next=first[x];
	first[x]=tot;
}
inline bool cmp(const int &a,const int &b)
{
	return a>b;
}
inline void dfs(int now,int fa)
{
	for(int u=first[now];u;u=edge[u].next)
	{
		int vis=edge[u].to;
		if(vis==fa)	continue;
		dfs(vis,now); 
	}
	int cnt=0;
	for(int u=first[now];u;u=edge[u].next)
	{
		int vis=edge[u].to;
		if(vis==fa)	continue;
		son[++cnt]=dp[vis];
	}
	sort(son+1,son+cnt+1);
	int ret=0;
	for(int i=1;i<=cnt;i++)
	{
		ret=max(ret,son[i]+cnt-i); 
	}
	dp[now]=ret+1;	//加1是因为仔细看了样例后发现默认时间是从一秒开始的orz 
}
vector <int> con;
int main()
{
	cin>>n;
	for(int i=2;i<=n;i++)
	{
		int x;
		cin>>x;
		addedge(i,x);
		addedge(x,i);
	}
	for(int i=1;i<=n;i++)
	{
		dfs(i,0);
		int tmp=dp[i];
		if(!ans||tmp<ans)
		{
			ans=tmp;
			con.clear();
			con.push_back(i);
		} 
		else if(tmp==ans)	con.push_back(i);
	}
	cout<<ans<<endl;
	for(int i=0;i<con.size();i++)	cout<<con[i]<<" ";
	return 0;
}

7.有线电视网

点击打开链接

树上背包。

这道对于本蒟蒻来说比较难 

背包的总容量相当于该点为根节点的子树中所有的用户数量。然后,把该节点的每个儿子看成一组,每组中的元素为选一个,选两个...选n个用户。

转移方程 dp[i][j]=max(dp[i][j],dp[i][j-k]+dp[v][k]-这条边的花费) i,j不解释了,v表示枚举到这一组(即i的儿子),k表示枚举到这组中的元素:选k个用户。
//dp[i][j] 当前节点为i选j个用户 所能得到的最大收益
//dp[i][1]=pay[i];(叶子节点)
//dp[i][某个儿子的编号]=max{dp[i][某个儿子的编号-k(需要枚举)]+dp[vis][k]-val}	(j>k>=1,) 
#include<bits/stdc++.h>
using namespace std;
const int N=3005;
int n,m,first[N],pay[N],tot,dp[N][N];
struct Edge
{
	int to,next,v;
}edge[N*2];
inline void addedge(int x,int y,int z)
{
	tot++;
	edge[tot].to=y;
	edge[tot].next=first[x];
	edge[tot].v=z;
	first[x]=tot;
}
int dfs(int now)
{
	dp[now][0]=0;
	if(now>n-m)	//用户一定为叶子 
	{
		dp[now][1]=pay[now];
		return 1;
	}
	int j=0;
	for(int u=first[now];u;u=edge[u].next)
	{
		int vis=edge[u].to;
//		if(fa==vis)	continue;	
		j+=dfs(vis);
		for(int i=j;i;i--)//枚举每一个j 
		{
			for(int k=0;k<=i;k++)
			{
				dp[now][i]=max(dp[now][i],dp[now][i-k]+dp[vis][k]-edge[u].v);	//倒序来压维 如果正序的话 dp[now][i-k]就被更新过了 并不是上一个儿子的值 
			}
		}
	}
	return j;
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n-m;i++)
	{
		int k;
		cin>>k;
		for(int j=1;j<=k;j++)
		{
			int son,val;
			cin>>son>>val;
			addedge(i,son,val);
		//	addedge(son,i,val);
		}
	}
	for(int i=1;i<=m;i++)
	{
		cin>>pay[i+n-m];
	}
	memset(dp,128,sizeof(dp));	//128是负无穷大 
	dfs(1);
	for(int i=m;i>=0;i--)
	{
		if(dp[1][i]>=0)	
		{
			cout<<i;	
			break;
		}
	}
	return 0;
} 



总结:

通常来说 把一棵树转化为二叉树 然后整个问题的最优只涉及到左右儿子的最优 然后考虑根节点随之的变化 这样化简了问题 也很容易推出状态转移方程 

当然 也不是所有问题都要这样 我们应该仔细推敲每个结点的状态 以及相应状态与父子结点的联系等 就是如何从子节点的最优值推出父节点的最优值

  • 10
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值