树形DP(树形动态规划)算法 + 例题(树的重心,树上最远距离...)

一、 简介:

树形DP就是在树的数据结构上计算DP值。

树形DP有两个方向:叶->根、根->叶。

树形DP通过记忆化搜索实现,因此采用递归实现。

时间复杂度一般为O(n),若有维数m,则为O(n*m)。

二、 经典问题:

 1.  树的重心:http://poj.org/problem?id=1655   叶->根

所谓重心就是各分支大小围绕该点能较均匀的分部,所以要求最大的分支大小最小。

建树完成后先以任意一点为根节点进行一次DFS,计算所有点所连子树大小。

如下图所示,以1为根,进行DFS,得到的结果为{5, 3, 1, 1, 1},那么接下来计算以任意一点如2为根的子树大小时,只需比较2直接所连4,5的大小以及(总节点数n-节点2的大小)即可。

看代码。

#include <iostream>
#include <stdio.h>
#include <string.h>

using namespace std;

const int N=2e5+5;

int n;


struct node{               //链式前向星存边
    int from;
    int to;
    int next;
}edge[2*N];
int head[N];
int cnt;
void add(int from,int to)
{
    cnt++;
    edge[cnt].from=from;
    edge[cnt].to=to;
    edge[cnt].next=head[from];
    head[from]=cnt;
}


int pre[N],sz[N];          //pre用于记录某节点的父节点,sz用于记录某节点所连子树大小。
int dfs(int p,int u)       //随机以某点为根DFS
{
    pre[u]=p;    //记录父节点
    sz[u]=1;
    for(int i=head[u];i!=0;i=edge[i].next)    
    {
        int to = edge[i].to;
        if(to!=p) sz[u] += dfs(u, to);
    }
    return sz[u];
}


int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        cnt=0;
        memset(head,0,sizeof(head));
        memset(edge,0,sizeof(edge));
        memset(pre,0,sizeof(pre));
        memset(sz,0,sizeof(sz));
        scanf("%d",&n);
        for(int i=1;i<n;i++)
        {
            int x,y;
            scanf("%d%d",&x, &y);
            add(x,y);
            add(y,x);
        }
        dfs(0,1);

        int ansV=N;    //记录最大Balance和节点序号
        int ansO=N;
        for(int i=1;i<=n;i++)
        {
            int mx = n - sz[i];    //当以i为中心时唯一需要特殊判断的地方,先设为最大值

            for(int j=head[i];j!=0;j=edge[j].next)    //接下来比较i所有直接连接的子树大小
            {
                int to=edge[j].to;
                if(to!=pre[i]) mx = max(mx, sz[to]);
            }

            if(mx < ansV)    //得到以i为中心时最大balance,与答案比较取小
            {
                ansV = mx;
                ansO = i;
            }
        }
        printf("%d %d\n",ansO,ansV);
    }
}

2.  没有上司的聚会:http://poj.org/problem?id=2342  叶->根

扫描二维码关注公众号,回复: 9414723 查看本文章

简单来说就是给你一个公司的树形结构图,每个人有一个欢乐值,当一个人出现时他的直接上司不能出现,要求总欢乐值最大。

设dp[i][1]表示i出现时的最大欢乐值,dp[i][0]表示i不出现时的最大欢乐值, a[i]表示i自己的欢乐值。

设u为v的上司,则有dp[u][1] = a[u] + dp[v][0],dp[u][0] = max(dp[v][0], dp[v][1])。

由叶向根逐步更新即可。

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

using namespace std;

const int N=6e3+5;

int n;
int dp[N][2],pre[N];    //pre储存某点父节点



int head[N],cnt;    //链式前向星存边
struct nodes{
    int from;
    int to;
    int next;
}edge[N*2];


void Dfs(int node)
{
    for(int i=head[node];i!=0;i=edge[i].next)
    {
        int to=edge[i].to;
        Dfs(to);
        dp[node][1]+=dp[to][0];
        dp[node][0]+=max(dp[to][1],dp[to][0]);
    }
}


int main()
{
    while(cin>>n)
    {
        memset(dp,0,sizeof(dp));
        memset(pre,0,sizeof(pre));
        memset(head,0,sizeof(head));
        memset(edge,0,sizeof(edge));
        cnt=0;


        for(int i=1;i<=n;i++)
        {
            scanf("%d",&dp[i][1]);    //直接把a[i]存入dp[i][1]
        }

        int x,y;
        while(scanf("%d%d",&x,&y)!=-1)
        {
            if(x==0 && y==0) break;

            cnt++;                //有向
            edge[cnt].from=y;
            edge[cnt].to=x;
            edge[cnt].next=head[y];
            head[y]=cnt;

            pre[x]=y;
        }

        int root;
        for(int i=1;i<=n;i++)    //找根节点(根节点上级为0)
        {
            if(pre[i]==0)
            {
                root=i;
                break;
            }
        }
        Dfs(root);
        printf("%d\n",max(dp[root][0],dp[root][1]));
    }

}

啰嗦一下,之前有看到一种不用链式前向星存边直接暴力的方法,说实话能过是因为这题数据比较水,我试着写了一下,跑完数据用掉567ms,而链式前向星只要78ms,差距还是很大的,这题数据才6e3。不过从中有一个意外收获,看看if的神奇之处。

众所周知if在 && 条件下当前一个条件成立时就不会执行后面的判断语句,所以应该把能过滤掉较多答案的条件放在第一个,避免多余浪费时间的判断(像平时 || 能转成 && 的情况也要注意)。

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

using namespace std;

const int N=6e3+5;

int n;
int dp[N][2];
int pre[N];
bool vist[N];

void Dfs(int node)
{
    vist[node]=true;
    for(int i=1;i<=n;i++)
    {
        if(pre[i]==node && vist[i]==false)  //如果将if中两个条件调换位置直接超时
        {                                   //能过滤掉较多选项的是第一个条件
            Dfs(i);
            dp[node][1]+=dp[i][0];
            dp[node][0]+=max(dp[i][0], dp[i][1]);
        }
    }
}

int main()
{
    while(cin>>n)
    {
        memset(dp,0,sizeof(dp));
        memset(pre,0,sizeof(pre));
        memset(vist,false,sizeof(vist));
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&dp[i][1]);
        }
        int x,y;
        while(scanf("%d%d",&x,&y)!=-1)
        {
            if(x==0 && y==0) break;
            pre[x]=y;
        }
        int root=0;
        for(int i=1;i<=n;i++)
        {
            if(pre[i]==0)
            {
                root=i;
                break;
            }
        }
        Dfs(root);
        printf("%d\n",max(dp[root][1],dp[root][0]));
    }
}

3. 树上最远距离:http://acm.hdu.edu.cn/showproblem.php?pid=2196  叶->根 && 根->叶

简单来说就是有一颗树,每条边有一个权值,求每一个点能到达的最远距离。

首先在一个树中,对于某一个点,它最远距离可以来自父节点方向或者子节点方向。

设dp[i][1]为i往父节点方向的最远距离,dp[i][0]为i往子节点方向的最远距离, pre[i]表示i的父节点。

对于子节点方向,设u为v父节点,有 dp[u][0] = max{dp[v][0] + w(u,v)},通过DFS自底向上求出即可。

对于父节点方向,设u,v为兄弟节点,有 dp[u][1] = w(u, pre[u]) + max{ dp[pre[u]][1],  dp[v][0] + w(pre[u], v) }。

直观来说,即往父节点方向的最远距离 = 该节点到父节点的距离 + (父节点继续往父节点方向走 || 父节点往其它兄弟子节点走) 的最大值。从根开始向叶DFS即可。

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

using namespace std;

const int N=1e4+5;

int n;

int dp[N][2],prevs[N];


int head[N],cnt;        //链式前向星存边
struct nodes{
    int from;
    int to;
    int value;
    int next;
}edge[2*N];

void Add(int x,int y,int v)
{
    cnt++;
    edge[cnt].from=x;
    edge[cnt].to=y;
    edge[cnt].value=v;
    edge[cnt].next=head[x];
    head[x]=cnt;
}




int Dfs(int node,int pre)        //计算往子节点方向最远距离
{
    for(int i=head[node];i!=0;i=edge[i].next)
    {
        int to=edge[i].to;
        if(to==pre) continue;
        int value=edge[i].value;
        prevs[to]=node;
        dp[node][0]=max(dp[node][0],value+Dfs(to,node));
    }
    return dp[node][0];
}



void Dfs2(int node,int pre)       //计算往父节点方向最远距离
{
    int v=0;               //记录该点到父节点的距离
    dp[node][1]=dp[pre][1];    //混入父节点与兄弟节点的判断

    for(int i=head[pre];i!=0;i=edge[i].next)    //拿出所有兄弟边比较,看看父节点下一步该往哪走
    {
        int to=edge[i].to;
        if(to==prevs[pre]) continue;
        int value=edge[i].value;
        if(to==node) v=value;
        else dp[node][1]=max(dp[node][1], dp[to][0]+value);
    }


    dp[node][1]+=v;          //选完父节点下一步后再把该点到父节点的距离加上去

    for(int i=head[node];i!=0;i=edge[i].next)
    {
        int to=edge[i].to;
        if(to!=pre) Dfs2(to,node);
    }
}


void Origin()    //初始化函数
{
    memset(prevs,0,sizeof(prevs));
    memset(dp,0,sizeof(dp));
    memset(head,0,sizeof(head));
    cnt=0;
    memset(edge,0,sizeof(edge));
}


int main()
{
    while(cin>>n)
    {
        Origin();
        int p,v;
        for(int i=1;i<n;i++)
        {
            scanf("%d%d",&p,&v);
            Add(i+1,p,v);
            Add(p,i+1,v);
        }
        Dfs(1,0);
        Dfs2(1,0);
        for(int i=1;i<=n;i++)
        {
            printf("%d\n",max(dp[i][0], dp[i][1]));
        }
    }
}

三、 拓展问题:

1. Godfather:http://poj.org/problem?id=3107

树的重心问题的拓展。

#include <iostream>
#include <stdio.h>
#include <string.h>

using namespace std;

const int N=5e4+5;

int n;

int mxOfNode[N];

int head[N],cnt=0;
struct node{
    int from;
    int to;
    int next;
}edge[2*N];

void Add(int x,int y)
{
    cnt++;
    edge[cnt].from=x;
    edge[cnt].to=y;
    edge[cnt].next=head[x];
    head[x]=cnt;
}


int pre[N],sz[N];

int Dfs(int prev,int node)
{
    pre[node]=prev;
    sz[node]=1;
    for(int i=head[node];i!=0;i=edge[i].next)
    {
        int to=edge[i].to;
        if(to!=prev) sz[node] += Dfs(node,to);
    }
    return sz[node];
}



int main()
{
    cin>>n;
    for(int i=1;i<n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        Add(x,y);
        Add(y,x);
    }
    Dfs(0,1);

    int ansV=N;
    for(int i=1;i<=n;i++)
    {
        int mx = n-sz[i];
        for(int j=head[i];j!=0;j=edge[j].next)
        {
            int to=edge[j].to;
            if(to!=pre[i]) mx = max(mx,sz[to]);
        }
        mxOfNode[i]=mx;
        if(mx<=ansV)
        {
            ansV=mx;
        }
    }
    int cnt=0;
    for(int i=1;i<=n;i++)
    {
        if(mxOfNode[i]==ansV)
        {
            if(cnt==0) printf("%d",i);
            else printf(" %d",i);
            cnt=1;
        }
    }
    printf("\n");

}

2. The more, The Better:http://acm.hdu.edu.cn/showproblem.php?pid=1561

背包类树形DP

把可以先攻克的节点作为父节点,形成一颗树,树构成森林。

由于题中有父节点为0这种情况,我们可以把所有森林中树的父节点都视为0,这样就把森林变成一颗树。

设dp[i][j]表示第i个节点中取j个节点的最大价值。

设u为v的父节点,有dp[u][j] = max( dp[u][j], dp[u][k] + dp[v][j-k] )

自底向上更新即可。

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

using namespace std;

const int N=205;

int n,m;

int head[N],cnt;
struct nodes{
    int from;
    int to;
    int next;
}edge[2*N];

int dp[N][N];
bool vist[N];

void Origin()
{
    memset(head,0,sizeof(head));
    cnt=0;
    memset(edge,0,sizeof(edge));
    memset(dp,0,sizeof(dp));
    memset(vist,false,sizeof(vist));
}

void Dfs(int node)
{
    vist[node]=true;
    for(int i=head[node];i!=0;i=edge[i].next)
    {
        int to=edge[i].to;
        if(vist[to]==false) Dfs(to);
        for(int j=m;j>=2;j--)
        {
            for(int k=1;k<j;k++)
            {
                dp[node][j]=max(dp[node][j],dp[node][k]+dp[to][j-k]);
            }
        }


    }
}

int main()
{
    while(cin>>n>>m)
    {
        Origin();
        if(n==0 && m==0) break;
        int x,y;
        for(int i=1;i<=n;i++)
        {
            scanf("%d%d",&x,&y);

            cnt++;                //单向
            edge[cnt].from=x;
            edge[cnt].to=i;
            edge[cnt].next=head[x];
            head[x]=cnt;

            dp[i][1]=y;
        }
        m++;    //因为我们虚拟多处了一个0节点
        Dfs(0);
        printf("%d\n",dp[0][m]);
    }
}
发布了34 篇原创文章 · 获赞 26 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/sinat_40471574/article/details/100014642