树形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)

posted @ 2020-09-28 12:53  独恒而后  阅读(248)  评论(0)    收藏  举报