0-1背包,即便是老生常谈也要讲讲故事
最近啊新开了博客嫌文章太少,先随便写点东西吧(太随便了吧!)
我们有 n 种物品,其中物品 j 的重量为 wj ,价值为 vj 。
我们假定所有物品的重量和价格都是非负的。背包所能承受的最大重量为 W 。
求一种取物策略 Strategy(i) ,意为取 Strategy(i) 个物品 i
要求在约束 ∑ni=1Strategy(i)∗wi≤W 下,使得 ∑ni=1Strategy(i)∗vi 最大。
如果限定每种物品只能选择 0 个或 1 个(即 Strategy(i)∈{0,1} ),则问题称为0-1背包问题。
我们先来一个最朴素的解法。
// Language : C
// Mode : C99 and above
// solve : W(最大容量), n(物品数量), w(物品重量列表), v(物品价值列表)
// return : maxValueSum(最大价值和)
int solve(int W, int n, int* w, int* v){
// allocate the memory of the solution table and initialize
int** solutionTable = (int**) malloc(sizeof(int*) * n);
for(int i = 0; i < n; i++)
solutionTable[i] = (int*) malloc(sizeof(int) * (W + 1));
for(int i = 0; i < n; i++)
for(int j = 0; j <= W; j++)
solutionTable[i][j] = 0;
for(int i = 0; i < n; i++){ // for each item: i
for(int j = 0; j <= W; j++){ // for remaining weight: j
if(j > w[i]){ // there is still spare to get the item
if(i == 0)
solutionTable[i][j] = v[i];
else {
int stagegy1 = solutionTable[i - 1][j - w[i]] + v[i];
int stagegy2 = solutionTable[i - 1][j];
if(stagegy1 > stagegy2)
solutionTable[i][j] = stagegy1;
else
solutionTable[i][j] = stagegy2;
}
}
}
}
return solutionTable[n -1][W];
}
我可以负责任地告诉你们上面那段程序我不负责任地没有编译测试过。
但我想应该是对的。
嗯,看得懂看不懂都不要紧,让我们开始接下来的话题。
在这样一个决策问题里,一共有N个选择点,每个选择点可以选择2个项。整个决策空间的大小将是 2N ;而对于每个决策,可以用 O(N) 的时间去求它的值;因此“深谋远虑”的代价是 O(2NN) ,简直爆炸,简直无解!
要是我以后写Galgame,我一定要出这样一个这样原理的,20个选择点不过分吧?只有求得最优解才能获得完美结局(丧心病狂)。
没关系我手里还有外挂:平行世界监视器与世界线切换器。
本挂逼表示要开挂了:
首先,我先检查一下自己身上的余钱
剩余 12 文钱
于是我决定同时开W个新的平行世界,分别叫做 α,β,...
哎算了太麻烦了,叫world 0,1,2,3,...,11 吧。
那么在world i 里面的那个“我”呢,身上有 i 文钱
笑话!我怎么可能让他们比我有钱呢?不然我不是很没面子?
(画外音:你确定你自己不是被更有钱的你自己创造出来的吗?)
(懵逼+陷入沉思……)
于是我们开始走进步行街开始买买买……
当我们看到第一件……两碗酒与一碟茴香豆套餐时,卧槽那个图片诱惑得不得了,我们看得眼都直了,一看标价:9文。
于是我与world 9, 10, 11 的 兄弟 便排出九文大钱,享用了一顿套餐,最后我们一致认为店家这个东西其实很扯,最多只能打60分。
可怜的world 0 ~ 8 的兄弟 表示 “我就这么默默看着你们这些高富帅装逼”。
world | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12(我) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
gain | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 60 | 60 | 60 | 60 |
我想,虽然觉得有点亏,但如果我们的人生就此结束了,我觉得我还是比 0~8 的兄弟们 多吃了 60 分的 东西,赚了。
但是,我们逛着逛着又看上了另外一件宝贝……滑稽面具:5文钱,我们兄弟13人围着面具评价了一番
卧槽这真像百度贴吧的滑稽表情,这逼我给98分,留两分怕它骄傲!
大伙表示我的评价很中肯。
然后……
我发现自己没钱买了,要是我一开始没吃坑爹的茴香豆套餐该多好啊!!
于是我掏出了外挂,回到过去……
具体的逻辑是:我本来有12文钱,如果这次买了我还有7文钱……那么
我相信world 7 的兄弟也是最聪明的(他也是挂逼),他在之前一定能把7文钱用到极致。
那么问题就很简单了:我看看用到极致的7文钱所获得的收益加上这98分滑稽面具能否超过我之前认为的极致策略。
综合两件物品的最大收益是: gain[12−5]+98=98 或者 gain[12]=60 中的最大值 98。
于是我们不要脸地回到过去退了茴香豆套餐,然后买了
world | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12(我) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
gain_old | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 60 | 60 | 60 | 60 |
gain | 0 | 0 | 0 | 0 | 0 | 98 | 98 | 98 | 98 | 98 | 98 | 98 | 98 |
机智的我们马上发现了如下一个事实:
gain(i)=max(gain(i−5)+98,gain(i)),i>5
这大概还只是一个猜想,让我们继续。
现在发现了 2文钱 价值 2 分的辣鸡。
同样的,从 world 2 的兄弟开始买得起了,然后world 3的兄弟会参考world 1 的兄弟的用钱方式来得出需不需要开挂(回到过去)……world 4 参考 world 2 ……
worldi 参考 worldi−2 ……( i>2 )
world | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12(我) |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
gain_old_old | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 60 | 60 | 60 | 60 |
gain_old | 0 | 0 | 0 | 0 | 0 | 98 | 98 | 98 | 98 | 98 | 98 | 98 | 98 |
gain | 0 | 0 | 2 | 2 | 2 | 98 | 98 | 100 | 100 | 100 | 100 | 100 | 100 |
world 5: 我是不要这个2文的辣鸡的,因为我5文买98分的才能把5文钱用到极致。
world 7: 然而我恰好有闲钱买,哈哈哈,实力嘲讽5号位的兄弟。
现在我们就可以大致得出一个逻辑
每当我们碰到一个新的物品,先考虑其价格 cost , 然后考虑其价值 value 。
为啥先考虑价格?让我们来采访一下 Mr. Zero:
world 0:卧槽黑心仙商不要脸!叫我出来还不给我钱…
首先,你得买得起。
如果你买得起,你可以参考一下 worldi−cost 的兄弟,你除了比它多 cost 文钱以外,你可以问问他那些钱是怎么花的,以此来考虑配置你的花钱方式。
如果你买了这个玩意,那么你能获得的最大价值是 gain[i−cost]+v[i] 。
如果你没买这个玩意,那么你能获得的最大价值是 gain[i]
孰轻孰重自己考虑……显然地,每次都有这样的决策:
gain[i]←max(gainold[i],gainold[i−cost]+v[i])
注意这里有一个前提:你所依赖的兄弟是聪明的(即在上一次物品到达的时候考虑过调整的)
比如你不能简单地这样写
int solve(int W, int n, int* w, int* v){
if(n <= 0) return 0; // 没东西拿个卵
if(n == 1){
if(w[0] < W) return v[0]; // 好的拿得起
else return 0; // 哎哟拿不动
}
return max(solve(n - 1, W, w, v), solve(n - 1, W - w[n - 1]) + v[n - 1]); // wrong!!
}
而应该按照一开始的例子进行计算,一层层地更新。
当然,聪明或有经验的读者会发现可以在每层逆序地计算,这使得我们无须保存二维的solutionTable
// Language : C
// Mode : C99 and above
// solve : W(最大容量), n(物品数量), w(物品重量列表), v(物品价值列表)
// return : maxValueSum(最大价值和)
int solve(int W, int n, int* w, int* v){
// allocate the memory of the solution table and initialize
int* solutionTable = (int*) malloc(sizeof(int) * (W + 1));
for(int i = 0; i <= W; i++)
solutionTable[i] = 0;
for(int i = 0; i < n; i++){ // for each item: i
for(int j = W; j >= w[i]; j--){ // for remaining weight: j
if(i == 0)
solutionTable[j] = v[i];
else {
int stagegy1 = solutionTable[j - w[i]] + v[i];
int stagegy2 = solutionTable[j];
if(stagegy1 > stagegy2)
solutionTable[j] = stagegy1;
else
solutionTable[j] = stagegy2;
}
}
}
return solutionTable[W];
}
卧槽写了这么多辣鸡,浪费了好多时间啊……