背景介绍:贪婪算法可以说是离我们生活最近的一种算法,人总是贪婪的嘛,所以这种算法的设计是很符合人性的。之所以这么说,是因为人们会在生活中有意无意的使用贪婪算法来解决问题。最常见的就是找零钱了,每个人都没学过该怎么找零钱,但在所有面额的钱都充足时,每个人都会找出同样组合来凑够需要的钱。其实这里面就是贪婪算法在起作用。
设计思路:贪婪法的设计思路可以从两方面来理解,即直观上和数学上。从直观上理解贪婪算法就是用最快的方法来解决问题。在这里面“快”是主要目标,例如上面找零钱的例子,假如你要找的零钱为6.6元。那首先要拿一张5元的,因为这可以使你凑的钱增长最快。如果人民币有6元的面额那你肯定会选6元的而不是拿两张别的来凑6元;从数学上来理解贪婪算法就是在做判断时以当前最优解为目标,类似于最优化中的最速下降法。这种方法的好处是解题速度极快,基本上是一次历遍就可以完成。
算法缺陷:正如做人不能太贪婪一样,贪婪算法本身有着致命的缺陷,这使得其应用背景收到了很多限制。因为算法是取的局部最优解,没有考虑以后的问题。这就像一个自私自利的人一样,虽然短时间内可以获得一些利益,但长期以往,很难会有大的成就。当然,社会很复杂,也许会有人一直自私下去而生活的还不错。这体现在算法上就是在一些情况下(具体下面会提到),贪婪算法是可以得到最优解的,这对于算法设计来说当然是好事。
算法难点:算法的难点是和其缺陷直接相关的,贪婪算法的难点就是如何证明你的算法可以得到最优解。这里面有两个性质,只有满足这两个性质的问题才可以用贪婪法来解决,它们分别是贪婪选择性质和最优子结构性质。所谓贪婪选择性质,就是问题的全局最优解可以通过一系列局部最优解的选择来达到,每进行一次选择,就得到一个局部最优解,并且把原问题化简为一个规模更小的同类型子问题。而最优子结构,是说其全局最优解中包含其子问题的最优解。这是一个充分必要的关系,贪婪选择性质是局部最优解一定导致全局最优解,而最优子结构是全局最优解一定包含局部最优解。这两个概念有点儿饶,我们举个例子来说一下:加入我们要选择从北京到广州的最短路径,条件是只能途径省会城市。这个问题是否具有最优子结构性质呢?也就是说假如我们得到了最优路线为北京——石家庄——郑州——武汉——长沙——广州。那这个解中的北京——石家庄——郑州——武汉是否是从北京到武汉的最短路径呢?显然是的,用反证法可轻松证明。那这个问题是否具有贪婪选择性质呢?也就是说选择在北京的时候选择离北京最短的省会城市(天津)能否将原问题“化简”为从天津到广州的子问题呢?这显然是不能的!也就是说每次都选择局部最优解并不能得到全局最优解。这也说明了该问题不能用贪婪法求解。
设计方法:前面已经提到了,贪婪算法的难度在于证明其算法有效性,一旦有效性被证明,那设计方式是比较简单直接的。贪婪算法的应用背景一般是选择问题,即从多个可行解中选择一个解作为当前解,当然这个选择策略就是我们的贪心策略。所以算法中大多会有一个排序的过程,通过一个指标对当前可行解排序然后选择指标最好的作为解的一部分并入局部解。如此迭代循环,最终得到原问题的最优解。
算法实例:
一方面,贪婪算法的实例在网上和各种资料上都非常多,所以在这里就举一个有代表性的就好了。另一方面,如果你已经查看了一些例子,你会发现很多例子都是重复的。这也说明了单纯的贪婪算法应用背景还是比较少的,贪婪算法大多作为一种辅助算法,在其他算法尤其是启发式算法中给算法的进行提供一种方向性的选择。
背包问题:
背包问题是贪婪算法最经典的例子之一,今天就拿它开刀吧。问题背景就不用多介绍了,不会的百度一下有一万个答案。在这里主要注意一下背包问题和0—1背包问题的不同。我们这里研究的背包问题是可以将物品分割的那种。0—1背包不符合贪心选择策略,并不能用贪心算法解决。
分析前先说一句,其实网上各种大神分析有很多。这里是重点分析了算法的应该过程,即如果从算法的设计的范式中得到具体问题的设计策略,并在此后给出了严格的数学证明。我相信这部分可能是让很多人头疼的,我更相信绝大部分人不会仔细把证明看一遍,然而,但是,尽管如此,我还是给出了证明(当然是抄的,不是本人证明的),希望大家还是仔细捋一遍,原因前面也说了,算法设计没啥难度,关键就是证明你的算法是对的。
好了,开始分析
首先很明确的是我们的问题是一个选择问题,要选择的就是要装入背包的物品。怎么选择呢,即设计怎样的选择策略呢,说的更直接一点,该怎么将物品排序呢?总不能随机选一个吧,那不成了随机算法了么。物品有两个属性,重量和价值,那我们就来分别看一下:以重量排序可以吗?把最重的先装进去?这个肯定是不行,反例很容易想到。那以价格排序?先把最值钱的放进去?这个好像有点儿道理,毕竟要求总价值最大嘛。可在仔细想想,如果一个物品虽然很值钱,但重量更是大的出奇。从直观上理解这就不合适。这里的问题相当于是用剩余重量来换价值,这么看那些价值比较高而重量更大的物品就像是质量较好但价格超级贵的商品。会过日子的人肯定不会买它,那我们该卖什么样儿的呢?怎么买东西是成功的呢?显然这里有个性价比的指标,我们要买的就是性价比最高的。而返回原问题我们就是要装单位质量最贵的物品。即应该以价格/质量作为排序指标。
有了选择策略那问题就解决一大半了,我们就是不断选择单价最高的物品装入背包,直到装完所有物品或背包满了。主要可以切割,所有一定会装满。
直接给出代码吧:
<span style="font-size:14px;">double knapsack_gready(double M,C_object instance[],double x[],size_t n)算法前面计算单价用时为O(n),归并排序(具体代码就不给出了)用时为O(nlogn),最后选择过程用时O(n),所有整个算法的时间复杂度为O(nlogn)。
{
//功能:解决背包问题
//输入:背包容量M,物品数组首地址instance,物品总个数n
//输出:选择结果向量x,显示最后装入的总价值
//说明:贪心算法
double m = 0.0;
double p = 0.0;
for(size_t i=0;i<n;i++)
{
instance[i].v=instance[i].p/instance[i].w;
}
merge_sort(instance,n);
m = M;
for(size_t i=0;i<n;i++)
{
if(instance[i].w<=m)
{
x[i]=1; m -= instance[i].w;
p += instance[i].p;
}
else
{
x[i] = m/instance[i].w;
p += x[i]*instance[i].p;
break;
}
}
return p;
}</span>
下面给出算法的证明,即当物体排序后,用knapsack_gready算法可以求得原问题最优解。
【证明】设解向量X=(x1,x2,…,xn)分两种情况:
(1)所有物品全部装入,此时X=(1,1,…,1),显然这就是最优解。(装没了,肯定价值最大了)
(2)物品没有装完,即有X=(1,1,…1,a,0,…0)。其中a为大于0小于1的小数,即是装了一半儿的那个物品。
这时有 w1x1+w2x2+…+wnxn=M1=M
假设此时有最优解向Y=(y1,y2,…,yn),且满足w1y1+w2y2+…+wnyn=M2=M
由于X!=Y,那肯定存在第一个不相等的元素,即xk!=yk,这时有两种情况:
①xk<yk,因为yk<=1,则xk<1,而有解的性质知xk以后的x肯定为0。所以有上面的M1<M2,这和已知是冲突的,所以不对,
②xk>yk,由于M1=M2,所以yk后面肯定还有y不等于0。我们可以把后面这个不为0的y的值减少,而增加yk的值。这样得到的新解肯定比Y更好。如果yk后面全为0了,那新解=X,即X为最优解。如果yk后面还有大约0的元素,那我们继续减小其值,增加到yk上,如此反复,最终有新解=X,即X为最优解。【证毕】
最后总结:贪婪是每个人都有的,贪婪算法正是来自于人们内心的贪婪。这种贪婪的优点是效果非常好,但难点是证明你的贪婪是合理的。正所谓君子爱财取之以道,只有道正了,那财产才是合法的。另一方面,只要是合法的,那无论你有多少财产,那都是你自己的,别人没权利说三道四。