贪心算法

贪心算法
基本思想
◇ 概念

下棋时,每一步的决策都需要考虑对后续棋局的影响。而在网球比赛中,选手的行为仅取决于当前的状况,选择当下最为正确的动作,而不关心后续的影响。这说明在某些情况下选择当下最佳行为的决策,可以得到一个最优解,但并非所有情况都如此,贪心算法适用与上述第二类问题。
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的是在某种意义上的局部最优解。
贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

◇ 思路

贪心算法的基本思路是从问题的某一个初始解出发一步一步地进行,根据某个优化测度,每一步都要确保能获得局部最优解。每一步只考虑一个数据,他的选取应该满足局部优化的条件。若下一个数据和部分最优解连在一起不再是可行解时,就不把该数据添加到部分解中,直到把所有数据枚举完,或者不能再添加算法停止 。
过程如下:
① 建立数学模型来描述问题;
② 把求解的问题分成若干个子问题;
③ 对每一子问题求解,得到子问题的局部最优解;
④ 把子问题的解局部最优解合成原来解问题的一个解。

◇ 特性

贪婪算法可解决的问题通常大部分都有如下的特性:
Ⅰ 随着算法的进行,将积累起其它两个集合:一个包含已经被考虑过并被选出的候选对象,另一个包含已经被考虑过但被丢弃的候选对象。
Ⅱ 有一个函数来检查一个候选对象的集合是否提供了问题的解答。该函数不考虑此时的解决方法是否最优。
Ⅲ 还有一个函数检查是否一个候选对象的集合是可行的,也即是否可能往该集合上添加更多的候选对象以获得一个解。和上一个函数一样,此时不考虑解决方法的最优性。
Ⅳ 选择函数可以指出哪一个剩余的候选对象最有希望构成问题的解。
Ⅴ 最后,目标函数给出解的值。
Ⅵ 为了解决问题,需要寻找一个构成解的候选对象集合,它可以优化目标函数,贪婪算法一步一步的进行。起初,算法选出的候选对象的集合为空。接下来的每一步中,根据选择函数,算法从剩余候选对象中选出最有希望构成解的对象。如果集合中加上该对象后不可行,那么该对象就被丢弃并不再考虑;否则就加到集合里。每一次都扩充集合,并检查该集合是否构成解。如果贪婪算法正确工作,那么找到的第一个解通常是最优的。


算法分析

⒈ 贪心算法适用的问题
贪心策略适用的前提是局部最优策略能导致产生全局最优解。
一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,即可做出判断。
⒉ 贪心算法的实现框架
从问题的某一初始解出发:

while(能朝给定总目标前进一步)
{
   利用可行的决策,求出可行解的一个解元素;
}
由所有解元素组合成问题的一个可行解;

⒊ 贪心策略的选择
因为用贪心算法只能通过局部最优解的策略来达到全局最优解,因此,一定要注意判断问题是否适合采用贪心算法策略,找到的解是否一定是问题的最优解。


例题
背包问题

有一个背包,背包容量是M=150kg。有7个物品,物品不可以分割成任意大小。要求尽可能让装入背包中的物品总价值最大,但不能超过总容量。
物品 A B C D E F G
重量 35kg 30kg 6kg 50kg 40kg 10kg 25kg
价值 10$ 40$ 30$ 50$ 35$ 40$ 30$
分析:
目标函数:∑pi最大
约束条件是装入的物品总重量不超过背包容量:∑wi<=M(M=150)
⑴ 根据贪心的策略,每次挑选价值最大的物品装入背包,得到的结果是否最优?
⑵ 每次挑选所占重量最小的物品装入是否能得到最优解?
⑶ 每次选取单位重量价值最大的物品,成为解本题的策略。
值得注意的是,贪心算法并不是完全不可以使用,贪心策略一旦经过证明成立后,它就是一种高效的算法。
贪心算法还是很常见的算法之一,这是由于它简单易行,构造贪心策略不是很困难。
可惜的是,它需要证明后才能真正运用到题目的算法中。
一般来说,贪心算法的证明围绕着:整个问题的最优解一定由在贪心策略中存在的子问题的最优解得来的。
对于例题中的3种贪心策略,都是无法成立(无法被证明)的,解释如下:
⑴ 贪心策略:选取价值最大者。
反例:
W=30
物品:A B C
重量:28 12 12
价值:30 20 20
根据策略,首先选取物品A,接下来就无法再选取了,可是,选取B、C则更好。
⑵ 贪心策略:选取重量最小。它的反例与第一种策略的反例差不多。
⑶ 贪心策略:选取单位重量价值最大的物品。
反例:
W=30
物品:A B C
重量:28 20 10
价值:28 20 10
根据策略,三种物品单位重量价值一样,程序无法依据现有策略作出判断,如果选择A,则答案错误。
【注意:如果物品可以分割为任意大小,那么策略3可得最优解】
对于选取单位重量价值最大的物品这个策略,可以再加一条优化的规则:对于单位重量价值一样的,则优先选择重量小的!这样,上面的反例就解决了。
但是,如果题目是如下所示,这个策略就也不行了。
W=40
物品:A B C
重量:25 20 15
价值:25 20 15
附:本题是个DP问题,用贪心法并不一定可以求得最优解,以后了解了动态规划算法后本题就有了新的解法。

背包问题

在8×8方格的棋盘上,从任意指定方格出发,为马寻找一条走遍棋盘每一格并且只经过一次的一条路径。
【初步设计】
首先这是一个搜索问题,运用深度优先搜索进行求解。算法如下:
⒈ 输入初始位置坐标x,y;
⒉ 步骤 c:
如果c> 64输出一个解,返回上一步骤c--
(x,y) ← c
计算(x,y)的八个方位的子结点,选出那些可行的子结点
循环遍历所有可行子结点,步骤c++重复2
显然2是一个递归调用的过程,主要代码如下:

void dfs(int x,int y,int count)
{
    int i,tx,ty;
    if(count>N*N)
    {
        output_solution();//输出一个解
        return;
    }
    for(i=0; i<8; i++)
    {
        tx=hn[i].x;//hn[]保存八个方位子结点
        ty=hn[i].y;
        s[tx][ty]=count;
        dfs(tx,ty,count+1);//递归调用
        s[tx][ty]=0;
    }
}

这样做是完全可行的,它输入的是全部解,但是马遍历当8×8时解是非常之多的,用天文数字形容也不为过,这样一来求解的过程就非常慢,并且出一个解也非常慢。
怎么才能快速地得到部分解呢?
下面介绍用贪心算法解决该问题:
其实马踏棋盘的问题很早就有人提出,且早在1823年,J.C.Warnsdorff就提出了一个有名的算法。在每个结点对其子结点进行选取时,优先选择‘出口’最小的进行搜索,‘出口’的意思是在这些子结点中它们的可行子结点的个数,也就是‘孙子’结点越少的越优先跳,为什么要这样选取,这是一种局部调整最优的做法,如果优先选择出口多的子结点,那出口少的子结点就会越来越多,很可能出现‘死’结点(顾名思义就是没有出口又没有跳过的结点),这样对下面的搜索纯粹是徒劳,这样会浪费很多无用的时间,反过来如果每次都优先选择出口少的结点跳,那出口少的结点就会越来越少,这样跳成功的机会就更大一些。这种算法称为为贪心算法,也叫贪婪算法或启发式算法,它对整个求解过程的局部做最优调整,它只适用于求较优解或者部分解,而不能求最优解。这样的调整方法叫贪心策略,至于什么问题需要什么样的贪心策略是不确定的,具体问题具体分析。实验可以证明马遍历问题在运用到了上面的贪心策略之后求解速率有非常明显的提高,如果只要求出一个解甚至不用回溯就可以完成,因为在这个算法提出的时候世界上还没有计算机,这种方法完全可以用手工求出解来,其效率可想而知。

最小生成树

贪心算法当然也有正确的时候。求最小生成树的Prim算法和Kruskal算法都是漂亮的贪心算法。
求最小生成树在数据结构之图(七)的时候讲过,具体如下:
Ⅰ 普里姆(Prim)算法
问题1:有一块木板,板上钉了一些钉子,这些钉子可以由一些细绳连接起来。如果每个钉子可以通过一根或者多根细绳连接起来,那么如何用最少的细绳把所有的钉子连接起来? 问题2:在某地分布着N个村庄,现在需要在N个村庄之间修路,每个村庄之间的距离不同,问怎么修才能使路程最短,事各个村庄连接起来。
以上问题都可以归纳为最小生成树的问题,用正式的表述方法描述为:给定一个无方向的带权图G=(V,E),最小生成树的集合T,T是以最小代价连接V中所有顶点互相连接的边E的权值最小集合。集合T中的边能够形成一棵树,这是因为每个结点(除了根结点)都能向上找到一个父结点。解决最小生成树问题已经有前人做过相关的研究,Prim(普里姆)算法和Kruskal(克鲁斯卡尔)算法,分别从点和边下手解决了该问题。
Prim算法简介:普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)),且其所有边的权值之和亦为最小。该算法于1930年由捷克数学家沃伊捷赫·亚尔尼克(英语:Vojtěch Jarník)发现;并在1957年由美国计算机科学家罗伯特·普里姆(英语:Robert C. Prim)独立发现;1959年,艾兹格·迪科斯彻再次发现了该算法。因此,在某些场合,普里姆算法又被称为DJP算法、亚尔尼克算法或普里姆-亚尔尼克算法。
Prim算法从任意一个顶点开始,每次选择一个与当前顶点集最近的一个顶点,并将两顶点之间的边加入到树中。Prim算法在找当前最近顶点时使用到了贪心算法。
算法描述如下:
① 在一个加权连通图中,顶点集合V,边集合为E。
② 任意选中一个点作为初始顶点,标记为visit,计算所有与之连接的点的距离,选择距离最短的,标记为visit。
③ 重复以下操作,知道所有点都被标记为visit:在剩下的点中,计算与已标记visit点距离最小的点,标记visit,证明加入了最小生成树。

最小生成树的过程如下:
① 起初,从顶点A开始生成最小生成树,如下图所示:
原始图
② 选择顶点A后,顶点置成visit,计算周围与它连接的点的距离。如下图所示:
访问顶点A
③ 与之相连的点距离分别为7、6、4,选择E点的距离最短,标记E,同时将AE边加入最小生成树,如下图所示:
访问顶点E
④ 计算与A、E相连的点的距离(已经标记的点不算),因为与A相连的已经计算过了,只需要计算与E相连的点,如果一个点与A、E都相连,那么它与A之间的距离之前已算过,如果它与E的距离更近,则更新距离值,这里计算的是未标记的点距离标记的点的最近距离,B、A之间距离为7,B、E之间距离为6,更新B和已访问的点集距离为6,而EF、EC的距离分别为8,9,所以还是标记B,将BE边加入最小生成树。如下图所示:
访问顶点B
⑤ DB之间距离最短,标记D点,将BD边加入最小生成树。如下图所示:
访问顶点D
⑥ FD之前距离为7,FB之间距离为4,更新F的最短距离值为4,标记F,将BF边加入最小生成树。如下图所示:
访问顶点F
⑦ EC距离为9,FC距离为1,更新C点的最短距离值为1,标记C,将FC加入最小生成树。如下图所示:
访问顶点C


Ⅱ 克鲁斯卡尔(Kruskal)算法
Kruskal是另一种计算最小生成树的算法,其算法原理如下:首先,将每个顶点放入其自身的数据集合中。然后,按照权值得升序来选择边,当选择每条边时,判断定义边的顶点是否在不同的数据集中。如果是,将此边插入最小生成树的集合中,同时,将集合中包含每个顶点的联合体取出;如果不是,就移动到下一条边。重复这个过程,直到所有的的边都探查过。
通过一组图式来变现算法的过程如下:
① 初始情况,一个联通图,定义针对边的数据结构,包括七点。终点和边长度。如下图所示:
原始图
② 首先找到第一短的边AB,将AB放入到一个集合中,如下图所示:
访问顶点A和B
③ 继续找到第二短的边DE,将DE放入到一个集合中,如下图所示:
访问顶点D和E
④ 继续找,找到第三短的边AC,因为A、B已经在一个集合里,再将C加入,如下图所示:
访问顶点A和C
⑤ 继续找,找到B、C,因为B、C已经同属于一个集合,连起来的话就形成了环,所以边B、C不加入最小生成树。如下图所示:
去掉BC边
⑥ 继续搜索,找到CD,因为D、E是一个集合的,B、A、C是一个集合,所以再合并这两个集合,如下图所示:
这里写图片描述
这样所有的点都归到了一个集合里,生成了最小生成树。
可参考:数据结构之图(七)
想学习更多的贪心算法例题可去:贪心算法例题

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值