zoukankan      html  css  js  c++  java
  • 算法复习——树形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;
    }
  • 相关阅读:
    新一代MQ apache pulsar的架构与核心概念
    Flutter使用fluwx实现微信分享
    BZOJ3622 已经没有什么好害怕的了 动态规划 容斥原理 组合数学
    NOIP2016提高组Day1T2 天天爱跑步 树链剖分 LCA 倍增 差分
    Codeforces 555C Case of Chocolate 其他
    NOIP2017提高组Day2T3 列队 洛谷P3960 线段树
    NOIP2017提高组Day2T2 宝藏 洛谷P3959 状压dp
    NOIP2017提高组Day1T3 逛公园 洛谷P3953 Tarjan 强连通缩点 SPFA 动态规划 最短路 拓扑序
    Codeforces 873F Forbidden Indices 字符串 SAM/(SA+单调栈)
    Codeforces 873E Awards For Contestants ST表
  • 原文地址:https://www.cnblogs.com/AseanA/p/7543518.html
Copyright © 2011-2022 走看看