树上的动规

树形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 再减去最长的那条路径的长度。想想为什么呢?顺便吐槽一句,官方题解说是虚树,反正我这里没有一点虚树的做法,我也不会写虚树。

原文地址:https://www.cnblogs.com/Y-Knightqin/p/12322289.html