• 算法复习——树形dp


    树形dp的状态转移分为两种,一种为从子节点到父节点,一种为父节点到子节点,下面主要讨论子节点到父亲节点的情况:

    例题1(战略游戏):

    这是一道典型的由子节点状态转移到父节点的问题,而且兄弟节点之间没有相互影响,我们用f[i][0]/f[i][1]表示i不取/要取时其所在子树总共最少取的节点数,不难得出dp方程:

    代码:

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cmath>
    #include<cstring>
    #include<string>
    #include<algorithm>
    #include<cstdio>
    #include<cstdlib>
    #include<vector>
    using namespace std;
    const int N=1505;
    int first[N],next[N*2],go[N*2],tot,n,father[N],f[N][2];
    vector<int>son[N];
    inline int R()
    {
      char c;int f=0;
      for(c=getchar();c<'0'||c>'9';c=getchar());
      for(;c<='9'&&c>='0';c=getchar())
        f=(f<<3)+(f<<1)+c-'0';
      return f;
    }
    inline void comb(int a,int b)
    {
      next[++tot]=first[a],first[a]=tot,go[tot]=b;
      next[++tot]=first[b],first[b]=tot,go[tot]=a;
    }
    inline void dfs(int u,int fa)
    {
      for(int e=first[u];e;e=next[e])
      {
        int v=go[e];
        if(v==fa)  continue;
        father[v]=u;
        son[u].push_back(v);
        dfs(v,u);
      }
    }
    inline void dp(int u)
    {
      int temp=1e+8;
      if(!son[u].size())
      {
        f[u][0]=0;f[u][1]=1;
        return;
      }
      for(int i=0;i<son[u].size();i++)
      {
        dp(son[u][i]);
        f[u][0]+=f[son[u][i]][1];
        f[u][1]+=min(f[son[u][i]][1],f[son[u][i]][0]);
      }
      f[u][1]++;
    }
    int main()
    {
      //freopen("a.in","r",stdin);
      int a,b,c;  
      n=R();
      for(int i=1;i<=n;i++)
      {
        a=R(),b=R();
        for(int j=1;j<=b;j++)
          c=R(),comb(a,c);
      }
      dfs(0,0);dp(0);
      int ans=min(f[0][1],f[0][0]);
      cout<<ans<<endl;
      return 0;
    }

    通过这道题我们也可以看出,如果兄弟节点间并不存在相互影响制约的关系,dp的第二维通常是非常小的

    那么当兄弟节点间会相互影响时,又该怎么办,我们先讨论较为简单的二叉树的情况:

    例题2(二叉苹果树):

    首先将保留树枝数量转化成保留节点数量+1,(不转化也可以),然后每个树枝上的苹果数量设为相应节点的点权,我们发现,如果用f[i][k]表示i所在子树总共留下了k个节点,由于儿子节点只有两个,我们就可以枚举左儿子保留的节点数量x,则右儿子即为k-x-1,推出dp方程:

        f[i][k]=max{f[lson][x]+f[rson][k-x-1],f[i][k]},0<=x<k

    代码:

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cmath>
    #include<ctime>
    #include<cctype>
    #include<cstring>
    #include<string>
    #include<algorithm>
    using namespace std;
    const int N=105;
    int first[N],next[N*2],go[N*2],val[N*2],tot;
    int n,m;
    int dp[N][N];
    inline void comb(int a,int b,int c)
    {
      next[++tot]=first[a],first[a]=tot,go[tot]=b,val[tot]=c;
      next[++tot]=first[b],first[b]=tot,go[tot]=a,val[tot]=c; 
    }
    inline void dfs(int u,int fa,int Val,int k)
    {
      if(u==0||k==0)  
      {
        dp[u][k]=0;
        return;
      }
      if(dp[u][k]!=-1)
        return;
      dp[u][k]=0;
      for(int i=0;i<k;i++)
      { 
        int l=0,r=0,vl,vr;
        for(int e=first[u];e;e=next[e])
        {
          int v=go[e];
          if(v==fa)  continue;   
          if(l==0) 
            l=v,vl=val[e];
          else 
          {  
            r=v,vr=val[e];
            break;
          }
        }
        dfs(l,u,vl,i);
        dfs(r,u,vr,k-i-1);
        dp[u][k]=max(dp[l][i]+dp[r][k-i-1]+Val,dp[u][k]);
      }
      return;
    }
    int main()
    {
      //freopen("a.in","r",stdin);
      scanf("%d%d",&n,&m);
      m++;
      memset(dp,-1,sizeof(dp));
      int a,b,c;
      for(int i=1;i<n;i++)
      {
        scanf("%d%d%d",&a,&b,&c);
        comb(a,b,c);
      }
      dfs(1,0,0,m);
      cout<<dp[1][m]<<endl;
      return 0;
    }

    通过这道题,我们可以发现,如果兄弟节点间存在着相互影响的情况,那么常常会在dfs之前枚举儿子状态,保证儿子的状态不冲突,由于枚举会出现重复的情况,我们也常常会用到记忆化搜索

    上面只是两个儿子的情况,如果面对多个儿子,上面的方法显然是不可取的

    例题3(选课):

    根据题意由先修课向它的课程连边,建成一颗树,树的点权对应课程学分

    此时我们必须将多叉树转化为二叉树:把第一个孩子作为父节点的左子树,其它孩子作为第一个孩子的右子树。

    具体实现为:每次读入一个节点i的儿子j,我们将son[i]=j,而将brother[j]设为之前的son[i],也就是离j最近的兄弟节点。

    设f[u][k]为新建的二叉树的后,节点u所在子树保留k个儿子能得到得最多学分,可以得出dp方程:

      f[u][k]=max(f[u][k],f[son[u]][i]+dp[brother[u]][k-i-1]+val[u]),0<=i<k;

    其中val表示该节点对应课程的的学分

    由实际意义不难发现,我们这样枚举少考虑了一个情况,也就是当前u节点所对应的原来的图的子树的节点都不取的情况,

    因此在最后我们还要这样:

      f[u][k]=max(f[u][k],f[brother[u]][k])

    多叉树转二叉树的方法是面对分配附加维时常用的一种方法

    例题4(偷天换日,洛谷3360):

    首先要考虑的是这道题的建图,如果根据题意新建一个图是在太麻烦,因此我们可以直接考虑边读入信息边再已知的树上dfs,(详细见代码)

    用f[i][j]表示已i为节点的子树(包含i与其父亲连接的边)中花费j的时间能获得的最大价值

    当我们dfs到展览室的时候,f[i][j]的计算无疑是一个01背包问题,在此不多叙述

    其他情况我们可以发现,除去末端展览室的情况,树的其他部分都是一颗二叉树,因此直接用我们上面提到的方法即可:

    f[x][i]=max(f[x][i],f[l][j]+f[r][i-j-dis]);dis<=i<=n,j<=i-dis;

    其中dis表示i与其父节点连接的边的权值;

    代码如下:(引用洛谷题解)

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    #define ll long long
    ll n,m=1;
    ll f[1001][1001]={0};
    ll value[1001],tim[1001];
    inline void sca(ll x)
    {
        ll dis,pd;
        scanf("%lld%lld",&dis,&pd);
        dis*=2;
        if(pd)
        {
            for(ll j=1;j<=pd;j++)
            scanf("%lld%lld",&value[j],&tim[j]);
            for(ll i=1;i<=pd;i++)
                for(ll j=n;j>=tim[i];j--)
                if(j-tim[i]>=dis)
                f[x][j]=max(f[x][j],f[x][j-tim[i]]+value[i]);
            return;
        }
        ll l=++m;sca(m);
        ll r=++m;sca(m);
        for(ll i=dis;i<=n;i++)
        for(ll j=0;j<=i-dis;j++)f[x][i]=max(f[x][i],f[l][j]+f[r][i-j-dis]);    
    }
    int main()
    {
        scanf("%lld",&n);n--;
        sca(1);
        printf("%lld",f[1][n]);
        return 0;
    }

    例题5(骑士,bzoj1040):

    首先想到的是如果按照痛恨关系建边的话有两种情况:有一个环或者为一颗树,为树的话就和例题1一样了,十分简单,因此主要考虑有环的情况.

    为了让环更好处理,我们选择从被痛恨者向痛恨他的人连一条边····这样整个图即为奇环外向树(只有一个环且没有环外的点指向环内的点的图)

    这样的话我们可以找环上任意一点u让它与它的父节点father[u]断开,然后以它为根进行dp,dp方程与例题1一样。

    然而这样会出现冲突,因为最优答案有可能是由f[father[u]][1]更新过来的,因此我们要将环上的的点单独处理

    首先直接将f[father[u]][1]赋值为f[father[u]][0]这样的话可以保证u取了而father[u]一定没有取,然后依次往father[father[u]],father[father[father[u]]]·····跳,即重新遍历整个环,依次更新遍历的点的f[][0],f[][1],直到跳到u为止

    最后只更新f[u][1]就好了,ans加上max{f[u][1],f[u][0]}.

    代码:

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cmath>
    #include<ctime>
    #include<cctype>
    #include<cstring>
    #include<string>
    #include<algorithm>
    using namespace std;
    const int N=1e6+3;
    int first[N],next[N],go[N],tot,n,val[N],father[N],g[N];
    long long ans=0,f[N][2];
    bool visit[N];
    inline int R()
    {
      char c;int f=0;
      for(c=getchar();c<'0'||c>'9';c=getchar());
      for(;c<='9'&&c>='0';c=getchar())
        f=(f<<3)+(f<<1)+c-'0';
      return f;
    }
    inline void comb(int a,int b)
    {
      next[++tot]=first[a],first[a]=tot,go[tot]=b;
    }
    inline void dfs(int u)
    {
      visit[u]=true;f[u][1]=val[u];
      for(int e=first[u];e;e=next[e])
      {
        int v=go[e];
        if(!visit[v])  
        { 
          dfs(v);
          f[u][0]+=max(f[v][0],f[v][1]);
          f[u][1]+=f[v][0];
        } 
      }
    }
    inline void dp(int a)
    {
      int root;
      for(root=a;g[root]!=a;root=father[root])
        g[root]=a;
      dfs(root);
      int b=father[root];
      f[b][1]=f[b][0];
      for(int x=father[b];x!=root;x=father[x])
      {
        f[x][1]=val[x],f[x][0]=0;
        for(int e=first[x];e;e=next[e])
        {
          int v=go[e];
          f[x][0]+=max(f[v][0],f[v][1]);
          f[x][1]+=f[v][0];
        }
      }
      f[root][1]=val[root];
      for(int e=first[root];e;e=next[e])
      {
        int v=go[e];
        f[root][1]+=f[v][0];
      }
      ans+=max(f[root][1],f[root][0]);
    }
    int main()
    {
      //freopen("a.in","r",stdin);
      n=R();int a,b;
      for(int i=1;i<=n;i++)
      {
        a=R(),b=R();
        val[i]=a,comb(b,i);
        father[i]=b;
      }
      for(int i=1;i<=n;i++)  
        if(!visit[i])
          dp(i);
      printf("%lld",ans);
      return 0;
    }

     例题6(bzoj1023仙人掌)

    Description

      如果某个无向连通图的任意一条边至多只出现在一条简单回路(simple cycle)里,我们就称这张图为仙人掌
    图(cactus)。所谓简单回路就是指在图上不重复经过任何一个顶点的回路。

     

      举例来说,上面的第一个例子是一张仙人图,而第二个不是——注意到它有三条简单回路:(4,3,2,1,6
    ,5,4)、(7,8,9,10,2,3,7)以及(4,3,7,8,9,10,2,1,6,5,4),而(2,3)同时出现在前两
    个的简单回路里。另外,第三张图也不是仙人图,因为它并不是连通图。显然,仙人图上的每条边,或者是这张仙
    人图的桥(bridge),或者在且仅在一个简单回路里,两者必居其一。定义在图上两点之间的距离为这两点之间最
    短路径的距离。定义一个图的直径为这张图相距最远的两个点的距离。现在我们假定仙人图的每条边的权值都是1
    ,你的任务是求出给定的仙人图的直径。

    Input

      输入的第一行包括两个整数n和m(1≤n≤50000以及0≤m≤10000)。其中n代表顶点个数,我们约定图中的顶
    点将从1到n编号。接下来一共有m行。代表m条路径。每行的开始有一个整数k(2≤k≤1000),代表在这条路径上
    的顶点个数。接下来是k个1到n之间的整数,分别对应了一个顶点,相邻的顶点表示存在一条连接这两个顶点的边
    。一条路径上可能通过一个顶点好几次,比如对于第一个样例,第一条路径从3经过8,又从8返回到了3,但是我们
    保证所有的边都会出现在某条路径上,而且不会重复出现在两条路径上,或者在一条路径上出现两次。

    Output

      只需输出一个数,这个数表示仙人图的直径长度。

    Sample Input

    15 3
    9 1 2 3 4 5 6 7 8 3
    7 2 9 10 11 12 13 10
    5 2 14 9 15 10 8
    10 1
    10 1 2 3 4 5 6 7 8 9 10

    Sample Output

    8
    9

    HINT

    对第一个样例的说明:如图,6号点和12号点的最短路径长度为8,所以这张图的直径为8。


     


    【注意】使用Pascal语言的选手请注意:你的程序在处理大数据的时候可能会出现栈溢出。

    如果需要调整栈空间的大小,可以在程序的开头填加一句:{$M 5000000},其中5000000即

    指代栈空间的大小,请根据自己的程序选择适当的数值。

    这道题不得不说是真tm难啊····

    题解详见:http://z55250825.blog.163.com/blog/static/150230809201412793151890/

    说说我理解的吧····先进行一遍dfs造一颗树用f[i]表示从i往深处的链能到达的最大距离,注意是链,不是环·····,那么用这个尝试更新答案,(用u的儿子的f的次大值和最大值的和+1来更新)

    然后我们要讨论的就是如何处理环上的情况····

    对于环的话我们找到环上深度最高的点u,我们要按照f的定义求出f[u],(这里有点难想,设i为环上除u外的点,那么我们可以推出f[u]=max{f[i]+dis(u,i)},于是以后dfs到环上的话答案一定由f[u]更新而来

    接下来我们就要解决如何算f[u],并且在更新答案时我们还忽略了环上的点可以更新答案,打个比方,如果环上有两个点i,j,那么答案可能由f[i]+f[j]+dis(i,j)更新而来·····

    我们要先提取出这个环,然后利用单调队列来求这个东西····具体实现看代码吧,···大概有点类似于烽火传递····队列首与队列尾的距离不能大于环长度的一半(不然就不是最短距离),然后还要保持一个单调性···

    代码:

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cmath>
    #include<ctime>
    #include<cctype>
    #include<cstring>
    #include<string>
    #include<algorithm>
    using namespace std;
    const int N=5e4+5;
    int n,m,k,f[N],low[N],dfn[N],cnt,deep[N],father[N],tol;
    int first[N],go[N*10],next[N*10],tot,ans=0,queue[N*2],a[N*2];
    inline int R()
    {
      char c;int f=0;
      for(c=getchar();c<'0'||c>'9';c=getchar());
      for(;c<='9'&&c>='0';c=getchar())
        f=(f<<3)+(f<<1)+c-'0';
      return f;
    }
    void comb(int a,int b)
    {
      next[++tot]=first[a],first[a]=tot,go[tot]=b;
      next[++tot]=first[b],first[b]=tot,go[tot]=a;
    }
    void init()
    {
      n=R(),m=R();int a,b;
      while(m--)
      {
        k=R();a=R();
        for(int i=1;i<k;i++)  b=R(),comb(a,b),a=b;
      }
    }
    void dp(int root,int u)
    {
      int tail=1,head=1;tol=deep[u]-deep[root]+1;
      for(int i=u;i!=root;i=father[i])
        a[tol--]=f[i];
      a[tol]=f[root],tol=deep[u]-deep[root]+1;
      for(int i=tol+1;i<=tol*2;i++)
        a[i]=a[i-tol];
      queue[head]=1;
      for(int i=2;i<=tol+tol/2;i++)
      {
        while(head<=tail&&queue[head]<i-tol/2)  head++;
        ans=max(ans,a[queue[head]]+a[i]+i-queue[head]);
        while(head<=tail&&a[queue[tail]]-queue[tail]<=a[i]-i)  tail--;
        queue[++tail]=i;
      }
      for(int i=2;i<=tol;i++)
        f[root]=max(f[root],a[i]+min(i-1,tol-i+1));
    }
    void tarjan(int u)
    {
      low[u]=dfn[u]=++cnt;
      for(int e=first[u];e;e=next[e])
      {
        int v=go[e];
        if(father[u]==v)  continue;
        if(!dfn[v])  father[v]=u,deep[v]=deep[u]+1,tarjan(v);
        low[u]=min(low[v],low[u]);
        if(dfn[u]<low[v])  ans=max(ans,f[u]+f[v]+1),f[u]=max(f[u],f[v]+1);
      }
      for(int e=first[u];e;e=next[e])
      {
        int v=go[e];
        if(father[v]!=u&&dfn[u]<dfn[v])
          dp(u,v);
      }
    }
    int main()
    { 
      //freopen("a.in","r",stdin);
      init();
      tarjan(1);
      printf("%d
    ",ans); 
      return 0;
    }
  • 相关阅读:
    【题解】SCOI2007组队
    【题解】Atcoder AGC#16 E-Poor Turkeys
    整数拆分 [dp+多项式插值]
    王子 [费用流]
    [ARC083F] Collecting Balls [建二分图+环套树定向+建拓扑图+树的拓扑序计数]
    [BZOJ3196] 二逼平衡树 [权值线段树套位置平衡树]
    [TJOI2017][bzoj4889] 不勤劳的图书管理员 [线段树套线段树]
    [HNOI2015][bzoj4009] 接水果 [整体二分+扫描线]
    [bzoj3065] 带插入区间第k小值 [重量平衡树套线段树]
    [luogu3676] 小清新数据结构题 [树链剖分+线段树]
  • 原文地址:https://www.cnblogs.com/AseanA/p/7543518.html
Copyright © 2020-2023  润新知