枚举法.穷举法

枚举法,常常称之为穷举法,是指从可能的集合中一一枚举各个元素,用题目给定的约束条件判定哪些是无用的,哪些是有用的。能使命题成立者,即为问题的解。
采用枚举算法解题的基本思路:
(1) 确定枚举对象、枚举范围和判定条件;
(2) 一一枚举可能的解,验证是否是问题的解
下面我们就从枚举算法的的优化、枚举对象的选择以及判定条件的确定,这三个方面来探讨如何用枚举法解题。
例1:百钱买百鸡问题:有一个人有一百块钱,打算买一百只鸡。到市场一看,大鸡三块钱一只,小鸡一块钱三只,不大不小的鸡两块钱一只。现在,请你编一程序,帮他计划一下,怎么样买法,才能刚好用一百块钱买一百只鸡?
算法分析:此题很显然是用枚举法,我们以三种鸡的个数为枚举对象(分别设为x,y,z),以三种鸡的总数(x+y+z)和买鸡用去的钱的总数(x*3+y*2+z)为判定条件,穷举各种鸡的个数。
下面是解这个百鸡问题的程序

var x,y,z:integer;
begin
for x:=0 to 100 do
    for y:=0 to 100 do
        for z:=0 to 100 do
            {枚举所有可能的解}
            if (x+y+z=100) and (x*3+y*2+z div3=100) and (z mod3=0) then writeln(‘x=’,x,’y=’,y,’z=’,z);
            {验证可能的解,并输出符合题目要求的解}
end.

上面的条件还有优化的空间,三种鸡的和是固定的,我们只要枚举二种鸡(x,y),第三种鸡就可以根据约束条件求得(z=100-x-y),这样就缩小了枚举范围,请看下面的程序:

var  x,y,z:integer;
begin
for x:=0 to 100 do
    for y:=0 to 100-x do
    begin
        z:=100-x-y;
        if (x*3+y*2+z div3=100) and (z mod3=0) then writeln(‘x=’,x,’y=’,y,’z=’,z);
    end;
end.

未经优化的程序循环了1013 次,时间复杂度为O(n3);优化后的程序只循环了(102*101/2)次 ,时间复杂度为O(n2)。从上面的对比可以看出,对于枚举算法,加强约束条件,缩小枚举的范围,是程序优化的主要考虑方向。
在枚举算法中,枚举对象的选择也是非常重要的,它直接影响着算法的时间复杂度,选择适当的枚举对象可以获得更高的效率。如下例:

例2、将1,2…9共9个数分成三组,

分别组成三个三位数,且使这三个三位数构成1:2:3的比例,试求出所有满足条件的三个三位数.
例如:三个三位数192,384,576满足以上条件.(NOIP1998pj)

算法分析:这是1998年全国分区联赛普及组试题(简称NOIP1998pj,以下同)。此题数据规模不大,可以进行枚举,如果我们不加思地以每一个数位为枚举对象,一位一位地去枚举:
for a:=1 to 9 do
for b:=1 to 9 do
………
for i:=1 to 9 do
这样下去,枚举次数就有99次,如果我们分别设三个数为x,2x,3x,以x为枚举对象,穷举的范围就减少为93,在细节上再进一步优化,枚举范围就更少了。程序如下:

var  t,x:integer;
s,st:string;  c:char;
begin
for x:=123 to 321 do{枚举所有可能的解}
begin
    t:=0;
    str(x,st);{把整数x转化为字符串,存放在st中}
    str(x*2,s);
    st:=st+s;
    str(x*3,s);
    st:=st+s;
    for c:=’1′ to ’9′ do{枚举9个字符,判断是否都在st中}
        if pos(c,st)<>0 then
            inc(t)
        else break;{如果不在st中,则退出循环}
        if t=9 then   writeln(x,’ ‘,x*2,’ ‘,x*3);
    end;
end

在枚举法解题中,判定条件的确定也是很重要的,如果约束条件不对或者不全面,就穷举不出正确的结果, 我们再看看下面的例子。
例3 一元三次方程求解(noip2001tg)

问题描述 有形如:ax3+bx2+cx+d=0 这样的一个一元三次方程。给出该方程中各项的系数(a,b,c,d 均为实数),并约定该方程存在三个不同实根(根的范围在-100至100之间),且根与根之差的绝对值>=1。
要求由小到大依次在同一行输出这三个实根(根与根之间留有空格),并精确到小数点后2位。
提示:记方程f(x)=0,若存在2个数x1和x2,且x1<x2,f(x1)*(x2)<0,则在(x1,x2)之间一定有一个根。
样例
输入:1 -5 -4 20
输出:-2.00 2.00 5.00

算法分析:由题目的提示很符合二分法求解的原理,所以此题可以用二分法。用二分法解题相对于枚举法来说很要复杂很多。此题是否能用枚举法求解呢?再分析一下题目,根的范围在-100到100之间,结果只要保留两位小数,我们不妨将根的值域扩大100倍(-10000<=x<=10000),再以根为枚举对象,枚举范围是-10000到10000,用原方程式进行一一验证,找出方程的解。
有的同学在比赛中是这样做

var  k:integer;
a,b,c,d,x :real;
begin
    read(a,b,c,d);
    for k:=-10000 to 10000 do
    begin
        x:=k/100;
        if a*x*x*x+b*x*x+c*x+d=0 then write(x:0:2,’ ‘);
    end;
end.

用这种方法,很快就可以把程序编出来,再将样例数据代入测试也是对的,等成绩下来才发现这题没有全对,只得了一半的分。
这种解法为什么是错的呢?错在哪里?前面的分析好象也没错啊,难道这题不能用枚举法做吗? 看到这里大家可能有点迷惑了。

在上面的解法中,枚举范围和枚举对象都没有错,而是在验证枚举结果时,判定条件用错了。因为要保留二位小数,所以求出来的解不一定是方程的精确根,再代入ax3+bx2+cx+d中,所得的结果也就不一定等于0,因此用原方程ax3+bx2+cx+d=0作为判断条件是不准确的。

我们换一个角度来思考问题,设f(x)=ax3+bx2+cx+d,若x为方程的根,则根据提示可知,必有f(x-0.005)*(x+0.005)<0,如果我们以此为枚举判定条件,问题就逆刃而解。另外,如果f(x-0.005)=0,哪么就说明x-0.005是方程的根,这时根据四舍5入,方程的根也为x。所以我们用(f(x-0.005)*f(x+0.005)

我们设计一个函数f(x)计算ax3+bx2+cx+d的值,程序如下:

{$N+}
var  k:integer;
a,b,c,d,x:extended;
function f(x:extended):extended;
{计算ax3+bx2+cx+d的值}
begin
    f:=((a*x+b)*x+c)*x+d;
end;
begin
    read(a,b,c,d);
    for k:=-10000 to 10000 do
    begin
        x:=k/100;
        if (f(x-0.005)*f(x+0.005)<0) or (f(x-0.005)=0) then write(x:0:2,’  ‘); {若x两端的函数值异号或x-0.005刚好是方程的根,则确定x为方程的根}
    end;
end.

用枚举法解题的最大的缺点是运算量比较大,解题效率不高,如果枚举范围太大(一般以不超过两百万次为限),在时间上就难以承受。但枚举算法的思路简单,程序编写和调试方便,比赛时也容易想到,在竞赛中,时间是有限的,我们竞赛的最终目标就是求出问题解,因此,如果题目的规模不是很大,在规定的时间与空间限制内能够求出解,那么我们最好是采用枚举法,而不需太在意是否还有更快的算法,这样可以使你有更多的时间去解答其他难题。