贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,但对范围相当广泛的许多问题他能产生整体最优解或者是整体最优解的近似解。
贪婪算法是一种改进了的分级处理方法。其核心是根据题意选取一种量度标准。然后将这多个输入排成这种量度标准所要求的顺序,按这种顺序一次输入一个量。如果这个输入和当前已构成在这种量度意义下的部分最佳解加在一起不能产生一个可行解,则不把此输入加到这部分解中。这种能够得到某种量度意义下最优解的分级处理方法称为贪婪算法。
对于根据题意选取一种量度标准,比如钓鱼问题,选取的量度标准是:每个湖的余量,然后按照大小进行排序,依次选取当前最多的湖。又如喷水装置,选取的量度标准是:半径的大小,按照半径大小依次排序,依次选取半径最大的情况。
然而选取量度标准是个不容易的事情。一般都是先简历一个数学模型,然后再深入分析,是否符合用贪心法。
钓鱼问题:
黑书中的经典题:枚举+贪心
把每钓5分钟鱼称为钓一次鱼。首先枚举John需要走过的池塘的数目X,即从池塘1走到池塘X。减去路上花去的时间T=sum(Ti) i=1...X-1,这样我们可以认为John能从一个池塘"瞬间转移"到另一个池塘,即在任意一个时刻都可以在池塘1到池塘X中任选一个钓一次鱼(很重要)。现在采用贪心策略,每次选择鱼最多的池塘钓一次鱼。对于每个池塘来说,由于在任何时候鱼的数目只和John在该池塘里钓鱼的次数有关,和钓鱼的总次数无关,所以这个策略是最优的。假设一共允许钓k次鱼,那么每次在N个池塘中选择鱼最多的一个钓。总的时间复杂度为O(kn^2)。(黑书中的解释)
在最后的结果中,第一个最大值所对应的在每个池塘的钓鱼时间为题目要求的情况,因为如果John在后面的池塘钓了鱼,那么前面相应的时间就会减少。最后注意池塘中没有鱼的情况。
结题步骤如下:
1.在枚举了走过的池塘数目下,分别计算(总时间s-花在路上的时间t)/5=times就是钓鱼的次数。
2.首先选取当前池塘鱼数最多的池塘,然后可掉鱼数max+鱼数,然后次数减一,当池塘中的鱼<0时赋值为0,并退出本次枚举,或者次数为0时,退出本次枚举,然后记录鱼数。如果还有次数,但是没有鱼了,把剩余次数加在第一个池塘。
#include <iostream>
using namespace std;
//f[]为每个池塘的鱼数,d[]为每次减少的条数,t[]为路上花费的时间
int n,h,f[30],f1[30],d[30],t[30];
//用来查找当前鱼数最多的池塘
int chazhao(int n)
{
int i,falg=1;
for(i=1;i<=n;i++)
if(f[falg]<f[i])
{
falg=i;
}
return falg;
}
int main()
{
int i,j,max,Max,min,falg,time,times;
int count[30],count1[30];//记录每个湖钓的次数
cout<<"输入总的池塘数"<<endl;
while(scanf("%d",&n)!=EOF&&n)
{
cout<<"输入小时:"<<endl;
scanf("%d",&h);
Max = -1;
memset(count1,0,sizeof(count1));
memset(f,0,sizeof(f));
memset(d,0,sizeof(d));
memset(t,0,sizeof(t)); //数据的输入
cout<<"输入每个池塘的鱼数"<<endl;
for(i=1;i<=n;i++)
{
scanf("%d",&f[i]);
f1[i] = f[i]; //f1用来保存每个鱼塘的鱼数么???
}
cout<<"输入每个池塘的减少的鱼数"<<endl;
for(i=1;i<=n;i++)
scanf("%d",&d[i]);
cout<<"输入每个池塘的行走时间"<<endl;
for(i=1;i<n;i++)
scanf("%d",&t[i]);
for(i=1;i<=n;i++)//依次枚举贪心
{
time = 0;//花在路上的总时间
max = 0;
memset(count,0,sizeof(count));
for(j=1;j<=i;j++)
time+=t[j-1]*5;
times = (h * 60 - time)/5;//可钓鱼的次数
if(times <=0)//如果本次钓鱼次数为0,则结束本次枚举
continue;
while(1)
{
falg = chazhao(i);//找出本次枚举中,每次选择当前鱼数最多的池塘。
if(f[falg] <= 0)//如果最多池塘的鱼数也小于等于0了,则也同样跳出本次枚举。???
break;
max+=f[falg];//这个是在本次枚举中,每选择一次就要把钓到的鱼加起来。
count[falg]++;//记录每个池塘钓鱼的次数
times--;
f[falg]-=d[falg];
if(f[falg]<0)
f[falg] = 0;
if(times == 0)//如果次数等于0了,则跳出本次枚举
break;
}
if(times)//这里是因为,还有可以钓鱼的次数,但是因为池塘没有鱼了。
{
/* min = 1;
for(j = 2;j<=i;j++)
if(count[min] < count[j])
min = j;
count[min]+=times;*/
count[1]+=times;//就把所有的次数都加到
}
if(Max < max)
{
Max = max;
for(j=1;j<=i;j++)
count1[j] = count[j];//把每个池塘钓鱼的次数赋值给count1.
}
for(j=1;j<=n;j++)
f[j] = f1[j];//f1[]数组保存的是最原始的每个池塘的鱼数,这样每次枚举必须用的是初始值。
}
for(i=1;i<n;i++)
printf("%d, ",count1[i]*5);
printf("%d\n",count1[n]*5);
printf("%s%d\n\n","Number of fish expected: ",Max);
}
return 0;
}
喷水装置
关于喷水装置这个题,我当时不懂的地方是,长方形的中心线在哪?嘿嘿是中线的位置。
【解题思路】:由贪心算法,优先使用半径大的喷水装置。
【核心算法】:
sum=count=0; //初始化
cin>>n; //输入喷水装置总数
for(i=0;i<n;i++) cin>>r[i]; //输入
sort(r,r+n); //对半径升序排序
for(i=n-1;i>=0;i--) //找出最少装置数
{
sum+= sqrt(r[i]*r[i]-1); // sum+= sqrt(r[i]*r[i]-1)*2;
// sqrt(r[i]*r[i]-1)*2 :装置覆盖矩形区域的长度
count++; //记录喷水装置数
if(sum>=10) break; // if(sum>=20) break;
}
cout<< count<<endl; //输出最少装置数
潜水比赛
这个题我是栽了,当时还在想,是那一队N个人一起潜水,在水中的时候,轮流使用氧气瓶呢(人可以闭气么),但是人家都说了,潜水时必须使用氧气瓶。所以这个题跟过桥题一样,都是贪心法。
都是依照时间进行排序,然后选取最快的一对走,然后在另一头选取最快的拿回氧气瓶。
一.贪心算法的基本概念
当一个问题具有最优子结构性质时,我们会想到用动态规划法去解它。但有时会有更简单有效的算法。我们来看一个找硬币的例子。假设有四种硬币,它们的面值分别为二角五分、一角、五分和一分。现在要找给某顾客六角三分钱。这时,我们会不假思索地拿出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相容。活动安排问题就是要在所给的活动集合中选出最大的相容活动子集合。
在下面所给出的解活动安排问题的贪心算法gpeedyselector中,各活动的起始时间和结束时间存储于数组s和f{中且按结束时间的非减序:.f1≤f2≤…≤fn排列。如果所给出的活动未按此序排列,我们可以用o(nlogn)的时间将它重排。
template< class type>
void greedyselector(int n, type s[ 1, type f[ ], bool a[ ] ]
{ a[ 1 ] = true;
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个活动的开始时间和结束时间按结束时间的非减序排列如下:
算法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.贪心算法与动态规划算法的差异
贪心算法和动态规划算法都要求问题具有最优子结构性质,这是两类算法的一个共同点。但是,对于一个具有最优子结构的问题应该选用贪心算法还是动态规划算法来求解?是不是能用动态规划算法求解的问题也能用贪心算法来求解?下面我们来研究两个经典的组合优化问题,并以此来说明贪心算法与动态规划算法的主要差别。
五. 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背包问题。