0:写在前面
这里是一只纯小白(●ˇ∀ˇ●),最近闲来无事研究了一下背包问题。小白想写几篇博客来加深一下理解。文章可能写的很烂,欢迎大佬们指正。如果这些文章也能帮到你,那就太好了( ̄▽ ̄)"。
1:梦开始的地方——01背包
先上问题:
夜黑风高之时,你——立志成为天下第一盗的素人盗贼,悄无声息的潜入了一家珠宝店。店里有n件珠宝,其中第i件物品的重量为w[i],价值为v[i],而你的背包最大承载量为p,你会如何选择,使自己偷走的珠宝价值最大…(因为对于每件物品,只可选择拿或者不拿,所以这类问题被形象的称为01背包问题)
先给出一组数据:n=3,p=9,w,v如下:
i (编号) | 1 | 2 | 3 |
---|---|---|---|
w (每件的重量) | 6 | 5 | 4 |
v(每件的价值) | 9 | 7 | 8 |
你面对着这三件珠宝陷入了沉思…
咋搞?
首先,你想到了,偷,就要偷最贵的。100块和50块掉在地上,你捡哪张?只要不是思想出了问题,都会捡那张红色的吧(什么?全都要?贪心的人没有好果子吃哦)。于是,你直接抓起了第一件珠宝,看了一下,其他珠宝也装不进去了,那就跑路吧~
这时,你没注意到,角落里传来了一道诡异的目光…
一个小时后,你躺在床上,看着珠宝,心想”今天干了票大的,美滋滋,如果背包大一点,我将绝杀,可惜大不得“。突然,你意识到了一个问题,你没装进去的两件珠宝,加起来总重量是9,总价值15,远大于你现在手里的这件…(这就是贪心思想解决不了背包问题的原因———对于背包问题,贪心得到的局部最优解的组合不一定是整体最优解)
你高呼自己是个铁憨憨,如果让你重新来过,你一定会仔细思考。这时,随着一道闪光,一位身穿长袍,仙风道骨的精神小伙出现在你面前。小伙剑眉星目,眉宇间透露着睿智的光辉,你盯着他的眼睛,仿佛看到了宇宙的深邃,但是比起那深邃,果然,最令你震惊的,还是男子帅气的面容,为什么,世上会有这么帅的人…
:“你是谁?”
:“谁也不是,只是一个流浪者。”我说到。:“我说,你励志成为天下第一盗?可你这今晚干的什么事啊?就这?”
:“害在这阴阳怪气呢?给爷爬爬爬!”
:“喂,憨憨,给你第二次机会的话,你还会装第一件珠宝吗?”
:“想也知道不可能啊喂,我有这么憨憨吗?啊所以你到底是——”
:“第七式:两极反转——”
当你再次睁开眼睛时,你发现,自己竟然回到了一个小时之前。
:“我会助你成为天下第一大盗,所以,面对这三件珠宝,做出选择吧。”
“虽然还没搞清这是什么状况,但是不是代表我可以重偷一次了?”你想到
重新面对三件珠宝,你认真的想了下:
1:对于每件物品,你只有两种选择——拿它,或者是不拿。
2:拿这件物品,意味着我现在剩余空间可以装下,至于拿不拿,要看它值不值。
把整个大问题拆开,你提出了子问题,“拿第一件,前两件,前三件物品时,我得到的最大价值是多少呢?”
“机智!”我赞赏到。
“这个问题,该怎么接解决呢?”
为师帮你一手——
设一个二维数组f[i][j],其中i表示前i件物品(一定要注意是前i件),j表示当前剩余的容量,f[i][j]就表示在剩余容量为j时,选取前i件物品的最大价值,于是不难得出下面的公式:
(这个公式要记住,它是所有背包问题的根源!!!)
其中,第一行表示第i件物品装不下的情况,此时最大价值还是和前i-1件物品的最大价值相同,而背包剩余的容量也还是j。第二行表示第i件物品可以装下的情况,它又分成两种情况:①不拿这件物品,此时获得的价值和前i-1件的最大价值是一样的;②拿这件物品,此时获得价值即是前i-1件物品,剩余空间为j-w[i]时的最大价值加上现在的第i件物品的价值。所以第i件物品可以装下时,最大的价值就是上述两种状况中最大的那个。
(②具体来说就是:前i-1件物品,剩余容量是j时,把第i件物品放进去,空间变为j-w[i],而价值就是前i-1件物品,j-w[i]空间对应的价值再加上第i件物品的价值)
有了这个神奇的公式,你就可以列出下面的表格:(为了方便,设i=0或j=0时f=0)
(强烈建议自己动手写一下哦)
这个表格告诉了我们最大能获得的价值,即是f[3][9]=15,基于上面的公式,我们就可以写出解决01背包问题的代码:(只给出关键部分,其余的偷个懒,省了==)
for(int i=1;i<=n;i++)
{
for(int j=1;j<=p;j++)
{
if(j<w[i])
f[i][j]=f[i-1][j];
else
f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);
}
}
写出这个程序后,你就可以得到你可以获得的最大价值了。
不过,这个程序不会告诉你获得最大价值时是取了哪些物品。
你愤怒的大喊:“👴听你说了这么半天,结果你就告诉我最大值是多少,不告诉我怎么取?给👴爪巴!”
年轻人,别急,我们再拿出刚才那张表:
我们回溯一下,从后往前,看看每一个物品是否被选择。
通过刚才的公式,很容易看出,只要是f[i][j]=f[i-1][j],就代表没有选择第i个物品,那接下来就回到f[i-1][j]这项继续推;而f[i][j]!=f[i-1][j]时,意味着选择了第i个物品,此时应回到f[i-1][j-w[i]]继续推,直到推到i=0我们就可以得出选择了哪些物品。
举个栗子:
f[3][9]=15,f[2][9]=9, 所以选了第三件物品;下一步看f[2][5];
f[2][5]=7, f[1][5]=0, 所以选了第二件物品;下一步看f[1][0];
f[1][0]=0,f[0][0]=0,所以没选第一件物品;推到i=0,结束。
根据这个原理,我们可以写出一个函数,用来判断物品的选择,代码如下:
void Rx78(int i, int j) //传值的时候i和j对应n和p,至于这个函数名...不要在意
{
if (i > 0)
{
if(f[i][j]==f[i-1][j])
{
c[i]=0;//写函数之前要开一个数组c,这块有点桶排序的思想
Rx78(i-1,j);
}
else
{
c[i]=1;
Rx78(i-1,j-w[i]);
}
}
}
在程序的最后,调用这个函数,再加上一层循环,就可以输出选择谁辣!
呐,神奇吗,呐,呐~~
愉快的偷完东西,回到家,你美滋滋的看着手里的两件宝贝,心想*“有了这个程序,我就离天下第一盗不远辣!哇哈哈哈哈”*
:“年轻人啊,naive,你还是要学习一下”。我推了推自己的黑框眼镜,说到。
———————————————这里是萌萌哒的分割线o( ̄▽ ̄)o————————————————
2:能再给力点吗?——01背包的优化
上面所说的是01背包问题的基础,你在偷东西的时候,拿出你的bndroid手机,或者是pineapple手机,打开c语言编译器,随手一写就能看出要偷什么东西辣。
但万一…你要偷故宫(危险发言,诶,有人敲门,我看一下),(回来了,没事,送快递的)随便一间都有几百件宝贝,不像是这家店只有3件珠宝,那这个程序就很容易翻车。因为用二维数组空间复杂度有、大。你把数据都输进去,万一把手机搞爆了,你就GG了。
:“那能再给力点吗hxd?”
好,既然你诚心诚意的发问了,那我就大发慈悲的告诉你。
我们康康上面讲过的公式:
仔细看,你能发现这些公式有什么共同点?
:“emm好像,每个公式里都用到了i-1?”
:“nice,你发现了盲点。”
其实,每次求第i行的数据,都是根据i-1行数据得到的,所以我们可以只用一个一维数组,每次根据自身推出下一行的数据,就完成了空间上的优化。
听完这句话,机智的你很快写出了代码:
for(int i=1;i<=n;i++)
for(int j=w[i];j<=p;j++)//w[i]是为了防止越界
f[j]=max(f[j],f[j-w[i]]+v[i]);
用刚才的数据试下结果
输出结果:
最大价值:16
:“老板,怎么和说好的不一样,不应该是15咩?”
不要急,先想一下16是怎么出来的。
:“7+9?8+8?”
7+9是不可能的,因为5+6大于9,所以真相只有一个——8+8!
:“可是三号珠宝只有一件,这个程序当成2件算了吧。”
是这样的:for(int j=w[i];j<=p;j++),也就是在计算f[j]时,f[j-w[i]]已经计算过了,可是我们计算f[j]时用到的f[j-w[i]]应该是上一次计算、未被更新的f[j-w[i]] (也就是i-1时的f[j-w[i]]) (什么?为什么用的是i-1时的?都说了i行是i-1行推出来的) ,这么写我们计算的其实是max(f[i-1][j],f[i][j-w[i]]+v[i]),相当于拿走某件物品后又把它放了回去,所以会出现一件物品算了好几次的情况。
解决方法很简单:
for(int i=1;i<=n;i++)
for(int j=p;j>=w[i];j--)
f[j]=max(f[j],f[j-w[i]]+v[i]);
第二层循环改成逆序就好了。
应用上面的代码,就实现了空间上的优化。
:“学到了,这就去和故宫对线!”
:“别去,他们人多!”
学会的话,试试这个:洛谷P1048
———————————————这里是萌萌哒的分割线o( ̄▽ ̄)o————————————————
3:哇好神奇——完全背包
偷之力三段的你,在掌握低阶偷技后,决定去秀一波。于是,你又来到了之前的那家店。(店主:“喵喵喵?”)
嗯,一样的配方,一样的味道,一样的三件珠宝。
但这次,你有了巨大的发现:你发现了这家店的秘密仓库!
仓库里,虽然还是那三件珠宝,但每件都有无数件!(这个店主不一般啊)
那这次,怎么选择呢?
按照老规矩,写程序:
for(int i=1;i<=n;i++)
for(int j=p;j>=w[i];j--)
f[j]=max(f[j],f[j-w[i]]+v[i]);
这个程序应该不陌生了,可是它只适用于每件物品有一件的时候。
:“改改?”
突然间,你想到了师傅说过的话“…其实是max(f[i-1][j],f[i][j-w[i]]+v[i]),相当于拿走某件物品后又把它放了回去,所以会出现一件物品算了好几次的情况…”
你灵机一动,想到,上次写错的程序,会不会是这个问题的正解呢?
for(int i=1;i<=n;i++)
for(int j=w[i];j<=p;j++)/
f[j]=max(f[j],f[j-w[i]]+v[i]);
最大价值:16
于是你愉快的拿了两件三号珠宝,吹着口哨跑着跳,回到家,炒几个拿手小菜,倒一杯散装白酒,告诉自己的师傅,今天干了票大的。
:“嗯,干得漂亮,你写的代码,正是完全背包问题的通解(每件物品都有无数个,称为完全背包),对了,你的心情不错嘛。”
:“那是当然的。成功偷到了宝贝,我也要成为加把劲神偷,我们一直以来的努力并非全部木大。”
既然这样,来做道题吧 洛谷P1616
———————————————这里是萌萌哒的分割线o( ̄▽ ̄)o————————————————
以上是背包问题的基础,一切的背包问题都源于01背包,01背包的基础思想可以说是重中之重,一定要记牢==
这是小白的第一篇博客,写的不是很好,欢迎dalao们指正~~