树形dp粗解

树的性质:n个点,n-1条边,任意两个点之间只存在一条路径,可以人为设置根节点,对于任意一个节点只存在至多一个父节点,其余为子节点。

记忆化树形dp模型较为抽象难以理解,以下通过由浅到深的方式解析树形dp以及树的性质。

树形dp求树的直径:(在一颗树里找到点X,Y,使得|XY|最大)

如图,我们令A为根节点,令dfs遍历顺序为ABDGHEFC。

在我们的dfs计算中,我们从下往上对每一个节点,找到其最长的两个子树路径(两个没有重复路径的子树,例如节点B为BDG+BF,而不会出现BDG+BDH),并更新答案(ans=BDG+BF),然后将其中最长的子树路径上传给父节点(对于点A,B上传的只有最长的BDG),在A节点,得知最长的两个子树路径(ABDG+AC),并更新答案(ans=ABDG+AC)

处理方式如下:

根据思路,代码分为两个处理部分:更新答案和更新当前节点最长子树路径,其中更新答案是读取当前访问的子树中最长的子树路径(记为dp【子】),将其与现在该节点记录的最长子树(记为dp【当前】)相连来查看答案是否能更新,然后,如果dp【子】比dp【当前】大,则更新dp【当前】为dp【子】。

代码如下:

struct num
{
       int nex,val;
};
void dfs(int u,int fa)
{
       for(int i=0;i<q[u].size();i++)
       {
              int v=q[u][i].nex;//子节点 
              if(v!=fa)
              {
                     dfs(v,u);//先遍历 
                     ans=max(ans,dp[u]+dp[v]+q[u][i].val);//更新答案 
                     dp[u]=max(dp[u],dp[v]+q[u][i].val);//更新节点 
              }
       }
}

  

留下思考点:(底部有解析)

1.为何更新答案要早于更新节点

2.dp【节点】的值是什么意思

3.替换根节点会产生影响吗

 

 

理解了树形dp的基本结构后,来看一道稍微复杂的题目

Hdu4616:http://acm.hdu.edu.cn/showproblem.php?pid=4616

题意简述:

在一颗有n(n<5e4)个节点的树中,每个节点有权值和是否有陷阱,你可以最多踏进c(c<=3)个陷阱,当你进入第c个陷阱时,你就无法继续移动了,你可以在任意节点出发,获取经过节点的权值(无法重复获取同一个节点),求能得到的最大权值和。

题目和树的直径很相似,实际上获取点的权值和获取边的权值是没有本质区别的。

不过题目中的陷阱是一个比较有意思的元素,我们可以给dp数组增加一个维度(如dp[a][b],表示与节点a相连的子树中,经过b个陷阱时能获得的最大权值和)

但是,这会衍生出一个问题,如下:

假设图中BC点有陷阱,AD点无陷阱,我们最多经过2个陷阱,令A为根节点,那么在C节点时,dp[c][1]=CD,dp[c][0]=0 x在B节点时,dp[b][2]=BCD,dp[b][1]=B,注意dp[b][2]是不会再往A节点衍生的,因为达到了最大陷阱数,所以在A节点时,dp[a][2]=0,dp[a][1]=AB(对于dp表示的值有疑惑的回到上面再思考),此刻我们漏掉了一种情况,即ABC的值。

这个问题可以总结为:当我们访问了最大数量的陷阱时,路径至少要有一个起/终点在陷阱上,而我们的状态不足以得知是否有一个起/终点在陷阱上.

因此,我们需要再增加一维状态表示起点(子树中深度最深的节点)是否在陷阱上

代码如下:

void dfs(int u,int fa)
{
       dp[u][go[u]][go[u]]=val[u];//go为1表示有陷阱,val表示权值 
       for(int i=0;i<q[u].size();i++)
       {
              int v=q[u][i];
              if(v!=fa)
              {
                     dfs(v,u);
                     FOR(j,0,c)//更新答案 
                     {
                            FOR(k,0,c-j)
                            {
                                   ans=max(ans,dp[u][j][1]+dp[v][k][1]);
                                   if(j<c) ans=max(ans,dp[u][j][0]+dp[v][k][1]);
                                   if(k<c) ans=max(ans,dp[u][j][1]+dp[v][k][0]);
                                   if(k+j<c) ans=max(ans,dp[u][j][0]+dp[v][k][0]);
                            }
                     }
                     FOR(j,0,c)//更新节点 
                     {
                            if(j<c) dp[u][j+go[u]][0]=max(dp[u][j+go[u]][0],dp[v][j][0]+val[u]);
                            dp[u][j+go[u]][1]=max(dp[u][j+go[u]][1],dp[v][j][1]+val[u]);
                     }
              }
       }
}

  

值得一提的是初始化,dp数组初始化为不够小的值会导致答案错误,数组需要初始为极小值,关于这个情况,discuss中有hack样例。

 

 

Bzoj1509:https://www.lydsy.com/JudgeOnline/problem.php?id=1509

逃课的小孩(思路来源于陈瑜希论文)

题意简述:

在一个有n(n<=2e5)个节点的树中,找到三个点XYZ,使得XY<=XZ,求出XY+YZ的最大值。

如果把Y当作确定的点,那么这个命题等价于找出离Y最远的两个点(树的直径为找出最远的两个点),这就类似于求树的直径,复杂度为O(n),然而我们无法直接得知Y点,所以复杂度为O(n*n),在复杂度上无法满足。

或许这里有一个疑问,为什么一定要知道Y点才可以得出答案,难道不能像求树的直径一样吗?看如下情况

 

A为根节点,图中直线距离均为1,那么离A最远的点为C和D,距离为2和3,然而2+3不是答案,行动轨迹并不是想当然的C->A->D,因为D更近,所以是C->D->A,答案为1+3,很显然,共享部分路径的最远点的情况较为复杂,并不能通过简单的最远距离存储答案,最开始的简单模型无法应对这么复杂的情况。

枚举分叉点:对于任意的XYZ,必然存在一个分叉点A(可以与Y重合),使得XA,YA,ZA不包含除A以外的交点(树的性质,Y到X和Y到  Z的路径唯一,所以存在唯一的交点A)。枚举分叉点是可以规避上述的复杂情况(因为每条路径不存在重复点),但这又引入了一个问题,即“父节点响应”,接下来解释为什么需要“父节点响应”

A为根节点,dfs序为ABDEC,令BD<BE=AB=AC,很显然答案为DB+BE*2+BAC(D->B->E->B->A->C),那么在B节点时,B的信息只有DB和EB(从下往上),上传到A节点的信息只有EBA(枚举分叉点自然不能出现重复边,所以只能上传最大边),A的信息只有EBA和CA,仅靠这些信息时无法得出答案的。除非我们在B节点时能读取到父节点A的信息CA,并与AB相连,不过很显然,我们得到A节点的信息是晚于B节点的,所以我们需要再次dfs。

 

代码如下:

 

struct num
{
	int ne;
	ll l;
}k,ans[MAXN][4];
bool cmp(num a,num b)
{
	return a.l>b.l;
}
void dfs1(int u)
{
	int v;
	FOR(i,0,3)
	ans[u][i].l=0;
	for(int i=0;i<q[u].size();i++)
	{
		v=q[u][i].ne; 
		if(!vis[v])
		{
			vis[v]=1;
			dfs1(v);
			ans[u][3].l=ans[v][0].l+(ll)q[u][i].l;
			ans[u][3].ne=v;
			sort(ans[u],ans[u]+4,cmp);
		}
	}
	sum=max((ll)sum,ans[u][0].l+ans[u][1].l*2+ans[u][2].l);
}

void dfs2(int u)
{
	int v,l;
	for(int i=0;i<q[u].size();i++)
	{
		v=q[u][i].ne;
		if(vis[v])
		{
			if(ans[v][0].ne!=u)
			ans[u][3].l=ans[v][0].l+(ll)q[u][i].l;
			else
			ans[u][3].l=ans[v][1].l+(ll)q[u][i].l;
			ans[u][3].ne=v;
			sort(ans[u],ans[u]+4,cmp);
			sum=max((ll)sum,ans[u][0].l+ans[u][1].l*2+ans[u][2].l);
			break;
		}
	}
	for(int i=0;i<q[u].size();i++)
	{
		v=q[u][i].ne;
		l=q[u][i].l;
		if(!vis[v])
		{
			vis[v]=1;
			dfs2(v);
		}
	}
}

  

 

解答:

1.更新答案我们用到的是dp【当前节点】和dp【子】之和,因此dp【子】不能先改变dp【当前节点】。

2.dp【节点】的值的含义为:以该节点为根节点,得到的最长子树路径(即包含该节点及其对应子树的节点)。

3.无影响,这个是树的性质之一。

 

原文地址:https://www.cnblogs.com/qq936584671/p/10274268.html