一.贪心算法的基本概念 算法
当一个问题具备最优子结构性质时,咱们会想到用动态规划法去解它。但有时会有更简单有效的算法。咱们来看一个找硬币的例子。假设有四种硬币,它们的面值分别为二角五分、一角、五分和一分。如今要找给某顾客六角三分钱。这时,咱们会不假思索地拿出2个二角五分的硬币,1个一角的硬币和3个一分的硬币交给顾客。这种找硬币方法与其余的找法相比,所拿出的硬币个数是最少的。这里,咱们下意识地使用了这样的找硬币算法:首先选出一个面值不超过六角三分的最大硬币,即二角五分;而后从六角三分中减去二角五分,剩下三角八分;再选出一个面值不超过三角八分的最大硬币,即又一个二角五分,如此一直作下去。这个找硬币的方法实际上就是贪心算法。顾名思义,贪心算法老是做出在当前看来是最好的选择。也就是说贪心算法并不从总体最优上加以考虑,它所做出的选择只是在某种意义上的局部最优选择。固然,咱们但愿贪心算法获得的最终结果也是总体最优的。上面所说的找硬币算法获得的结果就是一个总体最优解。找硬币问题自己具备最优子结构性质,它能够用动态规划算法来解。但咱们看到,用贪心算法更简单,更直接且解题效率更高。这利用了问题自己的一些特性。例如,上述找硬币的算法利用了硬币面值的特殊性。若是硬币的面值改成一分、五分和一角一分3种,而要找给顾客的是一角五分钱。还用贪心算法,咱们将找给顾客1个一角一分的硬币和4个一分的硬币。然而3个五分的硬币显然是最好的找法。虽然贪心算法不是对全部问题都能获得总体最优解,但对范围至关广的许多问题它能产生总体最优解。如图的单源最短路径问题,最小生成树问题等。在一些状况下,即便贪心算法不能获得总体最优解,但其最终结果倒是最优解的很好的近似解。数组
活动安排问题是能够用贪心算法有效求解的一个很好的例子。该问题要求高效地安排一系列争用某一公共资源的活动。贪心算法提供了一个简单、漂亮的方法使得尽量多的活动能兼容地使用公共资源。优化
设有n个活动的集合e={1,2,…,n},其中每一个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。每一个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si<fi。若是选择了活动i,则它在半开时间区间[si,fi]内占用资源。若区间[si,fi]与区间[sj,fj]不相交,则称活动i与活动j是相容的。也就是说,当si≥fi或sj≥fj时,活动i与活动j相容。活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合。spa
在下面所给出的解活动安排问题的贪心算法gpeedyselector中,各活动的起始时间和结束时间存储于数组s和f{中且按结束时间的非减序:.f1≤f2≤…≤fn排列。若是所给出的活动未按此序排列,咱们能够用o(nlogn)的时间将它重排。.net
template< class type>code
void greedyselector(int n, type s[ 1, type f[ ], bool a[ ] ]排序
{ a[ 1 ] = true;ci
int j = 1;资源
for (int i=2;i< =n;i+ + ) {
if (s[i]>=f[j]) {
a[i] = true;
j=i;
}
else a[i]= false;
}
}
算法greedyselector中用集合a来存储所选择的活动。活动i在集合a中,当且仅当a[i]的值为true。变量j用以记录最近一次加入到a中的活动。因为输入的活动是按其结束时间的非减序排列的,fj老是当前集合a中全部活动的最大结束时间,即:
贪心算法greedyselector一开始选择活动1,并将j初始化为1。而后依次检查活动i是否与当前已选择的全部活动相容。若相容则将活动i加人到已选择活动的集合a中,不然不选择活动i,而继续检查下一活动与集合a中活动的相容性。因为fi
老是当前集合a中全部活动的最大结束时间,故活动i与当前集合a中全部活动相容的充分且必要的条件是其开始时间s 不早于最近加入集合a中的活动j的结束时间fj,si≥fj。若活动i与之相容,则i成为最近加人集合a中的活动,于是取代活动j的位置。因为输人的活动是以其完成时间的非减序排列的,因此算法greedyselector每次老是选择具备最先完成时间的相容活动加入集合a中。直观上按这种方法选择相容活动就为未安排活动留下尽量多的时间。也就是说,该算法的贪心选择的意义是使剩余的可安排时间段极大化,以便安排尽量多的相容活动。算法greedyselector的效率极高。当输人的活动已按结束时间的非减序排列,算法只需g(n)的时间来安排n个活动,使最多的活动能相容地使用公共资源。
例:设待安排的11个活动的开始时间和结束时间按结束时间的非减序排列以下:
i |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
s[i] |
1 |
3 |
|
5 |
3 |
5 |
6 |
8 |
8 |
2 |
12 |
f[i] |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
算法greedyselector的计算过程如图所示。
图中每行相应于算法的一次迭代。阴影长条表示的活动是已选人集合a中的活动,而空白长条表示的活动是当前正在检查其相容性的活动。若被检查的活动i的开始时间si小于最近选择的活动了的结束时间fj,则不选择活动i,不然选择活动i加入集合a中。
贪心算法并不总能求得问题的总体最优解。但对于活动安排问题,贪心算法greedyse—1ector却总能求得的总体最优解,即它最终所肯定的相容活动集合a的规模最大。咱们能够用数学概括法来证实这个结论。
事实上,设e={1,2,…,n}为所给的活动集合。因为正中活动按结束时间的非减序排列,故活动1具备最先的完成时间。首先咱们要证实活动安排问题有一个最优解以贪心选择开始,即该最优解中包含活动1。设 是所给的活动安排问题的一个最优解,且a中活动也按结束时间非减序排列,a中的第一个活动是活动k。若k=1,则a就是一个以贪心选择开始的最优解。若k>1,则咱们设 。因为f1≤fk,且a中活动是互为相容的,故b中的活动也是互为相容的。又因为b中活动个数与a中活动个数相同,且a是最优的,故b也是最优的。也就是说b是一个以贪心选择活动1开始的最优活动安排。所以,咱们证实了总存在一个以贪心选择开始的最优活动安排方案。
进一步,在做了贪心选择,即选择了活动1后,原问题就简化为对e中全部与活动1相容的活动进行活动安排的子问题。即若a是原问题的一个最优解,则a’=a—{i}是活动安排问题 的一个最优解。事实上,若是咱们能找到e’的一个解b’,它包含比a’更多的活动,则将活动1加入到b’中将产生e的一个解b,它包含比a更多的活动。这与a的最优性矛盾。所以,每一步所做的贪心选择都将问题简化为一个更小的与原问题具备相同形式的子问题。对贪心选择次数用数学概括法即知,贪心算法greedyselector最终产生原问题的一个最优解。
贪心算法经过一系列的选择来获得一个问题的解。它所做的每个选择都是当前状态下某种意义的最好选择,即贪心选择。但愿经过每次所做的贪心选择致使最终结果是问题的一个最优解。这种启发式的策略并不总能奏效,然而在许多状况下确能达到预期的目的。解活动安排问题的贪心算法就是一个例子。下面咱们着重讨论能够用贪心算法求解的问题的通常特征。
对于一个具体的问题,咱们怎么知道是否可用贪心算法来解此问题,以及可否获得问题的一个最优解呢?这个问题很难给予确定的回答。可是,从许多能够用贪心算法求解的问题中
咱们看到它们通常具备两个重要的性质:贪心选择性质和最优子结构性质。
1.贪心选择性质
所谓贪心选择性质是指所求问题的总体最优解能够经过一系列局部最优的选择,即贪心选择来达到。这是贪心算法可行的第一个基本要素,也是贪心算法与动态规划算法的主要区别。在动态规划算法中,每步所做的选择每每依赖于相关子问题的解。于是只有在解出相关子问题后,才能做出选择。而在贪心算法中,仅在当前状态下做出最好选择,即局部最优选择。而后再去解做出这个选择后产生的相应的子问题。贪心算法所做的贪心选择能够依赖于以往所做过的选择,但决不依赖于未来所做的选择,也不依赖于子问题的解。正是因为这种差异,动态规划算法一般以自底向上的方式解各子问题,而贪心算法则一般以自顶向下的方式进行,以迭代的方式做出相继的贪心选择,每做一次贪心选择就将所求问题简化为一个规模更小的子问题。
对于一个具体问题,要肯定它是否具备贪心选择性质,咱们必须证实每一步所做的贪心选择最终致使问题的一个总体最优解。一般能够用咱们在证实活动安排问题的贪心选择性质时所采用的方法来证实。首先考察问题的一个总体最优解,并证实可修改这个最优解,使其以贪心选择开始。并且做了贪心选择后,原问题简化为一个规模更小的相似子问题。而后,用数学概括法证实,经过每一步做贪心选择,最终可获得问题的一个总体最优解。其中,证实贪心选择后的问题简化为规模更小的相似子问题的关键在于利用该问题的最优子结构性质。
2.最优子结构性质
当一个问题的最优解包含着它的子问题的最优解时,称此问题具备最优子结构性质。问题所具备的这个性质是该问题可用动态规划算法或贪心算法求解的一个关键特征。在活动安排问题中,其最优子结构性质表现为:若a是对于正的活动安排问题包含活动1的一个最优解,则相容活动集合a’=a—{1}是对于e’={i∈e:si≥f1}的活动安排问题的一个最优解。
3.贪心算法与动态规划算法的差别
贪心算法和动态规划算法都要求问题具备最优子结构性质,这是两类算法的一个共同点。可是,对于一个具备最优子结构的问题应该选用贪心算法仍是动态规划算法来求解?是否是能用动态规划算法求解的问题也能用贪心算法来求解?下面咱们来研究两个经典的组合优化问题,并以此来讲明贪心算法与动态规划算法的主要差异。
给定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背包问题。