zoukankan      html  css  js  c++  java
  • 树上的动规

    树形dp,在没有写过之前还以为十分的难,其实掌握了基本的几种之后还是比较容易的。

    还是回到做dp最难的一个步骤,如何定义状态?

    我们对于一颗树,无非考虑的问题有几个,当前的根是谁?它的左右孩子如何处理?我们探讨一下这个几个问题。

    我将用题目来一起代入,算是树形dp入门题了。

    先来一道普适性最高的题目

    洛谷P2015 二叉苹果树

    首先我们定义F[ x ][ i ]表示当前为x 作为根节点,在这个根节点下面留住 i 条树枝 的所能留住的最多苹果的数量,则答案就是F[ 1 ][ m ].

    那么我们怎么实现呢?

    对于当前点,我们肯定是要知道子节点的最佳状态是多少才能做出抉择,那么我们只能递归这颗树,自下往上进行操作。

    对于叶子节点,F 值就为0;那么对于一个不为叶子节点,有孩子的点怎么做出抉择呢?

    我们考虑要在递归的时候算出每一个节点下面有多少个孩子,然后进行枚举考虑要留住多少个孩子,当然如果孩子数目小于要留住的m个枝条数目,那么只用枚举到孩子的数目;同理,留住的m个枝条数目大于孩子的数目。

    于是我们可以写出       F[ x ][ i ]=max(F [ x ] [ i ],F[ x ][ i-j-1 ]+F[ u ][ j ]+value[k]);   (u 是当前根节点x的子节点,F[ u ][ j ]表示子节点u留 j 条边的最大值,那么F[ x ][ i ]表示当前根节点留 i 条边的最大值,i>j)

    #include <bits/stdc++.h>
    #define maxn 1005
    using namespace std;
    int tot,next[maxn],first[105],zhi[maxn],value[maxn],x,y,v,n,m,i,f[105][maxn],size[105]; 
    void build(int x,int y,int v)
    {
        tot++;
        next[tot]=first[x];
        first[x]=tot;
        zhi[tot]=y;
        value[tot]=v;
    }
    void dfs(int x,int father)
    {
        int k,i,j,u;
        k=first[x];
        while (k!=-1)
        {
            u=zhi[k];
            if (u==father) 
            {
                k=next[k];
                continue;
            }
            dfs(u,x);
            size[x]+=size[u]+1;
            for (i=min(size[x],m);i>=0;i--)
                for (j=min(size[u],i-1);j>=0;j--)
                    f[x][i]=max(f[x][i],f[x][i-j-1]+f[u][j]+value[k]);
            k=next[k];
        }
    }
    int main()
    {
        cin>>n>>m;
        memset(first,-1,sizeof(first));
        for (i=1;i<=n-1;i++) 
        {
            cin>>x>>y>>v;
            build(x,y,v);
            build(y,x,v);
        }
        dfs(1,0);
        cout<<f[1][m]<<endl;
        return 0;
     } 
    View Code

    洛谷P1352 没有上司的舞会

    这道就比较简单了

    f[x][0]表示以x为根的子树,且x不参加舞会的最大快乐值

    f[x][1]表示以x为根的子树,且x参加了舞会的最大快乐值

    需要注意的是 上司参加了舞会,下属一定不能参加;而上司不参加舞会,下属不是一定要参加舞会,因为数据有负值出现

     状态转移方程:       f[x][1]+=f[u][0];
                   f[x][0]+=max(f[u][0],f[u][1]);

    #include <bits/stdc++.h>
    #define maxn 6005
    using namespace std;
    int tot,next[maxn*10],first[maxn],zhi[maxn*10],rong[maxn],i,n,f[maxn][2],l,k;
    void build (int x,int y)
    {
        tot++;
        next[tot]=first[x];
        first[x]=tot;
        zhi[tot]=y;
        rong[y]++;
    }
    void dfs(int x)
    {
        int k,u;
        k=first[x];
        while (k!=-1)
        {
            u=zhi[k];
            dfs(u);
            f[x][1]+=f[u][0];
            f[x][0]+=max(f[u][0],f[u][1]);
            k=next[k];
        }
    }
    int main()
    {
        scanf("%d",&n);
        memset(first,-1,sizeof(first));
        for (i=1;i<=n;i++) scanf("%d",&f[i][1]);
        for (i=1;i<=n;i++) 
        {
            scanf("%d%d",&l,&k);
            if (l!=0) build(k,l);
        }
        for (i=1;i<=n;i++)
            if (rong[i]==0) 
            {
                dfs(i);
                cout<<max(f[i][0],f[i][1]);
            }
        return 0;
    }
    View Code

    这两道算是最基本的树形dp模型了。

    现在来点难的。换根dp

    换根dp思想为:

    1.随便找一个点作为根进行常规树形dp

    2.再以原来点为根进行dp,通过找父亲与孩子之间的关系,寻找孩子节点的答案

    换根dp的通法:

    1.第一次dfs时,任选一个点为根,在“有根树”上执行一次树形DP,从自底向上的状态转移

    2.第二次dfs时,从刚才选出的根出发,对整棵树执行一次dfs,在每次递归前进行自上向底的推导,计算出换根后的解。

    这两个都是其他博主总结出来的,笔者觉得很是精辟,可谓一语击中,可以说是把换根dp的重点难点都说出来了。

    POJ 3585

    这题能说是典型网络流的题目,但是由于题目给的数据大小和给出来的是一棵树,导致只能用树形dp做。

    我们先第一遍常规dfs进行dp ,这里找 1为根节点。

    这时候来第二遍dfs,我们可以看到对于一个点,如果它有子树的话,那么这个点现有的答案是可以用的,因为第一遍dfs我们求出来每个点的dp值代表了每个点流向子树的最大流量,因此我们只需考虑这个点的“上面”的最大流量是多少(来自父亲节点的最大流量)。

    不就可以这么把父亲节点的dp值减去当前节点的dp值,就是“上面”的最大流量吗?

    换句话说,父亲节点dp值=左子树最大流量+右子树最大流量。

    现在假设当前节点就是父亲节点的右孩子,所以当前节点dp值=右子树最大流量,

    我们现在问题是要算可以从“上面”流向的最大流量是多少,那么意思不就是这些流量先经过父亲节点,然后再留过左子树。

    因此f[y]=dp[y]+min(f[x]−min(dp[y],len[p]),len[p]);

    f[y]=dp[y]+{min(f[x]min(dp[y],len[p]),len[p])len[p]du[x]>1du[x]=1

    最外层的min是判断一下当前节点流向父亲节点的那条管道是否有超过限制,同理内层的min

    #include <iostream>
    #include <algorithm>
    #include <cstdio>
    #include <cstring>
    #define maxn 1000000
    using namespace std;
    int next[maxn],first[maxn],zhi[maxn],value[maxn],du[maxn],i,t,n,x,y,v,maxx,tot,f[maxn],ans[maxn];
    void add(int x,int y,int v)
    {
        tot++;
        next[tot]=first[x];
        first[x]=tot;
        zhi[tot]=y;
        value[tot]=v;
        du[y]++;
    }
    void dp(int x,int father)
    {
        int i,u,k;
        for (k=first[x];k!=-1;k=next[k])
        {
            u=zhi[k];
            if (u==father) continue;
            dp(u,x);
            if (du[u]!=1) f[x]+=min(f[u],value[k]);
            if (du[u]==1) f[x]+=value[k];
        }
    }
    void dfs(int x,int father)
    {
        int i,u,k;
        for (k=first[x];k!=-1;k=next[k])
        {
            u=zhi[k];
            if (u==father) continue;
            if (du[x]==1) ans[u]=f[u]+value[k];
                else ans[u]=f[u]+min(ans[x]-min(f[u],value[k]),value[k]);
            dfs(u,x);
        }
    }
    int main()
    {
        cin>>t;
        while (t--)
        {
            cin>>n;
            tot=0;
            memset(first,-1,sizeof(first));
            memset(f,0,sizeof(f));
            memset(du,0,sizeof(du));
            for (i=1;i<n;i++) 
            {
                cin>>x>>y>>v;
                add(x,y,v);
                add(y,x,v);
            }
            dp(1,0);
            ans[1]=f[1];
            maxx=0;
            dfs(1,0);
            for (i=1;i<=n;i++) maxx=max(maxx,ans[i]);
            cout<<maxx<<endl;
        }
        return 0;
     } 
     /*
    2
    5
    1 2 11
    1 4 13
    3 4 5
    4 5 10
    5
    1 2 11
    1 4 13
    3 4 5
    4 5 10
    */
    View Code

    CCPC Wannafly day3 G

    这题难度就上来了,可能先看一下这题先才会更容易理解这里究竟怎么做。AcWing1073

    CCPC这题就直接在代码解释。

    #include <bits/stdc++.h>
    #define maxn 1000005
    using namespace std;
    typedef long long ll;
    ll size[maxn],res[maxn],sum[maxn],Big_max[maxn],Small_max[maxn],up[maxn];
    int n,m,tot,txen[maxn],f[maxn],first[maxn],zhi[maxn],value[maxn],fa[maxn],x,y,v,i;
    void add(int x,int y,int v)
    {
        tot++;
        txen[tot]=first[x];
        first[x]=tot;
        zhi[tot]=y;
        value[tot]=v;
    }
    void dfs(int x,int father)
    {
        int k,u;
        if (f[x]) size[x]=1;
        for (k=first[x];k!=-1;k=txen[k])
        {
            u=zhi[k];
            if (u==father) continue;
            dfs(u,x);
            size[x]+=size[u];//size数组意思是以1为根,x的子树中需要旅行的国家数量; 
            sum[x]+=sum[u]; 
            if (size[u])
            {
                sum[x]+=2*value[k];// 对于有要旅行的国家才统计长度 
                if (Big_max[x]<=Big_max[u]+value[k])
                {
                    Small_max[x]=Big_max[x];//选次大的路径  原因看acwing那一题 
                    Big_max[x]=Big_max[u]+value[k];//选最大的路径 
                    fa[x]=u;//记录最大的路径最后能走到哪 
                }
                else 
                if (Small_max[x]<Big_max[u]+value[k]) Small_max[x]=Big_max[u]+value[k];
            }
        }
    }
    void dp(int x,int father)
    {
        int k,u;
        for (k=first[x];k!=-1;k=txen[k])
        {
            u=zhi[k];
            if (u==father) continue;
            if (m-size[u])  // 如果当前点的子树下面不全包含所有要旅行的国家,那么需要往父亲节点的方向去走 
            {
                //up表示以1为根,x通过父亲走到国家的最大值
                if (fa[x]==u) up[u]=max(up[x],Small_max[x])+value[k];//如果当前点在以1为根的最长旅行路径上的话,我们需要尝试往父亲节点方向走次大的路径,因为往下走就是刚刚求出来的最长路径 
                    else up[u]=max(up[x],Big_max[x])+value[k]; //当前点不在以1为根的最长旅行路径上的话,肯定是要走最长路径 
            }
            res[u]=res[x];//res表示以x为根,到所有国家并返回的路径和;
            if (size[u]) res[u]-=value[k]*2;//如果下面有要旅行的国家,我们尝试切断与父亲节点的联系 
            if (m-size[u]) res[u]+=value[k]*2;//如果往父亲节点方向走有要旅行的国家,恢复父亲节点的联系
            dp(u,x);
        }
    }
    int main()
    {
        scanf("%d%d",&n,&m);
        memset(first,-1,sizeof(first));
        for (i=1;i<n;i++)
        {
            scanf("%d%d%d",&x,&y,&v);
            add(x,y,v);
            add(y,x,v);
        }
        for (i=1;i<=m;i++) cin>>x,f[x]=1;
        dfs(1,0);
        res[1]=sum[1];
        dp(1,0);
        for (i=1;i<=n;i++) printf("%lld
    ",res[i]-max(up[i],Big_max[i]));//不能保证当前点走次大路径后可能比走最大路径的值会大 
        return 0;
    }
    View Code

    对了,对于一个点到所要旅行的所有点的最小路径和,等于他到所有要旅行的点的路径和×2 再减去最长的那条路径的长度。想想为什么呢?顺便吐槽一句,官方题解说是虚树,反正我这里没有一点虚树的做法,我也不会写虚树。

  • 相关阅读:
    Jser 设计模式系列之面向对象
    jQuery 2.0.3 源码分析 回溯魔法 end()和pushStack()
    jQuery 2.0.3 源码分析 数据缓存
    jQuery 2.0.3 源码分析 Deferred(最细的实现剖析,带图)
    jQuery 2.0.3 源码分析 Deferred概念
    jQuery 2.0.3 源码分析 回调对象
    试试看 ? 离奇古怪的javascript题目
    jQuery 2.0.3 源码分析Sizzle引擎
    设计模式之美:Memento(备忘录)
    设计模式之美:Mediator(中介者)
  • 原文地址:https://www.cnblogs.com/Y-Knightqin/p/12322289.html
Copyright © 2011-2022 走看看