微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

HDU 1561 The more, The Better 超详细题解树形DP + 依赖背包


The more,The Better

Time Limit: 6000/2000 MS (Java/Others)Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 7970Accepted Submission(s): 4673


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个城堡所获得的最多宝物的数量

Sample Input
  
  
3 2 0 1 0 2 0 3 7 4 2 2 0 1 0 4 2 1 7 1 7 6 2 2 0 0

Sample Output
  
  
5 13

先贴网上看到的两个很好的题解:

题解一:

这题目可以看成是一个森林,对于需要攻占的城堡作为根节点,攻占了这个根节点以后可以立即攻占的城堡作为它的孩子,那么这就变成了树形DP了,因为这有可能是一个森林,所以我们将一开始就可以攻占的城堡作为0号节点的孩子,这样就可以将一个森林转化成一棵树了,然后对于每一个节点,因为它的孩子的组合有多种,同时孩子的孩子也会影响它们,所以这个问题可以转化为有依附的背包问题,每一个节点作为原件,它的孩子们作为附件,然后对孩子们跑一遍01背包,这样就可以用孩子的属性求出整棵子树的属性,然后一层一层地想向上推,最终就可以得到结果了。
  状态转移方程:dp[r][j]=max{dp[r][j],dp[r][j-k]+dp[ver[r][i]][k]} 其中r代表以r为根节点的子树;j为容量为j的背包,这里的意思是以r为根节点的子树一共取j个节点;k的意思是从r的第i个孩子的子树那里挑k个节点,ver[r][i]代表r的第i个孩子的标号;dp[r][j]的意思是从以r为根的子树取j个节点的最大价值是多少。
  代码那里有一个地方需要注意的,就是k不可以等于j,因为j是从一棵子树取的节点数,只要j>0,那么j里面一定要包含根节点,k==j的意思就是在不取根节点的情况下在子树上取j个节点,这不合题意。

题解二:

这题其实就是经典的《选课》问题
状态 dp[i][j] 为以 i 为根节点,选出 j 个节点的最大价值(包括 i 这个节点)
转移方程:dp[i][j]=max(dp[i1][j1]+dp[i2][j2]+....+dp[ik][jk])+a[i] j1+j2+...+jk=j-1
由于这个题目上的树并非一棵树而是一个森林,因此我们通过把根设为0使其变成真正意义上的树
那么答案:dp[0][m+1]
看到这个转移方程可能一时难以下手,因为你当前的状态转移是要搞定子节点的各个分支上的情况的
即子节点上各个分支分别取了多少个物品,才能使得决策最优
而对于这个问题,我们可以发现与经典的背包问题存在着相似性,这里的 j 其实就是背包的容量
因此对于这个子问题,我们可以用背包来取最优
记 dp0[i][j] 为以 i 为根节点,它的分支中取出 j 个节点的最大价值
那么转移方程就是经典的背包
dp0[i][j+k]=max{dp0[i][j+k],dp0[i][j]+dp[son[i]][k]}
即我们在一个分支中最优的取出 k 个物品后,从 j 转移到了 j+k

从这里我们也可以看出
本题的DP状态和子问题背包的DP状态其实两个是紧密相连的
两者的区别也仅仅在于本题的DP状态对于以 i 为根节点的子节点情况,它是包括 i 这个根节点本身的
而背包的状态则是不包括在内的,而就是这样的区别导致它们在具体转移的时候方程是很不一样的
实现的一些细节:
1:图用邻接表来存储,偷懒的方式是用一个 二维 vector
2:树形DP中表示某个点没有访问应该把它设置成 -1
3:用记忆化搜索实现比较容易而且高效,因为记忆化搜索只是计算了那些我们需要的状态,而递推来搞的话,往往把所有的状态都给算出来了

做题感悟:

第一次做树上背包。。想要很清楚的学会这个题,首先要了解什么是依赖背包,树上背包是一个很经典的依赖背包,依赖背包指的是在取某个点的之前,必须先取某个点,这与树不谋而合,你要到子节点必须要先经过父节点。树上背包一般是有个“代价”限制,每个节点都有一些“代价”跟“价值”,问你怎样才能找到在限制内获得最大价值。。对于每个节点来说,他是从所有儿子节点的不同组合里找到一个最优解,这些儿子节点的“代价和”sum,是总的代价和-父节点的代价,这个题每个节点的代价是1,所以很好做。。。但是如果代价不是1而且都不相同,我想不出来怎么做。。等着在学一下吧。。 这个题就是对节点的儿子进行一下背包,从所有儿子里挑出最优方案,这里的dp[][]数组是二维的,其中第一维是代表节点,第二维才是背包(相当于背包变成一维模式),从每个节点的儿子依次取出0-v-1(根节点必须选)个点,利用背包更新根节点的数组,这里就相当于分组背包了,引自背包九讲的一段话“

这个问题变成了每组物品有若干种策略:是选择本组的某一件,还是一件都不选。也就是说设f[k][v]表示前k组物品花费费用v能取得的最大权值,则有:

f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于第k}

使用一维数组的伪代码如下:

for所有的组k

for v=V..0

for所有的i属于组k

f[v]=max{f[v],f[v-c[i]]+w[i]}

注意这里的三层循环的顺序,甚至在本文的beta版中我自己都写错了。for v=V..0”这一层循环必须在“for所有的i属于组k”之外。这样才能保证每一组内的物品最多只有一个会被添加到背包中。

一个for是枚举当前节点的几个儿子,第二个for就是在这枚举背包大小了,第三个for枚举这个儿子里的情况(每种情况都已经知道了,即主件i附件集合先进行一次01背包,得到费用依次为0..V-c[i]所有这些值时相应的最大价值f'[0..V-c[i]]。那么这个主件及它的附件集合相当于V-c[i]+1个物品的物品组,其中费用为c[i]+k的物品的价值为f'[k]+w[i])典型的依赖背包

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int maxn = 205;
vector<int> p[maxn];
int dp[maxn][maxn],val[maxn],n,m;
void dfs(int x,int w)
{
    dp[x][1] = val[x]; //选一个肯定选自己,注意区别一般的依赖背包,这里的dp[x][]还没有回溯到,正在根据子节点提供的信息算dp[x][],所以不能直接一个for,dp[x][] = dp[i-1][j-c[i]]
    for(int i = 0; i < p[x].size(); i++) //这一块就是分组背包了,这里是一共i组
    {
        int to = p[x][i];
        dfs(to,w-1);  //每用掉一个节点,后面“代价总和”就要少一个节点,否则后面可能选出来x个点,但是它最多就能w歌点,这样就不对了。。要不断更新最大值,因为有依赖关系
        for(int v = w; v > 1; v--)  //这里是>1,因为要扣掉父亲节点。 这里是枚举v
        {
            for(int k = 0; k < v; k++) //每个儿子的“代价”是1  这里是美剧每一组的所有k,这种顺序是因为每组只能选一种情况,这样保证了这个条件,每个k对应的价值都已经回溯出来了
            {
                dp[x][v] = max(dp[x][v],dp[x][v-k]+dp[to][k]);
            }
        }
    }
}
int main()
{
    while(~scanf("%d%d",&n,&m),m+n)
    {
        int a,b;
        memset(dp,sizeof(dp));
        memset(val,sizeof(val));
        for(int i = 0; i <= maxn; i++) p[i].clear();
        for(int i = 1;i <= n; i++)
        {
            scanf("%d%d",&a,&b);
            p[a].push_back(i);
            val[i] = b;
        }
        m++;
        dfs(0,m);
        printf("%d\n",dp[0][m]);
    }
    return 0;
}

这题经常跟一般的依赖背包比较下:http://www.jb51.cc/article/p-dekygbko-bqn.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


迭代器模式(Iterator)迭代器模式(Iterator)[Cursor]意图:提供一种方法顺序访问一个聚合对象中的每个元素,而又不想暴露该对象的内部表示。应用:STL标准库迭代器实现、Java集合类型迭代器等模式结构:心得:迭代器模式的目的是在不获知集合对象内部细节的同时能对集合元素进行遍历操作
高性能IO模型浅析服务器端编程经常需要构造高性能的IO模型,常见的IO模型有四种:(1)同步阻塞IO(BlockingIO):即传统的IO模型。(2)同步非阻塞IO(Non-blockingIO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。注意这里所说的N
策略模式(Strategy)策略模式(Strategy)[Policy]意图:定义一系列算法,把他们封装起来,并且使他们可以相互替换,使算法可以独立于使用它的客户而变化。应用:排序的比较方法、封装针对类的不同的算法、消除条件判断、寄存器分配算法等。模式结构:心得:对对象(Context)的处理操作可
访问者模式(Visitor)访问者模式(Visitor)意图:表示一个作用于某对象结构中的各元素的操作,它使你在不改变各元素的类的前提下定义作用于这些元素的新操作。应用:作用于编译器语法树的语义分析算法。模式结构:心得:访问者模式是要解决对对象添加新的操作和功能时候,如何尽可能不修改对象的类的一种方
命令模式(Command)命令模式(Command)[Action/Transaction]意图:将一个请求封装为一个对象,从而可用不同的请求对客户参数化。对请求排队或记录请求日志,以及支持可撤消的操作。应用:用户操作日志、撤销恢复操作。模式结构:心得:命令对象的抽象接口(Command)提供的两个
生成器模式(Builder)生成器模式(Builder)意图:将一个对象的构建和它的表示分离,使得同样的构建过程可以创建不同的表示。 应用:编译器词法分析器指导生成抽象语法树、构造迷宫等。模式结构:心得:和工厂模式不同的是,Builder模式需要详细的指导产品的生产。指导者(Director)使用C
设计模式学习心得《设计模式:可复用面向对象软件的基础》一书以更贴近读者思维的角度描述了GOF的23个设计模式。按照书中介绍的每个设计模式的内容,结合网上搜集的资料,我将对设计模式的学习心得总结出来。网络上关于设计模式的资料和文章汗牛充栋,有些文章对设计模式介绍生动形象。但是我相信“一千个读者,一千个
工厂方法模式(Factory Method)工厂方法模式(Factory Method)[Virtual Constructor]意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类,使一个类的实力化延迟到子类。应用:多文档应用管理不同类型的文档。模式结构:心得:面对同一继承体系(Produc
单例模式(Singleton)单例模式(Singleton)意图:保证一个类只有一个实例,并提供一个访问它的全局访问点。应用:Session或者控件的唯一示例等。模式结构:心得:单例模式应该是设计模式中最简单的结构了,它的目的很简单,就是保证自身的实例只有一份。实现这种目的的方式有很多,在Java中
装饰者模式(Decorator)装饰者模式(Decorator)[Wrapper]意图:动态的给一个对象添加一些额外的职责,就增加功能来说,比生成子类更为灵活。应用:给GUI组件添加功能等。模式结构:心得:装饰器(Decorator)和被装饰的对象(ConcreteComponent)拥有统一的接口