动态规划——树形dp

  动态规划作为一种求解最优方案的思想,和递归、二分、贪心等基础的思想一样,其实都融入到了很多数论、图论、数据结构等具体的算法当中,那么这篇文章,我们就讨论将图论中的树结构和动态规划的结合——树形dp。

  其实如果看过《背包九讲》或者看过笔者的文章《动态规划——背包问题》的读者会对树形dp有一定的了解,下面引用笔者在《动态规划——背包问题》中一个一段。

  “ 依赖背包问题的模型很简单,就是说对于某个物体,将它装入背包必须以装入一个物体做前提。这其实十分类似我们上文提到的分组背包问题。我们将那些没有依赖性的物体成为“主件”,而那些有依赖性的物体成为“附件”,那么我们容易看到,如果将物体分组,我们可以将其分成n组,其中n是“主件”的数量,而在每一组中,我们以这个主件为根节点,那些依赖“主件”的物体,我们将其看做这个根节点下面的树。”

  通过这段对依赖背包模型的简介,我们可以看到,所谓树形dp,就是在决策过程中物体与物体之间存在依赖性,而图结构中的边刚好能够体现这种“联系”,那么我们就可以基于具体问题中的数据,来构建这样一棵树,而多棵树就形成了森林,我们只需要在构造一个根节点,连接各个树的根,这样森林便成为了一棵树,这样所有的数据都被存在了一个图当中,这样对于我们在找到状态转移方程然后求解最优方案是非常有利的(如果不形成森林,如何遍历所有的数据是一个非常棘手的问题)。

  我们结合一个具体的问题来继续深入的分析到底如何将动态规划和这种树结构结合起来。(Problem source : hdu 1561)

Problem Description
ACboy很喜欢玩一种战略游戏,在一个地图上,有N座城堡,每座城堡都有一定的宝物,在每次游戏中ACboy允许攻克M个城堡并获得里面的宝物。但由于地理位置原因,有些城堡不能直接攻克,要攻克这些城堡必须先攻克其他某一个特定的城堡。你能帮ACboy算出要获得尽量多的宝物应该攻克哪M个城堡吗?
 
Input
每个测试实例首先包括2个整数,N,M.(1 <= M <= N <= 200);在接下来的N行里,每行包括2个整数,a,b. 在第 i 行,a 代表要攻克第 i 个城堡必须先攻克第 a 个城堡,如果 a = 0 则代表可以直接攻克第 i 个城堡。b 代表第 i 个城堡的宝物数量, b >= 0。当N = 0, M = 0输入结束。
 
Output
对于每个测试实例,输出一个整数,代表ACboy攻克M个城堡所获得的最多宝物的数量。
 
 
  数理分析:容易看到,其实这样的问题就是典型的树形dp的模型——因为决策之间存在依赖性。通过上文对数据的“树形”化处理,接下来我们需要探讨的是寻求状态转移方程来获取最优方案。
  题设中给出的限制是城堡的个数,那么我们就可以设置二维数组f[i][j]来表示以i为根节点,选取了i的j个子树的最优方案。(在动态规划问题中设置记录方案的数组是一个关键所在,其往往能够体现整个状态转移的过程,而它的设置往往根据题目的变化而又这灵活的变化,比如在依赖背包的模型中,限制条件就变成了背包的容量,而算法的优化也伴随着记录方案数组的降维。)而树结构本质来将是一种图,我们要用到所有数据来得到所有的状态,势必要遍历该图的所有节点,不难想到,我们就需要利用遍历图的dfs算法了。
  我们来模拟状态转移的过程。假设我们当前遍历到了某个顶点vi,与vi直接相连的节点有vj、vk、vm、vn,其中vm、vn是树叶,即他们没有子树。那么我们现在单独来看一个全局下的子问题:对于以vi为根的树,选取m'个节点的最优方案什么呢?
  对于与vi直接相连的子节点有如下两种情况:
  1.倘若与vi直接相连的节点都是树叶,那么很好办,这本质上是很简单的01背包问题,甚至无需状态转移方程,只需将树叶从大到小排序,选出最大的m'个即可。
  2.而如果与vi直接相连的节点不全为树叶,如上文所描述的那样,那么对于另外两棵树的根vj、vk来说,如何取m'个点就不能简单得像情况1.那样了。
  此时我们需要递归的看问题了,通过深搜遍历树,我们首先处理的情况肯定是1.,这是基于深搜的特点,只要当前节点不是树叶,dfs就会继续深搜下去,那么深搜到了最底层的内点后vj,我们容易得到f[j][m'],那么我们再设置记录数组dp[j][m']来表示以j为根点,包含j且选择了m'的最优方案情况,则dp[j][m' + 1] = f[j][m'] + val[j]。那么基于这一条件,我们从树的最底层往根部递归,此时对于vi,我们想得到f[i][m'],则需要遍历访问vj、vk、vm、vn,并从vj开始记录f[i][m']的最优解,概括的来说,便是如下的状态转移方程:
  假设我们当前在遍历根节点vi下的vj,则f[i][m'] = max(f[i][m'] , f[i][m' - x] + dp[j][x]),其中x表示当前我们要选入以vj为根含x个点(包括vj)的树进入方案,按照此状态转移方程,遍历与vi直接相连的子节点,则可以完成对vi节点各种状态下的最优维护,然后按照深搜从数的底层回溯到根,按照我们的定义,则dp[0][m+1]则是整个森林选入m+1个点的最优解,出去我们人为构造的根节点,则dp[0][m+1]其实就是最终的答案。
  值得注意的是,上文给出的状态转移的方程m'是一个过程参量,这个求解过程类似深搜的过程,是通过f[i][j]、dp[i][j]数组j较小时候的值来递推得出j较大时候数组的值,而j较小时候的值,则是通过深搜访问树叶直接得到的。
  基于以上的算法分析,参考代码如下。
 
#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<string.h>
#define N 205
using namespace std;
int n , m , edgeNum = 0;
int ans[N],dp[N][N],f[N][N];
int visit[N] , head[N];
struct Line
{
     int v , next;
}edge[N];

void add(int u , int v)
{
      edge[edgeNum].v = v;
      edge[edgeNum].next = head[u];
      head[u] = edgeNum++;
}
void dfs(int root)
{
      visit[root] = 1;
      for(int i = head[root];i != -1;i = edge[i].next)
      {
            int u = edge[i].v;
            if(!visit[u])
            {
                  dfs(u);
                  for(int k = m;k >= 0;k--)
                      for(int j = 0;j <= k;j++)
                         f[root][k] = max(f[root][k],f[root][k-j] + dp[u][j]);
             }

      }
      for(int i =1 ;i <= m + 1;i++)
                  dp[root][i] = ans[root] + f[root][i-1];
}
int main()
{
          int a , b;
      while(scanf("%d%d",&n,&m) != EOF)
      {
    if(n == 0 && m == 0) break;
           edgeNum = ans[0] = 0;
           memset(f , 0 , sizeof(f));
           memset(dp , 0 , sizeof(dp));
           memset(head , -1 , sizeof(head));
           memset(visit , 0 , sizeof(visit));

        for(int i = 1;i <= n;i++)
        {
              scanf("%d%d",&a,&b);
              ans[i] = b;
              add(a , i);
        }
        dfs(0);
        printf("%d
",dp[0][m+1]);
      }

}
原文地址:https://www.cnblogs.com/rhythmic/p/5367510.html