0-1背包之小明和丁丁的奇遇

来源:互联网 发布:压缩包加密软件 编辑:程序博客网 时间:2024/04/29 06:15

又是一个阳光明媚的周末,是一个出去游玩的好日子。
小明背上书包,和他的好朋友丁丁一起去郊外游玩。
郊外芳草青青,流水潺潺。真是一个游玩的好地方啊!
正当小明和丁丁玩的正High的时候,突然间冒出来一个披头散发的老乞丐,把小明和丁丁吓个半死。
老乞丐突然从身后拿出一本书,上面赫然写着4个大字——“如来神掌”!


老乞丐对小明说:“小朋友啊,我看你相貌不凡,骨骼惊奇,是一个练武奇才。我这里有一本《如来神掌》,五块钱卖给你修炼如何?”
小明摆出一股不屑的表情,用拇指查了一下鼻子,说:“我不要!”
老乞丐看小明的表情如此坚决,无奈之下掏出了自己的所有家底:《如来神掌》、《降龙十八掌》、《六脉神剑》、《葵花宝典》、《凌波微步》、《蛤蟆功》、《黯然销魂掌》各一本。
老乞丐得意地看着这两个小朋友,说:“怕了你了,既然你不想学《如来神掌》,我这里还有别的武功秘籍,都是绝版的哦,江湖上就都只剩一本了。机不可失,失不再来,赶快下定离手!”
小明还是一股不屑的眼神,准备离开。
老乞丐无奈了,他深知这个小学生骨骼惊奇,如果学习了武功奖励一定能成为一代大侠,于是他做了一个他此生最重要的决定。“你要的话,全拿走吧,免费的。”
小明这才转过身来,准备全部拿走。
但是丁丁提醒小明:“小明,我们的书包不够大,最多只能装100页的书。”
小明这才想起来自己的书包不够装那么多数的事实。“啊,多么痛的领悟!”小明看到老乞丐手中的那些书,真是太多了,多到自己的书包都装不下了。
这个时候丁丁提醒小明:“我们可以尽量多装点书呀”
但是怎么装划算呢,小明不知道哪些书好那些书不好,这可愁死了他。
这个时候老乞丐发话了:“小朋友,这些书每一本都对应了一个武力值,当你学完了你本书的话你就能够增加相应的武力值。而这些武力值和书的价格是成正比的。比如这本《如来神掌》的价格是5元,那么当你学习了《如来神掌》之后,你的武力值就会增长5点。而这本《降龙十八掌》的价格是10元,也就是说……”。
“当我练完《降龙十八掌》之后,我的武力值将会增加10点!”小明说到。
老乞丐听到如此精确的回答,不禁肃然起敬,心想眼前的这位小学生不仅骨骼惊奇,连智力都高人一等。他赶忙回应道:“是的,不错。”
接下来老乞丐给两位小朋友列举了他手上所有的书的页数和价格。

书名页数价格《如来神掌》205《降龙十八掌》3010《六脉神剑》106《葵花宝典》5010《凌波微步》308《蛤蟆功》305《黯然销魂掌》2020

小明和丁丁计算了一下,这些书地总页数加起来是20 + 30 + 10 + 50 + 30 + 30 + 20 = 190(页),是不可能把这些书全部都拿走的。
小明是一个爱思考的好孩子,于是他将买书的这件事情归纳为了如下的一个问题:
    取几本书使得这几本书的页数之和不超过100页,并且它们的价格之和尽可能地大。
那么怎么来解决这个问题呢,这可让小明和丁丁烦懵了。
这个时候你出现了,告诉小明和丁丁,这其实是一个“0-1背包问题”。
0-1背包问题的大致描述是这样的:
    有N件物品和一个容量为V的背包。第i件物品的体积是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
这个问题为什么要在“背包”之前加一个“0-1”是因为对于每一个物品,它都只有两种状态:放进背包里面去了(对应状态1),或者没有放进背包里面去(对应状态0),所以称为“0-1背包问题”。
如果我们将故事中的每一本书对应成一个物品,将书的页数对应成物品的体积,将书的价格对应成物品的价值,那么连个问题就是一样的。
接下来我们来学习如何求解0-1背包问题。
0-1背包问题最基础的背包问题,它的特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值,可以得到它的状态转移方程:
    f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的背包中”,价值为f[i-1][v];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f[i-1][v-c[i]]再加上通过放入第i件物品获得的价值w[i]。

设物品的数量为N,背包的容量(如前所述)为V。以上方法的时间和空间复杂度为O(N*V),其中时间复杂度基本已经不能再优化了,但空间复杂度却可以优化到O(V)。
先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1..N,每次算出来二维数组f[i][0..V]的所有值。那么,如果只用一个数组f[0..V],能不能保证第i次循环结束后f[v]中表示的就是我们定义的状态f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]两个子问题递推而来,能否保证在推f[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]和f[i-1][v-c[i]]的值呢?事实上,这要求在每次主循环中我们以v=V..0的顺序推f[v],这样才能保证推f[v]时f[v-c[i]]保存的是状态f[i-1][v-c[i]]的值。伪代码如下:

for i=1..N    for v=V..c[i]        f[v]=max{f[v],f[v-c[i]]+w[i]};

其中的f[v]=max{f[v],f[v-c[i]]}一句恰就相当于我们的转移方程f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]},因为现在的f[v-c[i]]就相当于原来的f[i-1][v-c[i]]。如果将v的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v]由f[i][v-c[i]]推知,与本题意不符,但它却是另一个重要的背包问题(完全背包问题)最简捷的解决方案,故学习只用一维数组解0-1背包问题是十分必要的。

根据对0-1背包问题的分析,再代入小明和丁丁的问题。可以得到:
    容器数组(对应页数) c 为 [20, 30, 10, 50, 30, 30, 20] 
    价值数组(对应价格) w 为 [ 5, 10, 6, 10, 8, 5, 20]
最终获得的价值为44,可以通过如下的C++代码实现:

#include <cstdio>#include <iostream>using namespace std;int c[7] = {20, 30, 10, 50, 30, 30, 20};int w[7] = { 5, 10,  6, 10,  8,  5, 20};int N = 7, V = 100, f[101];int main(){    for (int i = 0; i < N; i ++)        for (int j = V; j >= c[i]; j --)            f[j] = max(f[j], f[j - c[i]]+w[i]);    int res = 0;    for (int i = 0; i <= V; i ++)        if (f[i] > res)            res = f[i];    printf("%d\n", res);    return 0;}

小明顺利地拿走了基本武林秘籍。令人庆幸地是,他没有拿走那本《葵花宝典》。
后来又有人遇到了那个老乞丐,拿走了葵花宝典,从而在武林中掀起了一番惊风骇浪,欲知后事如何,请看电视剧《笑傲江湖》。
小明后来送给了丁丁那本《如来神掌》,丁丁历尽艰辛,最终成为了一位武功大师,与之详情,请看电影《功夫》。

原创粉丝点击