树形dp小结1

常见问题:

1. 父亲节点与子节点有相互制约(只能选其一),或者是相互共存(要选子节点就必须选父亲节点),其实这两种问题都属于背包模型里的依赖型背包。解决这两类问题首先要确定父亲节点和子节点的关系,加入选了父亲节点就不能选子节点,那么在dp的时候,就要分两种情况来讨论,每个节点选还是不选,一般需要开一个二位数组dp[i][0],dp[i][1]分别代表这个节点选或是不选。

例题分析: Anniversary party HDU - 1520 

题目要求有相互制约关系的父亲节点和子节点,问怎么选择这些节点使得值最大。树行dp的状态转移并不像普通dp那样难以想象,因为每个点只会于子节点和父亲节点有直接关系,看不出来时候,可以画图分别研究它的子节点和父亲节点,树行dp的难点在于如何正确的定义出状态,比如这个题,如果你能定义出dp[i][0]表示第i个节点不选能得到的最大值,dp[i][1]表示第i个节点选择能得到的最大值,注意,这里对应的最大值是指以i为根节点的子树上。

树形dp难以逃避的两大事实,子树上的关系和父亲与孩子的关系。我们想要知道的最后答案就是这样通过子节点传递到父亲节点,到子树,最后到整颗树,另外树选谁做根节点,取决于dfs时的选择,dfs一开始传入的参数就是根节点,当然根节点的选择不同也会带来不同,这些将在后面展开。

下面介绍树形dp的套路————DFS

首先,dfs是从根节点开始,一步步往下搜索的过程,树形dp的核心在于得到父亲节点与子节点的关系,而dfs的回溯过程恰恰满足我们想要的。也就是说dfs一次我们只能得到子节点对父亲节点的影响状态。这个条件也就束缚了我们,在许多题目中往往要dfs两次,或者多次。

其次我们的递推式放在dfs函数的什么位置,也是一个要点,如果你彻底的理解dfs和你定义的状态,那你不会放错位置。

一般来说,在我们由子节点推父亲节点时,往往是放在回溯位置,但如果你想要由父亲节点推子节点,那需要在dfs回溯位置之前。

void dfs(int s)
{
    vis[s]=1;
    for(int i=head[s];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        if(vis[v]==1) continue;
        //回溯前位置
        dfs(v);
        dp[s][0]+=max(dp[v][0],dp[v][1]);//回溯位置
        dp[s][1]+=dp[v][0];
    }
}

2. 求树上点能到达的最远距离。

这个问题是树行dp中的常见问题,因为它同时涉及到了这个点的子节点和父亲节点,也就是说我们定义的状态dp[i]表示i点能达到的最远距离,它即可能往子节点方向走,也可能往父亲节点的方向走。

但我们一次dfs只能求出这个点往子节点方向能到达的最远距离,所以需要两次dfs才能实现。

但是在第一次dfs时,我们不能仅仅只求这个点往子节点方向的最远距离,还需要一个次远距离。为什么?原因很简单,i点的子节点相对于i的父亲节点而言,也是一个子孙节点,所以会出现i点的最远距离和它父亲的最选距离,走的是同一条路径。这个还是画个图比较好理解。同时求出最大和次大也算是一种技巧吧,多打两次就完全理解了。

例题:ComputerHDU - 2196 

核心代码:两次dfs,dfs求出的还不是最后答案,最后答案应该是max(dp[i][0],dp[i][2])

void dfs1(int s,int pre)
{
    ll mx0=0,mx1=0;
    for(int i=head[s];i!=-1;i=edge[i].next)
    {
        int v=edge[i].v;
        int w=edge[i].w;
        if(v==pre) continue;
        dfs1(v,s);
        if(mx0<=dp[v][0]+w)
        {
            mx1=mx0;//找到更大的,则直接把以前的给第二大
            mx0=dp[v][0]+w;
            idx[s]=v;//记录最大值走过的的直接子节点
        }
        else if(mx1

3. 树上背包

在背包9讲里,就看到过依赖型背包可以转化成树形dp,那么在树上到底给如何进行背包的操作?

首先需要确定的是,树上背包同样也是给定一些‘容量‘,在每个点或者经过每条边,会花费一些’容量‘,同时得到一些价值,问最后的最大价值是多少?

首先你需要确定这个普通背包该如何写,其次如何转移到树上。

可以这样考虑,有W的容量,放在树的根节点,该如何给他的子节点分容量,使得价值最大化。

实现的问题在于,dfs的时候每次只能回溯一个点,不能想我们普通背包一样每次直接枚举各种容量,再取最大值。

所以我们采取的办法是,对于每次回溯,我们都枚举一次将全部的容量分父亲节点和子节点,这样做的正确性在于,所有节点的容量不能可能超过这个。

例题: Starship Troopers HDU - 1011 

//树上背包的模板形式
void dfs(int p)
{
    int i,j,k;
    int temp=(cos[p]+19)/20;
    for(i=temp;i<=m;i++) dp[p][i]=weg[p];
    vis[p]=1;
    for(i=0;i=temp;j--)//只所以逆序是因为它和01背包的降维一样
        {
            for(k=1; k<=j-temp; k++)//留下temp攻打p
                dp[p][j]=max(dp[p][j],dp[p][j-k]+dp[t][k]);
        }
    }
}

边上背包问题:

刚刚的例题是每个点会有一定的消费与价值,这样很容易类比到01背包,但是,如果是边上的花费呢?、

例题: Find Metal Mineral HDU - 4003

题解

其实思路还是一样的,我们只需要考虑如何将机器人分配给子节点就行,不过有点坑点。

 

更复杂的问题:GeoDefense  HDU - 4044 

总而言之,树上背包的基本套路是基本一样,各题的情况可能略有不同。

 

4.  树形dp+二分问题

最小化最大限制问题(最大值最小化)

一般都是要转化为二分来解决的。

例题:Information Disturbing  HDU - 3586

题解:https://blog.csdn.net/qq_40774175/article/details/81366530

思路是:每次枚举最小限制,再去dfs求出最小值,看是否满足题意。

5. 树上找规律

很多时候,无法想出完美的状态表示我们想要求的值,这时候可以考虑画图找找树上的规律

例题: Holiday's Accommodation  HDU - 4118 

这道题。如果直接想状态的话,可能会难以想象。可以发现一个规律,每条边对答案的贡献,只与它两边的点数有关,所以只要dp求出每个点的子树上有多少个点就行。

例题2:Y HDU - 4705
题目要求不在同一路径上的三个点有多少,我们可以求出在同一路径上的个数,总的减去求出的即可。

现在问题就转化为如何求同一路径上的个数,我们可以这样求,对每个节点,选择它和它子树上的任意一点,以及他们之外的一点,即可以构成一条路径。

例题3:O - Tree2cycle HDU - 4714 

求将树转化为环的最小操作数,画几个图以后,应该可以发现,最小操作数就是分支数*2+1,所以问题转化为求分支数,

求解分支数也算是树上dp的一种常用方法,直接统计每个节点的子节点个数,他们对分支数的贡献就是 个数-1,但对于根节点来说,应该是 个数-2,因为这样才能保证主干最长。具体可以查看我的博客。

例题4: Balancing Act POJ - 1655

求树的重心,可以算是找规律吧,总之发现规律后dp写起来就很方便,统计下子树的子节点最多的,和它的子树节点总数就行。

树的重心

 

6. 树的直径

树的直径定义为树上最长的路径,求树的直径有很多方法。

例题1: Tree HDU - 3534

求树的直径和直径的数量,这可以算是比较裸的直径题目了,直接dp[i]表示能达到的最远距离(朝着子节点方向),ans记录最大值就行。对于数量,再多开个node数组来记录。

 

void dfs(int u,int fa)
{
	int i,v,tmp;
	dp[u]=0;
	node[u]=1;
	for(i=head[u];i!=-1;i=edge[i].next)
	{
		int v=edge[i].v,w=edge[i].w;
        if(v==fa) continue;
        dfs(v,u);
        tmp=dp[v]+w;
        if(tmp+dp[u]>ans)
        {
            ans=tmp+dp[u];
            k=node[u]*node[v];
        }
        else if(tmp+dp[u]==ans)
        {
            k+=node[u]*node[v];
        }
        if(tmp>dp[u])
        {
            dp[u]=tmp;
            node[u]=node[v];
        }
        else if(tmp==dp[u])
        {
            node[u]+=node[v];
        }
	}
}

 

还有一种求树的直径方法就是两次dfs,第一次dfs记录每个节点的深度,第二次dfs 从深度最大的开始,再次dfs即可,算法的正确性可以画图分析,或者查看文献。

例题:Terrorist’s destroy HDU - 4679 

题解:https://blog.csdn.net/qq_40774175/article/details/81394727

 

你可能感兴趣的:(树形DP)