zoukankan      html  css  js  c++  java
  • 多叉树的树形背包常见建模方法

    参考:

    多叉树的树形背包常见建模方法 - CSDN博客
    http://blog.csdn.net/no1_terminator/article/details/77824790

    一.多叉树变二叉树。


    这个技巧其实也有两种具体的方法:树的孩子兄弟表示法与dfs序法。

    1.树的孩子兄弟表示法。

       大家在学习树形结构时一定接触了一个多叉树变二叉树的方法,就是把每个点与它的第一个儿子连边,然后将它的儿子依次连接起来。可以结合下面的图片理解这句话。


    总结成口诀就是:第一个儿子是左子树,其他儿子是左子树的右子树(似乎并不押韵,手动滑稽)

    2.dfs序法

    dfs序就是对一个树进行一个dfs,然后对于每个点记录一个时间戳dfn数组,这个时间戳就是这个节点的dfs序,然后我们再记录一个size数组,表示以每个节点为根的子树中节点的数量。

    假设根节点是u,那么可以容易的推出

    第一个儿子的dfs序dfn[first_son]就是dfn[u]+1

    第二个儿子的dfs序dfn[second_son]就是dfn[u]+size[first_son]+1

    其余同理。

    那么u的下一个兄弟的dfs序dfn[next_brother]就是dfn[u]+size[u]+1


    这两个方法大多用于树形依赖形背包(即使用一个节点必须要使用它所有的祖先),

    主要解决点权问题。

    主要作用就是可以使决策转移的过程变成O(1)的了。


    最常见的模型就是:有n个物品,有一个m大小的包,每个物品有wi物重与vi物品价值,物品之间存在只有装了物品a,才能装物品b的n-1条关系(就是一个树)。问能取得的最大价值。


    简要分析:显然是一个多叉树,考虑转换。

    1.孩子兄弟表示法:对于一个节点i,设dp[i][j]表示在以i为根的子树中,用大小为j的包能取得的最大价值,那么dp[i][j]=max(dp[left[i]][j-w[i]]+v[i],dp[right[i]][j])

    注意,这里的left[i]是i在原树中的第一个儿子,right[i]是i在原树中的下一个兄弟。

    这个方程是非常好理解的。效率就是O(nm)的。

    2.dfs序法:对于一个dfs序为i的节点u,同样设dp[i][j]表示在以u为根的子树中,用大小为j的包能取得的最大价值,那么dp[i][j]+v[i]->dp[i+1][j-w[i]]

    dp[i][j]->dp[i+size[i]+1][j]

    注意,这里的转移并不是常见的dp[i][j]=max()....(用dp[i][j]的前驱状态去计算dp[i][j]),而是用dp[i][j]去更新它的后继状态。这种方法被称为刷表法。

    两种方法都是非常巧妙的。但作用也是有限的,只能解决依赖性背包中的点权问题。


    二.分组的树形背包。


    这类问题也是有一个常见模型的,具体可参考洛谷P1272重建道路

    下面针对这道题来分析,能够解决多叉树的,分组的树形背包。

    此时,我们的儿子与父亲之间并不存在依赖型关系,那么我们设dp[k][i][j]表示以i为根的子树,在前k个儿子中,分离出一个大小为j的子树(必须包含i),所需要最少的操作次数。

    那么我们每计算到第k+1个新的儿子v时(full_son[v]表示v的儿子个数),

    dp[k+1][i][j]=min(dp[k][i][j-t]+dp[full_son[v]][v][t]);

    由于一个树形关系,我们需要在一个dfs上进行dp,即先dfs(v),然后更新dp[k+1][i][j]。

    这个k的一维显然可以用滚动数组优化掉。

    那么就是

    j=m->1

    t=1->j

    dp[i][j]=min(dp[i][j-t]+dp[v][t]);

    同时,dp一律要注意初始化,即刚开始时所有的dp[i][1]=du[i](du[i]表示与i连边的节点数,又称i的入度(树是无向边哟!))

    给出参考代码,方便理解:

     1 #include<cstdio>
     2 #include<algorithm>
     3 #include<cstring>
     4 using namespace std;
     5 const int INF=0x3f3f3f3f;
     6 const int N=201;
     7 struct Edge{
     8     int to,next;
     9 }e[N*2];
    10 int du[N],a[N],dp[N][N];
    11 int n,k,res=INF,EdgeCnt=0;
    12 void addedge(int u,int v){
    13     int p=++EdgeCnt;
    14     e[p].to=v;e[p].next=a[u];
    15     a[u]=p;
    16 }
    17 void dfs(int u,int fa){
    18     dp[u][1]=du[u];
    19     for (int p=a[u];p;p=e[p].next){
    20         int v=e[p].to;
    21         if (v!=fa){
    22             dfs(v,u);
    23             for (int j=k;j>=1;j--)
    24                 for (int k=1;k<=j;k++)
    25                     dp[u][j]=min(dp[u][j],dp[u][j-k]+dp[v][k]-2);
    26         }
    27     }
    28     res=min(res,dp[u][k]);
    29 }
    30 int main(){
    31     scanf("%d%d",&n,&k);
    32     memset(dp,0x3f,sizeof(dp));
    33     for (int i=1;i<n;i++){
    34         int u,v;
    35         scanf("%d%d",&u,&v);
    36         addedge(u,v);
    37         addedge(v,u);
    38         du[u]++;du[v]++;
    39     }
    40     dfs(1,0);
    41     printf("%d",res);
    42     return 0;
    43 }

    同样,这个方法也是有缺陷的,就是无法解决点权问题。大多数运用于边权问题。点权当然也可以,但是效率较低。


    最后总结一句:树形背包都与dfs离不开关系,所以我们可以在dfs上dp可以写的更简单,也可以在dfs预处理后再总刷表法dp。


    这三种方法都各有长处,各有短处,实际运用时还是要注意题目本身的。

  • 相关阅读:
    一个简单的禁止鼠标滚轮事件处理
    模仿抽奖转盘,并且用cookie记录历史次数
    学习jquery
    使用var提升变量声明
    Django 自带密码加密,自定密码加密方式 及自定义验证方式
    kindEditor使用并防止xss攻击(day88)
    python二维码生成库(qrcode)简介和实例
    js原型的区别
    js中 this与that
    python单元测试之unittest框架使用总结
  • 原文地址:https://www.cnblogs.com/Renyi-Fan/p/7482744.html
Copyright © 2011-2022 走看看