一、前言
在前面一期博客中,我们解析了贪心算法实例中的最优装载问题,本期博客我们紧接着来解析贪心算法中另一个经典的背包问题。
二、背包问题
有一天,阿里巴巴赶着一头毛驴上山砍柴。砍好柴准备下山时,远处突然出现一股烟尘,弥漫着向上空飞扬,朝他这儿卷过来,而且越来越近。阿里巴巴心里害怕,担心碰到的是一伙儿强盗,他赶紧把毛驴赶到丛林的小道里,自己爬到一棵大树上躲了起来,这棵大树生长在一个大石头旁边。靠近以后,他才看清原来是一支马队,他们共有四十人,一个个年轻力壮、行动敏捷。一个首领模样的人背负沉重的鞍袋,他从丛林中来到那个大石头跟前,喃喃地说道:“芝麻,开门吧! "随着那个头目的喊声,大石头前突然出现一道宽阔的门路,于是强盗们鱼贯而入。阿里巴巴躲在树上观察他们,直到他们走得无影无踪之后,才从树上下来。他大声喊道:“芝麻,开门吧! ”他的喊声刚落,洞门立刻打开了。他小心翼翼地走了进去,一下子惊呆了,洞中堆满了财物,还有多得无法计数的金银珠宝,有的散堆在地上,有的盛在皮袋中。突然看见这么多的金银财宝,阿里巴巴深信这肯定是强盗们数代经营、掠夺所积累起来的宝窟。为了让乡亲们开开眼界,见识一下这些宝物,他想一种宝物只拿一个,如果太重就用锤子凿开,但毛驴的运载能力是有限的,怎么才能用毛驴运走价值最大的财宝分给乡亲们呢?
1、问题分析
这是书中的原作者给我们的设置的具体场景的背包问题,我们简化一下问题就是有n种物品,每种物品只有一个,第种物品的重量为wi,价值为vi,背包的容量为W,物品可以分割。如何放置物品,使装入背包的物品价值之和最大?
我们假定所有物品的重量和价格都是非负的。背包所能承受的最大重量为W。如果物品可分割,则问题称为背包问题,如果限定每种物品只能选择0个或1个,则问题称为0-1背包问题。
我们可以用公式表示为:
如果限定物品j最多只能选择bj个,则问题称为有界背包问题。
我们可以用公式表示为:
如果不限定每种物品的数量,则问题称为无界背包问题。
各类复杂的背包问题总可以变换为简单的0-1背包问题进行求解。
我们回到上面的题目,如果我们要使装入背包的物品价值之和最大,按照贪心策略的话我们应该每次选择单位重量价值最大的物品装入背包。
2、算法设计
- 确定合适的数据结构并初始化,我们首先将物品的重量、价值和单位重量价值定义为一种结构体类型,然后对物品按单位重量价值从大到小进行排序。
- 然后根据贪心策略,我们按照单位重量价值从大到小选取物品,直到达到背包的最大容量,如果在装入第i个物品时超出背包容量,则取该物品的一部分装入背包。
3、算法解析
我们在这里假设背包容量W=30,然后列出物品的价值和重量表:
物品i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
重量w[i] | 4 | 2 | 9 | 5 | 5 | 8 | 5 | 4 | 5 | 5 |
价值v[i] | 3 | 8 | 18 | 6 | 8 | 20 | 5 | 6 | 7 | 15 |
我们现在按照贪心策略每次选单位重量价值(价值/重量)大的物品,因此我们可以按单位重量价值对物品进行降序排序,排序后结果如下:
物品i | 2 | 10 | 6 | 3 | 5 | 8 | 9 | 4 | 7 | 1 |
重量w[i] | 2 | 5 | 8 | 9 | 5 | 4 | 5 | 5 | 5 | 4 |
价值v[i] | 8 | 15 | 20 | 18 | 8 | 6 | 7 | 6 | 5 | 3 |
单位重量价值p[i] | 4 | 3 | 2.5 | 2 | 1.6 | 1.5 | 1.4 | 1.2 | 1 | 0.75 |
然后我们按照安心策略每次选择单位重量价值最大的物品装入背包。
物品ID | 背包剩余容量 | 当前已装入物品的最大价值 |
2 | 30-2=28 | 8 |
10 | 28-5=23 | 8+15=23 |
6 | 23-8=15 | 23+20=43 |
3 | 15-9=6 | 43+18=61 |
5 | 6-5=1 | 61+8=69 |
8 | 剩余容量为1,8号物品重量为4,无法全部装入 | 69+1×1.5=70.5 |
我们按照上面表格依次装入物品,到第八装入的时候背包就被装满了。
由上述表格可知,这个问题的最优解就是物品ID组成的(2,10,6,3,5,8),其中最后一个物品是部分装入,只装入了1/4,最后装入物品的最大价值是70.5。
4、算法详解
根据之前的分析结果,我们首先定义一个结构体:
structnode{ doublew; //每种物品的重量doublev; //每种物品的价值doublep; //每种物品的耽误重量价值}
然后我们对物品按单位重量价值进行排序,我们仍然利用之前讲过的排序函数sort最物品按单位重量价值从大到小进行排序,在对结构体类型的数据进行排序时,如果没有自定义比较函数,sort函数将不知道按照结构体的哪个成员进行排序,因此我们自定义比较函数cmp,指定按照物品的单位重量价值进行降序排列:
boolcmp(nodea,nodeb){ //自定义比较函数cmpreturna.p>b.p; //指定按照物品的单位重量价值进行降序排序} sort(s,s+n,cmp); //前两个参数分别为待排序数组的首地址和尾地址,cmp为比较函数
然后我们使用贪心算法求解问题,在单位重量价值排序的基础上,如果当前物品的重量小于或等于剩余容量,则可以装入,将剩余容量减去当前物品的重量,同时将已装入物品的价值加上当前物品的价值。如果当前物品的重量大于剩余容量,则表示不可以全部装入,但可以部分装入,直到背包装满,将剩余容量乘以当前物品的单位重量价值,与已装入物品的价值相加,即为已装入物晶的最大价值。
doublesolve(intn,doubleW){ doublesum=0.0; //sum表示已装入物品的价值之和doublecleft=W; //背包的剩余容量for(inti=0;i<n;i++){ //使用贪心算法求解问题if(s[i].w<=cleft){ //如果物品的重量小于或等于剩余容量cleft-=s[i].w; sum+=s[i].v; } else{ //如果物品的重量大于剩余容量sum+=cleft*s[i].p; //部分装入break; } } returnsum; }
5、算法分析
- 时间复杂度:时间主要耗费在对物品按单位重量价值进行排序上,一般采用快速排序法,时间复杂度是O(nlogn)。
- 空间复杂度:空间主要耗费在存储物品的单位重量价值上,空间复杂度是O(n)。
6、整体代码
以下代码是随书附带的源码,大家可以看看:
//program 2.3 背包问题 usingnamespacestd; constintM=10005; structnode{ doublew;//每个物品的重量doublev;//每个物品的价值doublep;//性价比}s[M]; boolcmp(nodea,nodeb){//自定义比较函数 returna.p>b.p;//根据物品的单位价值从大到小排序} doublesolve(intn,doubleW){ doublesum=0.0;//sum表示示装入物品的价值之和doublecleft=W;//背包剩余容量 for(inti=0;i<n;i++){//贪心算法求解 if(s[i].w<=cleft){//如果物品的重量小于等于剩余容量 cleft-=s[i].w; sum+=s[i].v; } else{//如果物品的重量大于剩余容量 sum+=cleft*s[i].p;//部分装入break; } } returnsum; } intmain(){ intt,n;//t为测试用例个数,n为物品个数doubleW;//背包容量 cin>>t; while(t--){ cin>>n>>W; for(inti=0;i<n;i++){ cin>>s[i].w>>s[i].v; s[i].p=s[i].v/s[i].w;//每个宝物单位价值 } sort(s,s+n,cmp); cout<<solve(n,W)<<endl;//输出装入宝物的最大价值 } return0; }
三、最后我想说
本期博客就到这里结束了,目前举例了两个贪心算法实例,还有很多实例,我后续也会进行更新了,这个14天阅读挑战赛我应该只能更新五篇博客了,不过算法这方面我肯定不会断更的,因为算法很重要,必须要好好的提升自己这方面的知识。
谢谢大家!