贪心算法(一)

概念:贪心法,指的是从问题的初始状态出发,通过若干次的贪心选择而得出最优值(或较优解)的一种解题方法。
其实,从“贪心策略”一词我们便可以看出,贪心策略总是做出在当前看来是最优的选择,也就是说贪心策略并不是从整体上加以考虑,它所做出的选择只是在某种意义上的局部最优解,而许多问题自身的特性决定了该题运用贪心策略可以得到最优解或较优解。

贪心法的特点

1、贪心选择性质: 所谓贪心选择性质是指应用同一规则f,将原问题变为一个相似的、但规模更小的子问题、而后的每一步都是当前看似最佳的选择。这种选择依赖于已做出的选择,但不依赖于未做出的选择。从全局来看,运用贪心策略解决的问题在程序的运行过程中无回溯过程。
2、局部最优解: 由于运用贪心策略解题在每一次都取得了最优解,但能够保证局部最优解的不一定是贪心算法。如后面我们要学习的动态规划算法、宽度优先搜索(BFS)等算法中的解题过程亦可以满足局部最优解。

贪心法的思路

基本思路
从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到某算法中的某一步不能再继续前进时,算法停止。


实现该算法的过程:
   从问题的某一初始解出发;
     while 能朝给定总目标前进一步 do
  求出可行解的一个解元素;
    由所有解元素组合成问题的一个可行解

 

  • 贪心算法其实是挺容易实现的,因为它是符合人的一般思维过程,按照人解决问题的思路来解决问题,这一点可以在下面的例题体现的非常明了。
  • 贪心法的优势在于编程简单、运行效率高、空间复杂度低,是信息学竞赛中的一个有力武器,受到了广大信息学爱好者的青睐。
  • 贪心法的应用非常广泛,比如在图论中,“Dijkstra算法”求解单源最短路径问题、“Prim 算法”和“Kruskla 算法”求最小生成树问题、哈夫曼树的构造(哈夫曼算法)等都是基于贪心法。

 

举例

例1:在N 行M 列的正整数矩阵中,要求从每行中选出1 个数,使得选出的总共N 个数的和最大。
【算法分析】
要使总和最大,则每个数要尽可能大,自然应该选每行中最大的那个数。因此,我们设计出如下算法:
读入N, M,矩阵数据;
Total := 0;
For I := 1 to N do begin {对N 行进行选择}
选择第I行最大的数,记为K;
Total := Total + K;
End;
输出最大总和Total;
从上例中我们可以看出,和递推法相仿,贪心法也是从问题的某一个初始解出发,向给定的目标递推。但不同的是,推进的每一步不是依据某一固定的递推式,而是做一个局部的最优选择,即贪心选择(在例中,这种贪心选择表现为选择一行中的最大整数),这样,不断的将问题归纳为若干相似的子问题,最终产生出一个全局最优解。

 

例2、删数问题(delete.???)
[问题描述]
 输入一个高精度的正整数n(≤240位),去掉其中任意s个数字后剩下的数字按原左右次序将组成一个新的正整数。
编程,要求对给定的n和s,寻找一种方案,使得剩下的数字组成的新数最小。
[输入格式]
输入文件delete.in,共两行,第一行为一个整数n,第二行为一个整数s。
[输出格式]
输出文件delete.out,仅一行,表示最后剩下的最小数。
[样例输入]
178543
4
[样例输出]
13

[算法分析]
由于正整数n的有效数位为240位,所以很自然地采用字符串类型存贮n。那么如何决定哪s位被删除呢?是不是最大的s个数字呢?显然不是,大家很容易举出一些反例。为了尽可能逼近目标,我们选取的贪心策略为:每一步总是选择一个使剩下的数最小的数字删去,即按高位到低位的顺序搜索,若各位数字递增,则删除最后一个数字;否则删除第一个递减区间的首字符,这样删一位便形成了一个新数字串。然后回到串首,按上述规则再删下一个数字。重复以上过程s次为止,剩下的数字串便是问题的解了。
例如:n=178543,s=4。删数的过程如下:
          n=178543        {删掉8}  
              17543          {删掉7}
              1543            {删掉5}
              143              {删掉4}
              13                {解即为13}
这样,删数问题就与如何寻找递减区间首字符这样一个简单的问题对应起来。不过还要注意一个细节问题,就是删除若干个字符后可能会出现字符串串首有若干0的情况,甚至整个字符串都是0的情况。
按以上贪心策略编制的程序框架如下:

 1 begin
 2 输入n,s;
 3      while s>0 do
 4      begin
 5         i:=1{从串首开始找}
 6         while (i<length(n)) and (n[i]<=n[i+1]) do  i:=i+1 7         delete (n,i,1);   {删除字符串n的第i个字符}
 8         s:=s-1 9      end10      while (length(n)>1)and (n[1]=‘0’) do delete(n,11);
11        {删去串首可能产生的无用零}
12      输出n;
13 End;

 

例3、独木舟(KAJ.???)
[问题描述]
    我们计划组织一个独木舟旅行。租用的独木舟都是一样的,最多乘两人,而且载重有一个限度。现在要节约费用,所以要尽可能地租用最少的舟。你的任务是读入独木舟的载重量,参加旅行的人数以及每个人的体重,计算出所需要的租船数目。
[输入格式]
输入文件kaj.in,第一行是w(80≤w≤200),表示每条独木舟最大的载重量。第二行是整数n(1≤n≤30000,参加旅行的人数。接下来的n行,每行是一个整数Ti(5≤Ti≤w),表示每个人的重量。
[输出格式]
输出文件仅一行,表示最少的租船数目。
[样例输入]
100
9
90
20
20
30
50
60
70
80
90
[样例输出]
6

[算法分析]
  基于贪心法,找到一个重量最大的人,让它尽可能与重量大的人同乘一船。如此循环直至所有人都分配完毕即可统计出所需要的独木舟数。
特别注意的是,局部贪心的选择是否可以得出全局最优是能否采用贪心法的关键所在。对于能否使用贪心策略,应从理论上予以证明。下面我们看看另一个问题。

 

例4:部分背包问题
给定一个最大载重量为M 的卡车和N 种食品,有食盐,白糖,大米等。已知第i 种食品的最多拥有Wi公斤,其商品价值为Vi元/公斤,编程确定一个装货方案,使得装入卡车中的所有物品总价值最大。

【算法分析】
因为每一个物品都可以分割成单位块,单位块的利益越大显然总收益越大,所以它局部最优满足全局最优,可以用贪心法解答,

方法如下:先将单位块收益按从大到小进行排列,然后用循环从单位块收益最大的取起,直到不能取为止便得到了最优解。因此我们非常容易设计出如下算法:

 1 问题初始化; {读入数据}
 2 按Vi从大到小将商品排序;
 3 I := 1;
 4 repeat
 5   if M = 0 then Break; {如果卡车满载则跳出循环}
 6   M := M - Wi;
 7   if M >= 0 then 将第I 种商品全部装入卡车
 8    else
 9     将(M + Wi)重量的物品I 装入卡车;
10    I := I + 1; {选择下一种商品}
11 until (M <= 0) OR (I >N)

在解决上述问题的过程中,首先根据题设条件,找到了贪心选择标准(Vi),并依据这个标准直接逐步去求最优解,这种解题策略被称为贪心法。

小结

利用贪心策略解题,需要解决两个问题:
首先,确定问题是否能用贪心策略求解;一般来说,适用于贪心策略求解的问题具有以下特点:
①可通过局部的贪心选择来达到问题的全局最优解。运用贪心策略解题,一般来说需要一步步的进行多次的贪心选择。在经过一次贪心选择之后,原问题将变成一个相似的,但规模更小的问题,而后的每一步都是当前看似最佳的选择,且每一个选择都仅做一次。
②原问题的最优解包含子问题的最优解,即问题具有最优子结构的性质。在背包问题中,第一次选择单位重量价值最大的货物,它是第一个子问题的最优解,第二次选择剩下的货物中单位重量价值最大的货物,同样是第二个子问题的最优解,依次类推。
③其次,如何选择一个贪心标准?正确的贪心标准可以得到问题的最优解,在确定采用贪心策略解决问题时,不能随意的判断贪心标准是否正确,尤其不要被表面上看似正确的贪心标准所迷惑。在得出贪心标准之后应给予严格的数学证明
以上几个例题所用的贪心思想还是很明显的,选取的贪心策略也很直观(可能也是唯一的),不仅能得到最优解,而且解的正确性也可以得到严格的证明。但有些问题采用贪心法求解时,贪心策略可能有多种,在针对性不同的测试数据下,带来的效果可能也不一样,并不是总能得到最优解,而且解的正确性证明也很困难,最典型的例子就是0/1背包问题。

例5、0/1背包问题(bb.???)
[问题描述]
有一容量为weight(longint范围以内)的背包。现在要从n(<20)件物品中选取若干装入背包中,每件物品i的重量为w[i],价值为p[i]。定义一种可行的背包装载为:背包中物品的总重量不能超过背包的容量,并且一个物品要么全部选取,要么不选取。定义最佳装载是指所装入的物品价值最高,并且是可行的背包装载。w[i],p[i]均为integer以内的正整数。
[样例输入]
11              {weight}
4                {n}
2 4 6 7       {w[i]}
6 10 12 13 {p[i]}
[样例输出]
0 1 0 1
23
 
[算法分析]
设数组choice[1..n],若choice[i]=1表示物品i装入背包中,choice[i]=0表示物品i不装入背包中。则choice=[0,1,0,1]是一种可行的背包装载方案,也是一种最佳装载方案,此时总价值为23。
0/1背包问题有好几种贪心准则,每种贪心准则都是采用多步过程来完成背包的装入,在每一步过程中利用某种固定的贪心准则选择一个物品装入背包。

  • 一种贪心准则为:从剩余的物品中,选出可以装入背包的价值最大的物品,利用这种规则,价值最大的物品首先被装入(假设有足够容量),然后是下一个价值最大的物品,如此继续下去。这种贪心准则不能保证得到最优解。例如weight=105,n=3,w=[100,10,10],p=[20,15,15]。按照以上这种“价值贪心准则”,获得的解为choice=[1,0,0],这种方案的总价值为20。而最优解为choice =[0,1,1],其总价值为30。
  • 另一种方案是“重量贪心准则”,即从剩下的物品中选择可装入背包的重量最小的物品。虽然这种规则对于前面的例子能产生最优解,但在一般情况下不一定能得到最优解,例如weight=25,n=2,w=[10,20],p=[5,100]。获得的解为choice=[1,0],总价值为5,比最优解choice =[0,1]的总价值100要差。
  • 本题的另一方案为“单位价值贪心准则”,这种方案是从剩余物品中选择可装入包的p[i] /w[i]值最大的物品,这种策略也不能保证得到最优解。例如weight=30,n=3,w=[20,15,15],p=[40,25,25]。获得的解为choice=[1,0,0],总价值为40。而最优解为choice=[0,1,1]的总价值为50。

既然任何一种贪心策略都不能保证得到最优解,那么本题是不是不应该用贪心法?其实我们不必因所考察的几个贪心策略都不能保证得到最优解而沮丧(或放弃),因为0/1背包问题是一个复杂的NP问题。对于这类问题,也许根本就不可能找到具有多项式时间的算法。虽然按“单位价值贪心准则”递减的次序装入物品不能保证得到最优解,但它是一个直觉上近似的解,而且时间复杂度仅为O(nlog2n)。造成多种贪心都不能全对的原因其实在于,在很多情况下,无法将背包填满,空间上的浪费间接降低了实际上的单位价值。在实际应用中,我们还可以就在一个程序中用多种贪心准则得到多个最优解,然后再打擂台选择一个最最优的输出。当然,本题还有一种思路,就是用动态规划求解。

说明:
数学上著名的“NP问题”,完整的叫法是“NP完全问题”,也即“NP COMPLETE问题”,NP就是Non-deterministic Polynomial的问题,中文意思是多项式复杂程度的非确定性问题。什么是非确定性问题呢?有些计算问题是确定性的,比如加减乘除之类,你只要按照公式推导,按部就班一步步来,就可以得到结果。但是,有些问题是无法按部就班直接计算出来的。比如,找大质数的问题,有没有一个公式,你只要一套这个公式,就可以一步步推算出来,下一个质数应该是多少呢?这样的公式是没有的。再比如,大的合数分解质因数的问题,有没有一个公式,把合数代进去,就直接可以算出,它的因子各自是多少?也没有这样的公式。这种问题的答案,是无法直接计算得到的,只能通过间接的“猜算”来得到结果。这就是非确定性问题。


这些问题通常都有一些算法,它不能直接告诉你答案是什么,但可以告诉你,某个可能的结果是正确的答案还是错误的。这个可以告诉你“猜算”的答案正确与否的算法,假如可以在多项式时间内算出来,就叫做多项式非确定性问题。而如果这个问题的所有可能答案,都可以在多项式时间内进行正确与否的验算的话,就叫完全多项式非确定问题。完全多项式非确定性问题可以用穷举法得到答案,一个个检验下去,最终便能得到结果。但是这样的算法,它的时间复杂度是指数关系,因此计算的时间随问题的复杂程度成指数的增长,很快便变得不可计算了。


因此,贪心不能简单进行,而需要全面的考虑,最后得到证明

贪心法的正确性证明

贪心法的正确性证明是个难点,尤其是在非常有限的竞赛时间内。所以,很多选手往往是大胆假设自己选择的贪心策略是正确的,这样难免会出错,但也是一种不得已而为之的办法。其实贪心法的证明虽然不容易,但一些常见的方法还是值得总结的。
当一个贪心算法不能确定其100%正确,使用之前就应该尝试证明它的不正确性。而要证明其不正确,一种最简单的方法就是举一个反例。其实,要严格证明一个贪心算法的正确性是很困难,目前最有效的一种方法叫“矩阵胚理论”,但是很复杂,不再介绍。其实,即使一个贪心算法是不完全正确的,我们也可以努力寻找一些调整方法,或制定多种贪心策略,通过调整优化、比较择优来争取得到最优解,甚至也可以先得到一个较优解,然后在此基础上进行搜索剪枝或分支定界。常见的证明方法有以下这样几种。

  • 反证法

        用贪心的策略,依次构造出一个解S1,假设最优解S2不同于S1,可以证明是矛盾的,从而得出S1就是最优解。

        P1005 NOIP1998_T2_联接数

  • 构造法

        根据描述的算法,用贪心的策略,依次构造出一个解,可证明一定是合法的解。即用贪心法找可行解。
        P1425 贪心算法_取数游戏

  • 调整法

              用贪心的策略,依次构造出一个解S1。假设最优解S2不同于S1,找出不同之处,在不破坏最优性的前提下,逐步调整S2,最终使其变为S1。从而S1也是最优解。
        P1421 贪心算法_排队接水

posted @ 2016-02-04 15:30  ZJQCation  阅读(7327)  评论(0编辑  收藏  举报