树形DP(个人)学习笔记

                                                树形DP学习笔记

       这篇是我学习使用的,里面引用了众多的大佬的思路和文章,如有冒犯,请告知我,我会删掉的,谢谢!!!

 

  树形DP是以树的结构为基础,所进行的DP,其实现是用DFS,在DFS中使用DP,我认为DFS是辅助,DP才是重点,树形DP最重要的就是使用DP的思想,用着DFS使其答案最优。

大概有两种DP方程:

要的实现形式是dp[i][j][0/1],i是以i为根的子树,j是表示在以i为根的子树中选择j个子节点,0表示这个节点不选,1表示选择这个节点。有的时候j或0/1这一维可以压掉

第一种是选择节点:

\left\{\begin{matrix} dp[i][0]=dp[j][1] \\ dp[i][1]=max/min(dp[j][0],dp[j][1]) \end{matrix}\right.

第二种是树形背包:

\left\{\begin{matrix} dp[v][k]=dp[u][k]+val\\ dp[u][k]=max(dp[u][k],dp[u][k-1]) \end{matrix}\right.

(这一段,摘要:这位大佬,ta写得好好哇!!!,如有不满,麻烦联系我,我会快速的删掉)

 

第一题  没有上司的舞会

题目点这里

可以看出,这是节点选和不选的,因为如果你选择一位员工A,要是A害怕他的老板B,B还能选吗?不能吗?如果选B后快乐的指数比选A大,那A还选吗?不选了吗?上述过程符合上面的方程,这是一道基础的树形dp,dp[i][1]是选,dp[i][0]是不选。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int fa[N],dp[N][2];
vector<int> e[N];

void dfs(int x)
{
	dp[x][0]=0;
	for(int i=0;i<e[x].size();i++)
	{
		int v=e[x][i];
		dfs(v);
		dp[x][0]+=max(dp[v][0],dp[v][1]);//不选该节点,它的子节点有两种情况,选和不选;
		dp[x][1]+=dp[v][0];//选了该节点,它的子节点不能选了,因为它怕!!
	}
}

int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&dp[i][1]);
	for(int i=1;i<+n;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		e[v].push_back(u);
		fa[u]=v;
	}
	
	int i;
	for(i=1;i<=n;i++)
		if(!fa[i]) break;
	dfs(i);
	printf("%d\n",max(dp[i][0],dp[i][1]));
	return 0;
}

 

第二题   最大子树和

题目看这里

这题和上面一题是差不多的,但是它们的dp方程式不同的,方程为 dp[u]+=max(dp[v],0) ; 有些子节点相加会为负数,这朵花看着会恶心,因此直接不要即为0,让它与0相比较。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int dis[N],dp[N];
vector<int> e[N];
int n,ans=0;

void dfs(int x,int fa)
{
	dp[x]=dis[x];//x节点的美丽指数
	for(int i=0;i<e[x].size();i++)
	{
		int v=e[x][i];
		if(v!=fa)
		{
			dfs(v,x);//继续往下寻找
			dp[x]+=max(dp[v],0);//状态转移
		} 
	}
	ans=max(ans,dp[x]);//跟新答案
}

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d",&dis[i]);
	for(int i=1;i<n;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs(1,0);
	printf("%d\n",ans);
	return 0;
}

 

 

第三题  选课

题目看这里

 

如果这题没有要选M门这个限制条件,就和《没有上司的舞》 会很像。

言归正传,如果暂时抛开这是一棵树的结构,再来看看题目,有N门课,每门课都有大小不一的学分,从中选出M门课,并保证选出的M门的课的学分最大,这和背包问题是差不多的,因此方程就有:dp[u][j]=max(dp[u][j],dp[v][k]+dp[u][j-k];   v是以u为根节点的子节点,j是前j个节点选k门课的方案数。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e3+100;
int dp[N][N];
vector<int> e[N];
int n,m;

void dfs(int u)
{
	for(int i=0;i<e[u].size();i++)
	{
		int v=e[u][i];
		dfs(v);
		for(int j=m+1;j>=1;j--)
			for(int k=0;k<j;k++)
				dp[u][j]=max(dp[u][j],dp[v][k]+dp[u][j-k]);
	}
}

int main()
{
	scanf("%d%d",&n,&m);
	for(int v=1;v<=n;v++)
	{
		int u,val;
		scanf("%d%d",&u,&val);
		dp[v][1]=val;
		e[u].push_back(v);
	}
	dfs(0);
	printf("%d\n",dp[0][m+1]);
	return 0;
}

 

 

第四题  战略游戏

题目看这里

简而言之,就选和不选该节点,这就用到上述的第一个方程了。

代码如下:
 

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+100;
int dp[N][2],cnt[N];
vector<int> e[N];
int n;

void dfs(int x)
{
	dp[x][1]=1;
	for(int i=0;i<e[x].size();i++)
	{
		int v=e[x][i];
		dfs(v);
		dp[x][1]+=min(dp[v][1],dp[v][0]);
		dp[x][0]+=dp[v][1];
	}
}

int main()
{
	scanf("%d",&n);
	for(int i=0;i<n;i++)
	{
		int node,k;
		scanf("%d%d",&node,&k);
		for(int j=0;j<k;j++)
		{
			int v;
			scanf("%d",&v);
			e[node].push_back(v);
			cnt[v]++;
		}
	}	
	
	int node;
	for(node=0;node<=n;node++)//找到一个根节点
		if(!cnt[node]) break;
	dfs(node);
	printf("%d\n",min(dp[node][0],dp[node][1]));
	return 0;
}

 

 

第五题  二叉苹果树

题目看这里

这题其实和上面一题挺像的,都是有N个节点,想要Q个节点的最多苹果树,但是这题比较麻烦的一点是这题的根节点(1)始终不能删去要保留着,并且u和v上的所有的边也是要保留的,虽然这样,我们也把问题简化看一下,先给出状态转移方程:dp[x][j]=max(dp[x][j],dp[x][j-k-1]+dp[v][k]+val);如果选的一条边上的数量cnt是小于Q,就有前cnt节点选k颗的方案数,如果选的一条边上的数量是大于Q的,题目说要保留Q颗,因此就有前Q个节点选K颗的方案数。(说得可能不是很好,要是有不懂的,私聊我吧,如果愿意的话。)

代码如下:

#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pill;
const int N=1e3+100;
int dp[N][N],cnt[N],dis[N];
vector<pill> e[N];
int n,m;

void dfs(int x,int fa)
{
	for(int i=0;i<e[x].size();i++)
	{
		int v=e[x][i].first;
		int val=e[x][i].second;
		if(v!=fa)
		{
			dfs(v,x);
			cnt[x]+=cnt[v]+1;
			for(int j=min(cnt[x],m);j>=0;j--)
				for(int k=min(cnt[v],j-1);k>=0;k--)
					dp[x][j]=max(dp[x][j],dp[x][j-k-1]+dp[v][k]+val);
		}
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++)
	{
		int u,v,val;
		scanf("%d%d%d",&u,&v,&val);
		e[u].push_back(make_pair(v,val));
		e[v].push_back(make_pair(u,val));
	}
	dfs(1,-1);
	printf("%d\n",dp[1][m]);
	return 0;
} 

 

 

第六题  Cell Phone Network G

题目看这里

题目大致是说,如果我们选了这个点,那么与这条边相邻的所有的点都会信号,这题似乎很战略游戏那题挺像的,但是值得注意的是这题

重点是从节点开始选的,而那题重点是在边,选完这条边再选周围的边就完事了,可是点涉及到的就有点多了,如图:

题目要求要建立最小的站点,如果选中的是C,那么A,B,D都会信号,这三个点就不用建立站点了,也就是这三个点不能再使用了,但是战略游戏那题不能使用的是边,点还是可以使用的,所以这就是该题与战略游戏不同之处,回到该题,既然我们选择了C,A,B,D都不能使用,是不是有着三个状态,第一个:自己被自己覆盖了信号(即在该节点上建立站点);第二种:自己被自己的子节点覆盖了信号;第三种:自己被自己的父亲覆盖了信号;


1.自己被自己染色

这时我们可以想一下,u被自己染色可以由什么转移过来,如果u已经被自己染色了的话,他的儿子v可以选择自己染色,也可以选择被自己(v)的儿子染色,当然也可以被uu染色,当然,我们要选最小的,所以转移方程就是

f[u][0]+=min(f[v][0],f[v][1],f[v][2])(v \epsilon son _{u} )

2.被自己的父亲结点染色

如果被父亲结点(fafa)染色了,那么uu的儿子vv只能选择自己染色或者被它的儿子染色,转移方程为

f[u][2]+=min(f[v][0],f[v][1])(v \epsilon son _{u} )

3.被自己的儿子结点染色

这是最麻烦的一种情况,因为u可能有多个儿子,只要有一个儿子自己染色了,就可以将uu覆盖,这种情况就成立了

而现在它的儿子有两种情况,分别是自己染色和被它儿子染色

我们可以先假设每个儿子都是被它自己染色(v被自己染色)的,然后看一下u的每个儿子(v)被其儿子染色是否使结果变得更小,把能让结果更小的 自己染色(v自己染色)的儿子 替换为 被其儿子染色的儿子(v被它儿子染色)的儿子

(参考了ysnerysner大佬的思路)

那么怎么实现呢?

  1. 先让f[u][1]加上所有的f[v][0](也就是假设所有的v目前都是自己给自己染色的)
  2. 在进行一的同时,用一个gg数组,表示vv被儿子染色所需的价值减去vv被自己染色的价值的差值,同时用一个变量tot记录一下一共有多少个儿子,即g[++tot] = f[v][1] - f[v][0]g[++tot]=f[v][1]−f[v][0]
  3. 如果uu没有儿子,即tottot为00,说明uu是一个叶结点,那么就没有从儿子来的价值,因为转移的时候我们要取小的,所以就把f[u][1]f[u][1]设为一个极大值
  4. 如果u有儿子,就将g从小到大排序,如果是负值,我们就可以替换,因为是负值的话就说明,此时的f[v][1]比f[v][0]小,所以就可以替换,只要是负的,值越小越好,所以就排序一下,是负的就替换,否则就breakbreak,当然我们最多替换tot-1个,因为要保证u被染色,必须有一个儿子是自己染色的

至此主要部分就讲完了,需要注意的是每次dfs的时候先将f[u][0]设为1,因为自己给自己染色肯定至少为1

(此部分是大佬的思路,超级棒棒!! 如有不满,请麻烦联系我,我会快速删掉!!!)


代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+100;
const int INF=0x3f3f3f;
int dp[N][3];
vector<int> e[N];
int n;

void dfs(int u,int fa)
{
	int g[N]={0},tot=0;
	dp[u][0]=1;
	for(int i=0;i<e[u].size();i++)
	{
		int v=e[u][i];
		if(v==fa) continue;
		dfs(v,u);
		dp[u][0]+=min(min(dp[v][1],dp[v][0]),dp[v][2]);
		dp[u][1]+=dp[v][0];
		dp[u][2]+=min(dp[v][0],dp[v][1]);
		g[++tot]=dp[v][1]-dp[v][0];
	}
	if(!tot) dp[u][1]=INF;
	else
	{
		sort(g+1,g+1+tot);
		for(int i=1;i<tot;i++)
			if(g[i]<0) dp[u][1]+=g[i];
			else break;
	}
}

int main()
{
	scanf("%d",&n);
	for(int i=1;i<n;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		e[u].push_back(v);
		e[v].push_back(u);
	}
	dfs(1,-1);
	printf("%d\n",min(dp[1][1],dp[1][0]));
	return 0; 
} 

 

看到这里,对于什么是树形DP是不是有个大概的了解,其重点是DP的思想,再其次是利用树的结构,进行递归实现。

 

最后,还有一些题,会尽快补上,对于上述被我引用的大佬们,非常感谢,也非常抱歉没有经过你们的同意擅自使用你们的文章部分内容,如有不满,请联系我,我会尽快删掉,在上述文章中除了我特别说明部分,剩下部分都是我原创。如果不解,可以私信我,谢谢。

                                                                                                                 (编辑于2020.9.28  时间:1.38)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值