【树形DP】树形DP入门详解+例题剖析

树形DP

树形DP准确的说是一种DP的思想,将DP建立在树状结构的基础上。整体的思路大致就是用树形的结构存储数据。

要学树形DP之前肯定是要先学会树和图的呀,至少先学会链式前向星,不会的话可以看一下我之前写的博客
链接:【图论】图,实现图(三种方式),二分图 详解

树形DP的关键和实现方法是 d f s dfs

先找到树根,从树根开始运用 d f s dfs 递归,跟 d f s dfs 一样先初始化,然后递归到叶子节点上为止,把最底层的 f [ i ] [ j ] f[i][j] 更新完毕,再回来往上走,自底向上地根据题意更新上层的 f f 数组,最后输出答案即可。

一般基础的题转移方程有两种模式:
选择节点类

{ f [ i ] [ 0 ] = f [ j ] [ 1 ] f [ i ] [ 1 ] = max / min ( f [ j ] [ 0 ] , f [ j ] [ 1 ] ) \begin{cases}f[i][0]=f[j][1]\\f[i][1]=\max/\min(f[j][0],f[j][1])\\\end{cases}

选择节点式的题首先前提条件是整个数据是由树形结构存储的,或者应该用树形结构存,效率更高什么的,然后会告诉你相邻的节点是不能同时存在的,要求取最大最小值 ,类似P2016 战略游戏、P1352 没有上司的舞会(下面都有详解和题目链接哦)

树形背包类

{ f [ v ] [ k ] = f [ u ] [ k ] + v a l f [ u ] [ k ] = m a x ( f [ u ] [ k ] , f [ v ] [ k 1 ] ) \begin{cases}f[v][k]=f[u][k]+val\\f[u][k]=max(f[u][k],f[v][k-1])\\\end{cases}
树形背包,就是背包加上条件,一个物品只有选择了它的主件(根节点)才能选择,类似 P 2014 [ C T S C 1997 ] P2014[CTSC1997] 选课

例题1、P1352 没有上司的舞会

P1352 没有上司的舞会
在这里插入图片描述
最基础的入门题,用链式前向星建树,直接用上面总结的转移方程

{ f [ u ] [ 0 ] + = m a x ( f [ v ] [ 0 ] , f [ v ] [ 1 ] ) ; u f [ u ] [ 1 ] + = f [ v ] [ 0 ] ; u \begin{cases}f[u][0]+=max(f[v][0],f[v][1]);u不去,那么它的子节点(下属)可去可不去,取最大值即可\\f[u][1]+=f[v][0];u去了那么它的子节点一定不能去,直接加\\\end{cases}
找到根节点, a n s = m a x ( f [ r t ] [ 0 ] , f [ r t ] [ 1 ] ) ans=max(f[rt][0],f[rt][1]) ,非常简单。

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<queue>
#include<math.h>

#define ls (p<<1)
#define rs (p<<1|1)
#define mid (l+r)/2
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)

using namespace std;
typedef long long ll;//全用ll可能会MLE或者直接WA,试着改成int看会不会A
const ll N=100000;
const ll INF=1e9+9;
const ll mod=2147483647;
const double EPS=1e-10;//-10次方约等于趋近为0
const double Pi=3.1415926535897;

template<typename T>void read(T &x)
{
    x=0;char ch=getchar();ll f=1;
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
    while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}x*=f;
}
struct node
{
    ll v,nex;
}e[N];

ll n,m,a[N];
ll head[N],f[N][2],vis[N],cnt;

inline void add(ll u,ll v)//从u到v,从父节点到子节点
{
    e[++cnt].nex=head[u];
    e[cnt].v=v;
    head[u]=cnt;
}

void dp(ll u)
{
    f[u][1]+=a[u];//不用担心,每个节点只会dp到一次
    for(ll i=head[u];i;i=e[i].nex)
    {
        ll v=e[i].v;
        dp(v);
        f[u][0]+=max(f[v][0],f[v][1]);
        f[u][1]+=f[v][0];
    }
}

int main()
{
    scanf("%lld",&n);
    over(i,1,n)scanf("%lld",&a[i]);
    over(i,1,n)
    {
        ll l,k;
        scanf("%lld%lld",&l,&k);
        vis[l]++;
        //if(l&&k)
            add(k,l);//从k到l
    }
    ll rt;
    over(i,1,n)
        if(!vis[i]){rt=i;break;}
    dp(rt);
    printf("%lld\n",max(f[rt][0],f[rt][1]));
    return 0;
}

P2016 战略游戏
在这里插入图片描述
这道题的城堡是一颗树
题中有

注意,某个士兵在一个结点上时,与该结点相连的所有边将都可以被了望到。

所以定义数组 f [ i ] [ 1 / 0 ] f[i][1/0] 表示的是节点i上放士兵或者不放士兵
根据题意,如果当前节点不放置士兵,那么它的子节点必须全部放置士兵,因为要满足士兵可以看到所有的边。
所以有 { } { } { } { } { } { } { } { } { } { } { } { } { } { } { } { } { } f [ u ] [ 0 ] + = f [ v ] [ 1 ] f[u][0]+=f[v][1] { } { } { } { } { } { } { } { } { } { } { } 其中v是u的子节点
如果当前节点放置士兵,它的子节点选不选已经不重要了(因为树形dp自下而上,上面的节点不需要考虑),所以 { } { } { } { } { } { } { } { } { } { } { } { } { } { } { } f [ u ] [ 1 ] + = m i n ( f [ v ] [ 0 ] , f [ v ] [ 1 ] ) f[u][1]+=min(f[v][0],f[v][1])

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<queue>
#include<math.h>

#define ls (p<<1)
#define rs (p<<1|1)
#define mid (l+r)/2
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)

using namespace std;
typedef long long ll;//全用ll可能会MLE或者直接WA,试着改成int看会不会A
const ll N=4000;
const ll INF=1e9+9;
const ll mod=2147483647;
const double EPS=1e-10;//-10次方约等于趋近为0
const double Pi=3.1415926535897;

template<typename T>void read(T &x)
{
    x=0;char ch=getchar();ll f=1;
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
    while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}x*=f;
}

struct node
{
    ll v,nex;
}e[N];

ll head[N],cnt,n,m,k,x;
ll f[N][2];//二维的只需要0或1即可,开大了memset会超时
ll t,arr[N],rt;

inline void add(ll u,ll v)
{
    e[++cnt].nex=head[u];
    e[cnt].v=v;
    head[u]=cnt;
}

inline void init()
{
    memset(f,0,sizeof f);
    memset(head,0,sizeof head);
    memset(arr,0,sizeof arr);
    cnt=0;
}
void dfs(ll u)
{
    f[u][0]=0,f[u][1]=1;//站或不站,站则至少需要1名士兵
    for(ll i=head[u];i;i=e[i].nex)
    {
        dfs(e[i].v);//往下遍历
        f[u][0]+=f[e[i].v][1];//若不站则相邻的必须站有士兵
        f[u][1]+=min(f[e[i].v][1],f[e[i].v][0]);
    }
}

int main()
{
    while(scanf("%lld",&n)!=EOF)
    {
        init();
        over(j,1,n)
        {
            ll a,b;
            scanf("%lld%lld",&a,&b);
            over(i,1,b)
            {
                ll c;
                scanf("%lld",&c);
                arr[c]++;
                add(a,c);//树是有向图
            }
        }
        over(i,0,n)
        {
            if(!arr[i])//找根
            {
                rt=i;
                break;
            }
        }
        dfs(rt);
        printf("%lld\n",min(f[rt][1],f[rt][0]));
    }
    return 0;
}

双倍经验UVA1292 Strategic game

只有输入有些许的不同

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<queue>
#include<math.h>

#define ls (p<<1)
#define rs (p<<1|1)
#define mid (l+r)/2
#define over(i,s,t) for(register long long i=s;i<=t;++i)
#define lver(i,t,s) for(register long long i=t;i>=s;--i)

using namespace std;
typedef long long ll;//全用ll可能会MLE或者直接WA,试着改成int看会不会A
const ll N=4000;
const ll INF=1e9+9;
const ll mod=2147483647;
const double EPS=1e-10;//-10次方约等于趋近为0
const double Pi=3.1415926535897;

template<typename T>void read(T &x)
{
    x=0;char ch=getchar();ll f=1;
    while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
    while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}x*=f;
}

struct node
{
    ll v,nex;
}e[N];

ll head[N],cnt,n,m,k,x;
ll f[N][2];//二维的只需要0或1即可,开大了memset会超时
ll t,arr[N],rt;

inline void add(ll u,ll v)
{
    e[++cnt].nex=head[u];
    e[cnt].v=v;
    head[u]=cnt;
}

inline void init()
{
    memset(f,0,sizeof f);
    memset(head,0,sizeof head);
    memset(arr,0,sizeof arr);
    cnt=0;
}
void dfs(ll u)
{
    f[u][0]=0,f[u][1]=1;//站或不站,站则至少需要1名士兵
    for(ll i=head[u];i;i=e[i].nex)
    {
        dfs(e[i].v);//往下遍历
        f[u][0]+=f[e[i].v][1];//若不站则相邻的必须站有士兵
        f[u][1]+=min(f[e[i].v][1],f[e[i].v][0]);
    }
}

int main()
{
    while(scanf("%lld",&n)!=EOF)
    {
        init();
        over(j,1,n)
        {
            ll a,b;
            scanf("%lld:(%lld)",&a,&b);
            over(i,1,b)
            {
                ll c;
                scanf("%lld",&c);
                arr[c]++;
                add(a,c);//树是有向图
            }
        }
        over(i,0,n)
        {
            if(!arr[i])//找根
            {
                rt=i;
                break;
            }
        }
        dfs(rt);
        printf("%lld\n",min(f[rt][1],f[rt][0]));
    }
    return 0;
}

题型2、P2014 [CTSC1997]选课

P2014 [CTSC1997]选课
在这里插入图片描述
题意为选一门课前要看它是否有前提条件:即选了一门主课才能选 “副科”,所以可以树形背包来做。
注意是不能用分组背包来做,因为这道题附件有很多个,光是两个附件的分组背包就需要四个转移方程,在这里根本没法做。
链式前向星建树。
本身这道题的数据是一组森林,但是森林很难一起dfs所以就把所有的树根都以0为根节点建一颗大树,直接链式前向星前序遍历即可。
本题最多能选M节课
转移方程 f [ p ] [ j ] f[p][j] 是指 f [ p ] [ ] f[以p为根节点][剩余可选课数]
因为每门课都只能选一次。所以类似01背包,因此倒序来压缩空间,从三维压缩到二维
转移方程:
f [ p ] [ j ] = m a x ( f [ p ] [ j ] , f [ v ] [ k ] + f [ p ] [ j k ] ) f[p][j]=max(f[p][j],f[v][k]+f[p][j-k])
就类似01背包拿当前节点的子节点或者不拿。(子节点是必须父节点被选上的时候才可以选子节点,所以用树形背包做)
解释的应该很清楚了,代码也非常简单,有问题的话就问我

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define ls (p<<1)
#define rs (p<<1|1)
#define mid (l+r)/2
using namespace std;
typedef long long ll;
const ll N=1e3+7;
const ll mod=2147483647;
const double EPS=1e-6;
struct node
{
    ll u,v,pre;
}edge[N];
ll head[N],n,m,f[N][N],cnt;
inline void init()
{
    memset(head,-1,sizeof head);
    memset(f,0,sizeof f);
    cnt=0;
}
inline void add(ll u,ll v)
{
    edge[++cnt].pre=head[u];
    edge[cnt].v=v;
    head[u]=cnt;
}
inline void dfs(ll p)
{
    for(int i=head[p];~i;i=edge[i].pre)
    {
        ll v=edge[i].v;
        dfs(v);
        for(int j=m+1;j>=1;--j)
        {
            for(int k=0;k<j;++k)
            {
                f[p][j]=max(f[p][j],f[v][k]+f[p][j-k]);
            }
        }
    }
}
int main()
{
    init();
    scanf("%lld%lld",&n,&m);
    for(int i=1;i<=n;++i)
    {
        ll a,b;
        scanf("%lld%lld",&a,&b);
        f[i][1]=b;
        add(a,i);
    }
    dfs(0);
    printf("%lld\n",f[0][m+1]);
    return 0;
}

注:如果您通过本文,有(qi)用(guai)的知识增加了,请您点个赞再离开,如果不嫌弃的话,点个关注再走吧,日更博主每天在线答疑 ! 当然,也非常欢迎您能在讨论区指出此文的不足处,作者会及时对文章加以修正 !如果有任何问题,欢迎评论,非常乐意为您解答!( •̀ ω •́ )✧

发布了160 篇原创文章 · 获赞 150 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/weixin_45697774/article/details/105352366