首页 > 代码库 > 贪心算法

贪心算法

贪心算法

贪心算法通过一系列的选择来得到问题的解。它所做的每一个选择都是当前状态下局部的最好选择,即贪心选择。
贪心选择的一般特征:贪心选择性质和最优子结构性质。

贪心选择性质:

所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。在动态规划算法中,每步所做的选择往往依赖于相关子问题的解。因而只有在解出相关子问题后,才能做出选择。而在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择。然后再去解出做出这个选择后产生的相应的子问题。贪心算法所做的贪心选择可以依赖于以往所做过的选择,但决不依赖于将来所做的选择,也不依赖于子问题的解。正是由于这种差别,动态规划算法通常以自底向上的方式解各个问题,而贪心算法则通常以自顶向下的方式进行,以迭代的方式做出相继的贪心选择,没做一次贪心选择就将所求问题简化为规模更小的子问题。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所做的贪心选择最终导致问题的整体最优解。

最优子结构性质:

当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。

弹性算法与动态规划算法的差异:

贪心算法和动态规划算法都要求问题具有最优子结构性质,这是两类算法的一个共同点。大多数时候,能用贪心算法求解的问题,都可以用动态规划算法求解。但是能用动态规划求解的,不一定能用贪心算法进行求解。

活动安排问题:

问题:

设有n个活动的集合E={1,2,...,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每个活动i都有一个要求使用该资源的起始时间Si和一个结束时间Fi,且Si<Fi。如果选择了活动i,则它在半开时间区间[Si,Fi)内占用资源。若区间[Si,Fi)与区间[Sj,Fj)不相交,则称活动i与活动j是相容的。也就是说Si>=Fj或Sj>=Fi时,活动i与活动j相容。活动安排问题就是要在所给的活动集合中选择出最大的相容活动子集合。

关于证明:

这个问题可以使用贪心算法进行求解。其实这个问题的关键并不是如何用贪心算法求解,而是如何证明这个问题可以用贪心算法求解。鉴于证明的复杂性,还是不在此讨论证明问题。其实贪心算法问题的证明无非都是用数学归纳法证明,不错就是那个传说中万能证明法,数学归纳法。还是直接讨论实现过程吧。

实现:

首先将活动按照活动的结束时间非递减:F1<=F2<=...<=Fn排序。如果所给的活动未排序,则先将活动按此序排列,时间复杂度一般为O(nlogn)可解决。以下是解决问题的算法。

 1 //贪心算法-活动安排的实现 2  3 #include "stdafx.h" 4 #include "stdio.h" 5  6 template<class Type> 7 void GreedySelector(int n,Type s[],Type f[],bool A[]) 8 { 9     A[0]=1;    //第一个直接选取10     int j=0;11     for(int i=1;i<n;i++)12     {13         if(s[i]>=f[j]){A[i]=true;j=i;}    //如果第i+1的活动的开始时间大于或等于第i个活动的结束时间,则标记该活动14         else A[i]=false;15     }16 }17 18 int _tmain(int argc, _TCHAR* argv[])19 {20     //初始化数据21     int n=3;22     int s[3]={1,2,4};    //开始时间23     int f[3]={3,3,5};    //结束时间24     bool A[3]={0,0,0};    //数组A用于标记是否选择活动,1表示选择,0表示不选择25 26     GreedySelector<int>(n,s,f,A);27     for(int i=0;i<n;i++)28     {29         printf("%d\n",A[i]);30     }31 }

 


该算法的贪心选择的意义是使剩余的可安排的时间段极大化,以便安排尽可能多的相容活动。也就是说,每次选择完了之后,保证这次的选择之后留下的时间是最多的。

测试结果

时间复杂度:

GreedySelector算法效率极高。当输入的数据是已经按照结束时间非递减排序好的时候,算法只需要O(n)的时间安排n个活动,使最多的活动能相容地使用公共资源。

适用范围:

贪心算法并不能总求得问题的整体最优解。但对于某些问题,却总能求得整体最优解,这要看问题时什么了。只要能满足贪心算法的两个性质:贪心选择性质和最优子结构性质,贪心算法就可以出色地求出问题的整体最优解。即使某些问题,贪心算法不能求得整体的最优解,贪心算法也能求出大概的整体最优解。如果你的要求不是太高,贪心算法是一个很好的选择。最优子结构性质是比较容易看出来的,但是贪心选择性质就没那么容易了,这个时候需要证明。证明往往使用数学归纳法。

适用范围个人总结的一句话,能证明用就可以用。(因为个人觉得证明比算法实现还难)

 

 0-背包问题 

给定n种物品和一个背包。物品i的重量是w ,其价值为v ,背包的容量为c.问应如何选择装入背包中的物品,使得装入背包中物品的总价值最大? 在选择装入背包的物品时,对每种物品i只有两种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i。

    此问题的形式化描述是,给定c>0,wi>0,vi>0,1≤i≤n,要求找出一个n元0—1向

量(xl,x2,…,xn), ,使得 ≤c,而且 达到最大。

      背包问题:与0-1背包问题类似,所不同的是在选择物品i装入背包时,可以选择物品i的一部分,而不一定要全部装入背包。

    此问题的形式化描述是,给定c>0,wi>0,vi>0,1≤i≤n,要求找出一个n元向量

(x1,x2,...xn),0≤xi≤1,1≤i≤n 使得 ≤c,而且 达到最大。

    这两类问题都具有最优子结构性质。对于0—1背包问题,设a是能够装入容量为c的背包的具有最大价值的物品集合,则aj=a-{j}是n-1个物品1,2,…,j—1,j+1,…,n可装入容量为c-wi叫的背包的具有最大价值的物品集合。对于背包问题,类似地,若它的一个最优解包含物品j,则从该最优解中拿出所含的物品j的那部分重量wi,剩余的将是n-1个原重物品1,2,…,j-1,j+1,…,n以及重为wj-wi的物品j中可装入容量为c-w的背包且具有最大价值的物品。

    虽然这两个问题极为相似,但背包问题可以用贪心算法求解,而0·1背包问题却不能用贪心算法求解。用贪心算法解背包问题的基本步骤是,首先计算每种物品单位重量的价值

vj/wi然后,依贪心选择策略,将尽可能多的单位重量价值最高的物品装入背包。若将这种物品全部装入背包后,背包内的物品总重量未超过c,则选择单位重量价值次高的物品并尽可能多地装入背包。依此策略一直进行下去直到背包装满为止。具体算法可描述如下:

 void knapsack(int n, float m, float v[ ], float w[ ], float x[ ] )

 sort(n,v,w);

 int i;

 for(i= 1;i<= n;i++) x[i] = o;

 float c = m;

 for (i = 1;i < = n;i ++) {

     if (w[i] > c) break;

    x[i] = 1;

    c-= w[i];

    }

 if (i < = n) x[i] = c/w[i];

算法knapsack的主要计算时间在于将各种物品依其单位重量的价值从大到小排序。因此,算法的计算时间上界为o(nlogn)。当然,为了证明算法的正确性,我们还必须证明背包问题具有贪心选择性质。 

这种贪心选择策略对0—1背包问题就不适用了。看图2(a)中的例子,背包的容量为50千克;物品1重10千克;价值60元;物品2重20千克,价值100元;物品3重30千克;价值120元。因此,物品1每千克价值6元,物品2每千克价值5元,物品3每千克价值4元。若依贪心选择策略,应首选物品1装入背包,然而从图4—2(b)的各种情况可以看出,最优的选择方案是选择物品2和物品3装入背包。首选物品1的两种方案都不是最优的。对于背包问题,贪心选择最终可得到最优解,其选择方案如图2(c)所示。

 

    对于0—1背包问题,贪心选择之所以不能得到最优解是因为它无法保证最终能将背包装满,部分背包空间的闲置使每千克背包空间所具有的价值降低了。事实上,在考虑0—1背包问题的物品选择时,应比较选择该物品和不选择该物品所导致的最终结果,然后再作出最好选择。由此就导出许多互相重叠的于问题。这正是该问题可用动态规划算法求解的另一重要特征。动态规划算法的确可以有效地解0—1背包问题。

定义  所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。

 

  贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解。

 

  贪心算法的基本思路如下:

 

  1.建立数学模型来描述问题。

 

  2.把求解的问题分成若干个子问题。

 

  3.对每一子问题求解,得到子问题的局部最优解。

 

  4.把子问题的解局部最优解合成原来解问题的一个解。

 

  实现该算法的过程:

 

  从问题的某一初始解出发;

 

  while 能朝给定总目标前进一步 do

 

  求出可行解的一个解元素;

 

  由所有解元素组合成问题的一个可行解;

 

 

贪心算法与动态规划

贪心法的基本思路:   
    
  从问题的某一个初始解出发逐步逼近给定的目标,以尽可能快的地求得更好的解。当达到某算法中的某一步不能再继续前进时,算法停止。   
  该算法存在问题:   
  1.   不能保证求得的最后解是最佳的;   
  2.   不能用来求最大或最小解问题;   
  3.   只能求满足某些约束条件的可行解的范围。实现该算法的过程:   
  从问题的某一初始解出发;
   
  while   能朝给定总目标前进一步   do   
  求出可行解的一个解元素;   
  由所有解元素组合成问题的一个可行解 

贪心算法最经典的例子,给钱问题。   
  比如中国的货币,只看元,有1元2元5元10元20、50、100   
    
  如果我要16元,可以拿16个1元,8个2元,但是怎么最少呢?   
  如果用贪心算,就是我每一次拿那张可能拿的最大的。   
  比如16,我第一次拿20拿不起,拿10元,OK,剩下6元,再拿个5元,剩下1元   
  也就是3张   10、5、1。   
    
  每次拿能拿的最大的,就是贪心。   
    
  但是一定注意,贪心得到的并不是最优解,也就是说用贪心不一定是拿的最少的张数   
  贪心只能得到一个比较好的解,而且贪心算法很好想得到。   
  再注意,为什么我们的钱可以用贪心呢?因为我们国家的钱的大小设计,正好可以使得贪心算法算出来的是最优解(一般是个国家的钱币都应该这么设计)。如果设计成别的样子情况就不同了   
  比如某国的钱币分为   1元3元4元   
  如果要拿6元钱   怎么拿?贪心的话   先拿4   再拿两个1     一共3张钱   
  实际最优呢?   两张3元就够了   


求最优解的问题,从根本上说是一种对解空间的遍历。最直接的暴力分析容易得到,最优解的解空间通常都是以指数阶增长,因此暴力穷举都是不可行的。
最优解问题大部分都可以拆分成一个个的子问题,把解空间的遍历视作对子问题树的遍历,则以某种形式对树整个的遍历一遍就可以求出最优解,如上面的分析,这是不可行的。
贪心和动态规划本质上是对子问题树的一种修剪。两种算法要求问题都具有的一个性质就是“子问题最优性”。即,组成最优解的每一个子问题的解,对于这个子问题本身肯定也是最优的。如果以自顶向下的方向看问题树(原问题作根),则,我们每次只需要向下遍历代表最优解的子树就可以保证会得到整体的最优解。形象一点说,可以简单的用一个值(最优值)代表整个子树,而不用去求出这个子树所可能代表的所有值。
动态规划方法代表了这一类问题的一般解法。我们自底向上(从叶子向根)构造子问题的解,对每一个子树的根,求出下面每一个叶子的值,并且以其中的最优值作为自身的值,其它的值舍弃。动态规划的代价就取决于可选择的数目(树的叉数)和子问题的的数目(树的节点数,或者是树的高度?)。
贪心算法是动态规划方法的一个特例。贪心特在,可以证明,每一个子树的根的值不取决于下面叶子的值,而只取决于当前问题的状况。换句话说,不需要知道一个节点所有子树的情况,就可以求出这个节点的值。通常这个值都是对于当前的问题情况下,显而易见的“最优”情况。因此用“贪心”来描述这个算法的本质。由于贪心算法的这个特性,它对解空间树的遍历不需要自底向上,而只需要自根开始,选择最优的路,一直走到底就可以了。这样,与动态规划相比,它的代价只取决于子问题的数目,而选择数目总为1。

 

贪心算法