小白的学习笔记——背包问题(1)(01背包,01背包优化,完全背包)

0:写在前面

  这里是一只纯小白(●ˇ∀ˇ●),最近闲来无事研究了一下背包问题。小白想写几篇博客来加深一下理解。文章可能写的很烂,欢迎大佬们指正。如果这些文章也能帮到你,那就太好了( ̄▽ ̄)"。

1:梦开始的地方——01背包

先上问题:
  夜黑风高之时,你——立志成为天下第一盗的素人盗贼,悄无声息的潜入了一家珠宝店。店里有n件珠宝,其中第i件物品的重量为w[i],价值为v[i],而你的背包最大承载量为p,你会如何选择,使自己偷走的珠宝价值最大…(因为对于每件物品,只可选择拿或者不拿,所以这类问题被形象的称为01背包问题)

先给出一组数据:n=3,p=9,w,v如下:

i (编号)123
w (每件的重量)654
v(每件的价值)978

  你面对着这三件珠宝陷入了沉思…
  咋搞?
  首先,你想到了,偷,就要偷最贵的。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们指正~~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值