为了设计的方便,我们把这两种情况统一看作是将a[I]-v张牌从第I堆移动到第I+1堆;移动后有:
a[I]:
=v;a[I+1]:
=a[I+1]+a[I]-v;
在从第i+1堆中取出纸牌补充第i堆的过程中,可能会出现第i+1堆的纸牌数小于零(a[i+1]+a[i]-v<0)的情况。
如n=3,三堆纸牌数为(1,2,27)这时v=10,为了使第一堆数为10,要从第二堆移9张纸牌到第一堆,而第二堆只有2张纸牌可移,这是不是意味着刚才使用的贪心法是错误的呢?
我们继续按规则分析移牌过程,从第二堆移出9张到第一堆后,第一堆有10张纸牌,第二堆剩下-7张纸牌,再从第三堆移动17张到第二堆,刚好三堆纸牌数都是10,最后结果是对的,从第二堆移出的牌都可以从第三堆得到。
我们在移动过程中,只是改变了移动的顺序,而移动的次数不变,因此此题使用贪心法是可行的。
参考源程序:
programaa;
var
i,n,s:
integer;
v:
longint;
a:
array[1..100]oflongint;
begin
readln(n);
v:
=0;
fori:
=1tondo
begin
read(a[i]);
inc(v,a[i]);
end;
v:
=vdivn;{每堆牌的平均数}
fori:
=1ton-1do
ifa[i]<>v{贪心选择}
thenbegin
inc(s);{移牌步数计数}
a[i+1]:
=a[i+1]+a[i]-v;{使第i堆牌数为v}
end;
writeln(s);
end.
◆利用贪心算法解题,需要解决两个问题:
一、问题是否适合用贪心法求解。
用贪心法解题很方便,但它的适用范围很小,判断一个问题是否适合用贪心法求解,目前还没有一个通用的方法,在信息学竞赛中,需要凭个人的经验来判断何时该使用贪心算法。
二、确定了可以用贪心算法之后,如何选择一个贪心标准,才能保证得到问题的最优解。
在选择贪心标准时,我们要对所选的贪心标准进行验证才能使用,不要被表面上看似正确的贪心标准所迷惑,如下面的列子:
例题3:
(NOIP1998tg)设有n个正整数,将他们连接成一排,组成一个最大的多位整数。
例如:
n=3时,3个整数13,312,343,连成的最大整数为:
34331213
又如:
n=4时,4个整数7,13,4,246连接成的最大整数为7424613
输入:
N个数
输出:
连接成的多位数
算法分析:
此题很容易想到使用贪心法,在考试时有很多同学把整数按从大到小的顺序连接起来,测试题目的例子也都符合,但最后测试的结果却不全对。
按这种贪心标准,我们很容易找到反例:
12,121应该组成12121而非12112,那么是不是相互包含的时候就从小到大呢?
也不一定,如:
12,123就是12312而非12112,这样情况就有很多种了。
是不是此题不能用贪心法呢?
其实此题是可以用贪心法来求解,只是刚才的贪心标准不对,正确的贪心标准是:
先把整数化成字符串,然后再比较a+b和b+a,如果a+b>b+a,就把a排在b的前面,反之则把a排在b的后面。
参考程序:
programbb;
var
s:
array[1..20]ofstring;
t:
string;i,j,k,n:
longint;
begin
readln(n);
fori:
=1tondo
begin
read(k);
str(k,s[i]);
end;
fori:
=1ton-1do
forj:
=i+1tondo
ifs[i]+s[j]
thenbegin{交换}
t:
=s[i];
s[i]:
=s[j];
s[j]:
=t;
end;
fori:
=1tondowrite(s[i]);
end.
◆贪心算法所做出的选择只是在某种意义上的局部最优解,所以得到的结果不一定是最优解。
例题4:
装箱问题
问题描述:
装箱问题可简述如下:
设有编号为0、1、…、n-1的n种物品,体积分别为v0、v1、…、vn-1。
将这n种物品装到容量都为V的若干箱子里。
约定这n种物品的体积均不超过V,即对于0≤i<n,有0<vi≤V。
不同的装箱方案所需要的箱子数目可能不同。
装箱问题要求使装尽这n种物品的箱子数要少。
(n<=100)
分析:
若考察将n种物品的集合分划成n个或小于n个物品的所有子集,最优解就可以找到。
但所有可能划分的总数太大,对比较大的n,找出所有可能的划分要花费的时间是无法承受的。
为此,我们可以对装箱问题采用非常简单的近似算法,即贪心法。
该算法依次将物品放到它第一个能放进去的箱子中,如果前面已经用到的箱子均放置不下此物体,则增加一个箱子,直到所有的物体均被放入。
此种算法虽不能保证找到最优解,但还是能找到非常好的解,不失一般性。
具体做法:
将n件物品按照体积从大到小排好序,放置在数组a中,定义另一个数组b作为箱子,开始时只有一个箱子,然后将物品按次序检查能否存放到已用的箱子中去,如果可以就放入第一个可以放入的箱子中,否则将箱子的数量增加一个,将物体放入,直到所有的物体均被放入就可以得到箱子的数量。
参考程序:
programexample4;
var
a,b:
array[0..100]oflongint;{a数组用来放置物品的体积,b数组用来表示箱子}
i,j,n,v,t,k:
longint;
f:
boolean;
begin
readln(n,v);{读入物品数量及箱子的容积}
fori:
=0ton-1do
begin
read(a[i]);{读入各物品的体积}
b[i]:
=0;{将所有的箱子均置为空}
end;
i:
=1;
repeat{对所有物品的体积从大到小进行冒泡排序}
f:
=true;
forj:
=0ton-i-1do
ifa[j]thenbegin
t:
=a[j];
a[j]:
=a[j+1];
a[j+1]:
=t;
f:
=false;
end;
i:
=i+1;
untilf;
k:
=0;{第一个箱子的下标为0}
fori:
=0ton-1do{将每一个物体依次放入箱子中}
begin
f:
=true;
forj:
=0tokdo{判断前面已经用过的箱子中能不能放下当前的物体}
ifa[i]+b[j]<=v
thenbegin{能放入已有箱子的话就放入}
b[j]:
=b[j]+a[i];
f:
=false;
break;{遇到第一个能放入的箱子并放入之后就不要试后面的箱子了}
end;
iff{如果前面用过的箱子均放不下当前物体,则增加一个箱子放置此物体}
thenbegin
k:
=k+1;
b[k]:
=b[k]+a[i];
end;
end;
write(k+1);{输出箱子的个数}
end.
上述算法能求出需要的箱子数,并能求出各箱子所装物品。
下面的例子说明该算法不一定能找到最优解,设有6种物品,它们的体积分别为:
60、45、35、20、20和20单位体积,箱子的容积为100个单位体积。
按上述算法计算,需三只箱子,各箱子所装物品分别为:
第一只箱子装物品1、3;第二只箱子装物品2、4、5;第三只箱子装物品6。
而最优解为两只箱子,分别装物品1、4、5和2、3、6。
贪心算法所作的选择可以依赖于以往所作过的选择,但决不依赖于将来的选择,也不依赖于子问题的解,因此贪心算法与其它算法相比具有一定的速度优势。
如果一个问题可以同时用几种方法解决,贪心算法应该是最好的选择之一。