4.1枚举算法

第四章 搜索算法策略

4.1枚举算法

 

枚举法(通常也称穷举法)是指在一个有穷的可能的解的集合中,枚举出集合中的每一个元素,用题目给定的约束条件去判断其是否符合条件,若满足条件,则该元素即为整个问题的解;否则就不是问题的解。
     【枚举算法解题必须满足下列条件】
    ⑴ 可预先确定解元素的个数n,且问题的规模不是很大;
    ⑵ 对于每个解变量A1A2…An的可能值必须为一个连续的值域。

    【枚举算法实现】
    通常使用嵌套的FOR结构循环语句枚举每个变量的取值,在最里层的循环中判断是否满足给定的条件,若满足则输出一个解。

例:求满足表达式A+B=C的所有整数解,其中A,B,C为1~3之间的整数。

分析:本题非常简单,即枚举所有情况,符合表达式即可。算法如下:

for A := 1 to 3 do 

  for B := 1 to 3 do 

    for C := 1 to 3 do 

      if A + B = C then 

        Writeln(A,‘+’,B,‘=’,C);

上例中的解变量有3个:A,B,C。其中

A解变量值的可能取值范围A∈{1,2,3}

B解变量值的可能取值范围B∈{1,2,3}

C解变量值的可能取值范围C∈{1,2,3}

则问题的可能解有27个

(A,B,C)∈{(1,1,1),(1,1,2),(1,1,3),

            (1,2,1),(1,2,2),(1,2,3),

                 ………………………………

              (3,3,1),(3,3,2),(3,3,3)}

在上述可能解集合中,满足题目给定的检验条件的解元素,即为问题的解。

    【枚举算法优化】
    ⑴ 减少枚举的变量。
     在使用枚举法之前,先考虑一下解元素之间的关联,将那些能由已枚举解元素推算出来的变量直接用计算表达式代替。
    ⑵ 减少枚举变量的值域。
    通过各枚举变量间的数学关系定义解元素的值域,在保证不遗漏的情况下越小越好。
    ⑶ 分解约束条件。
    将拆分的约束条件嵌套在相应的循环体间,即尽可能根据约束条件减少变量个数和缩小值域。

1:一根29cm长的尺子,只允许在上面刻七个刻度,要能用它量出1~29cm的各种长度。试问这刻度应该怎么选择?

const n=29; m=1;

var a:array [1..7] of  byte;

    b:array [1..n] of 0..1; i,j:integer;a2,a3,a4,a5,a6,a7:integer;

begin

 fillchar(a,sizeof(a),0);

 a[1]:=m;

 for a2:=2 to n-7 do

  for a3:=a2+1 to n-6 do

   for a4:=a3+1 to n-5 do

    for a5:=a4+1 to n-4 do

     for a6:=a5+1 to n-3 do

      for a7:=a6+1 to n-2 do

       begin

        a[2]:=a2;a[3]:=a3;a[4]:=a4;a[5]:=a5;a[6]:=a6;a[7]:=a7;

for i:=1 to 29 do b[i]:=0;

        for i:=1 to 7 do

         begin

          b[a[i]]:=1; b[n-a[i]]:=1; b[n]:=1;

          for j:=i+1 to 7 do b[abs(a[j]-a[i])]:=1;

         end;

        j:=0;

        for i:=1 to n do j:=j+b[i];

        if j=n then

         for i:=1 to 7 do if i<>7 then write(a[i]:4) else writeln(a[i]:4);

       end;

end.

 

例2: 巧妙填数

   1

   9

  2

   3

   8

  4

   5

   7

  6

    将1~9这九个数字填入九个空格中。每一横行的三个数字组成一个三位数。如果要使第二行的三位数是第一行的两倍, 第三行的三位数是第一行的三倍, 应怎样填数。如图

 

 

 

           

分析:本题目有9个格子,要求填数,如果不考虑问题给出的条件,共有9!=362880种方案,在这些方案中符合问题条件的即为解。因此可以采用枚举法。

但仔细分析问题,显然第一行的数不会超过400,实际上只要确定第一行的数就可以根据条件算出其他两行的数了。这样仅需枚举400次。因此设计参考程序: 

program exam9;

var

   i,j,k,s:integer;

 

function sum(s:integer):integer;

begin

  sum:=s div 100 + s div 10 mod 10 + s mod 10

end;

 

function mul(s:integer):longint;

begin

  mul:=(s div 100) * (s div 10 mod 10) * (s mod 10)

end;

 

begin

   for i:=1 to 3 do

   for j:=1 to 9 do

   if j<>i then for k:=1 to 9 do

   if (k<>j) and (k<>i) then begin

     s := i*100 + j*10 +k;        {求第一行数}

     if 3*s<1000 then

    if (sum(s)+sum(2*s)+sum(3*s)=45) and 

(mul(s)*mul(2*s)*mul(3*s)=362880) then  {满足条件,并数字都由1~9组成}

       begin

          writeln(s);

          writeln(2*s);

          writeln(3*s);

          writeln;

       end;

   end;

end.

例3:有4个学生,上地理课时提出我国四大淡水湖的排序如下。

甲:洞庭湖最大,洪泽湖最小,鄱阳湖第三;

乙:洪泽湖最大,洞庭湖最小,鄱阳湖第二,太湖第三;

丙:红泽湖最小,洞庭湖第三;

丁:鄱阳湖最大,太湖最小,洪泽湖第二,洞庭湖第三;

对于各个湖泊应处的地位,每个人只说对了一个。

根据以上情况,编一个程序,让计算机判断各个湖泊应处在第几位。

分析:

var k,d,h,b,t:integer;

begin

  k:=0;

  for d:=1 to 4 do

    for h:=1 to 4 do

      begin

        if h<> d then for b:=1 to 4 do

          begin

            if (b<>h) and (b<>d) then

              begin

                t:=10-d-h-b;

                  if ord(d=1)+ord(h=4)+ord(b=2)=1 then

                    begin

                      if ord(h=1)+ord(d=4)+ord(b=2)+ord(t=3)=1 then

                        if ord(h=4)+ord(d=3)=1 then

                          if ord(b=1)+ord(t=4)+ord(h=2)+ord(d=3)=1 then

                            begin

                              writeln('dth=',d);

                              writeln('hzh=',h);

                              writeln('byh=',b);

                              writeln('th=',t);

                            end;

                     end

                   else writeln('false');

                end;

              end;

         end;

end.

例4:最佳游览线路 (NOI94)

某旅游区的街道成网格状(如图)。其中东西向的街道都是旅游街,南北向的街道都是林荫道。由于游客众多,旅游街被规定为单行道,游客在旅游街上只能从西向东走,在林阴道上则既可从南向北走,也可以从北向南走。

阿龙想到这个旅游区游玩。他的好友阿福给了他一些建议,用分值表示所有旅游街相邻两个路口之间的街道值得游览的程度,分值时从-100到100的整数,所有林阴道不打分。所有分值不可能全是负分。

例如图是被打过分的某旅游区的街道图:

 
  

阿龙可以从任一个路口开始游览,在任一个路口结束游览。请你写一个程序,帮助阿龙找一条最佳的游览线路,使得这条线路的所有分值总和最大。

输入数据:

输入文件是INPUT.TXT。文件的第一行是两个整数M和N,之间用一个空格符隔开,M表示有多少条旅游街(1≦M≦100),N表示有多少条林阴道(1≦M≦20001)。接下来的M行依次给出了由北向南每条旅游街的分值信息。每行有N-1个整数,依次表示了自西向东旅游街每一小段的分值。同一行相邻两个数之间用一个空格隔开。

输出数据:

输出文件是OUTPUT.TXT。文件只有一行,是一个整数,表示你的程序找到的最佳游览线路的总分值。

输入输出示例:

INPUT.TXT

OUTPUT.TXT

3 6

-50 –47 36 –30 –23

17 –19 –34 –13 –8

-42 –3 –43 34 –45

84

 

分析:

设Lij为第I条旅游街上自西向东第J段的分值(1 ≦ I ≦ M,1 ≦ J ≦ N – 1)。例如样例中L12=-47,L23=-34,L34=34。

我们将网格状的旅游区街道以林荫道为界分为N – 1个段,每一段由M条旅游街的对应成段,即第J段成为{L1J,L2J,……,LMJ}(1≦ J ≦ N – 1)。由于游览方向规定横向自西向东,纵向即可沿林阴道由南向北,亦可由北向南,而横行的街段有分值,纵行无分值,因此最佳游览路现必须具备如下三个特征:

来自若干个连续的段,每一个段中取一个分值;

每一个分值是所在段中最大的;

起点段和终点段任意,但途经段的分值和最大。

设Li为第I个段中的分值最大的段。即Li=Max{L1I,L2I,……,LMI}(1≦I≦N – 1)。例如对于样例数据:

L1=Max(-50,17,-42)=17;

L2=Max(-47,-19,-3)=-3;

L3=Max(36,-34,-43)=36;

L4=Max(-30,-13,34)=34;

L5=Max(-23,-8,-45)=-8;

有了以上的基础,我们便可以通过图示描述解题过程,见下图。

 
  

我们把段设为顶点,所在段的最大分值设为顶点的权,各顶点按自西向东的顺序相连,组成一条游览路线。显然,如果确定西端为起点、东段为终点,则这条游览路线的总分值最大。

 

问题是某些段的最大分值可能是负值,而最优游览路线的起点和终点任意,在这种情况下,上述游览路线就不一定最佳了。因此,我们只能枚举这条游览路线的所有可能的子路线,从中找出一条子路线IàI+1à……àJ(1 ≦ I<J ≦ N – 1),使得经过顶点的权和LI+LI+1+……+LJ最大。

设Best为最佳游览路线的总分值,初始时为0;

Sum为当前游览路线的总分值。

我们可以得到如下算法:

Best := 0; Sum := 0;

for I := 1 to N – 2 do

for J := I + 1 to N – 1 do begin 

  Sum := LI + …… + LJ; 

  if Sum > Best then

    Best := Sum;

end

显然,这个算法的时间复杂度为O(N2)。而N在1~20001之间,时间复杂度比较高。于是,我们必须对这个算法进行优化。

仍然从顶点1开始枚举最优路线。若当前子路线延伸至顶点I时发现总分值Sum≦0,则应放弃当前子路线。因为无论LI+1为何值,当前子路线延伸至顶点I+1后的总分值不会大于Li+1。因此应该从顶点I+1开始重新考虑新的一条子路线。通过这种优化,可以使得算法的时间复杂度降到了O(N)。

优化后的算法描述如下:

Best := 0; Sum := 0;

for I := 1 to N – 1 do

 begin

  Sum := Sum + LI;

  if Sum > Best then

    Best := Sum;

    if Sum < 0 then

      Sum := 0;

end

 

程序描述如下:

 

{$R-,S-,Q-}

{$M 65520,0,655360}

program Noi94;

 const

   MaxN = 20001; {林阴道数的上限}

   Inp = 'input.txt';

   Outp = 'output.txt';

 var

   M, N: Word; {旅游街数和林阴道数}

   Best: Longint; {最佳游览路线的总分值}

   Score: array [1..MaxN] of ShortInt; {描述每个段的最大分值}

 

 procedure Init;

   var

     I, J, K: Integer;

     Buffer: array [1 .. 4096] of Char; {文件缓冲区}

   begin

     Assign(Input, Inp);

     Reset(Input);

     SetTextBuf(Input, Buffer); {开辟文件缓冲区}

     Readln(M, N); {读入旅游街数和林阴道数}

     FillChar(Score,Sizeof(Score),-128); {初始化各段的最大分值}1000 0000

     for I := 1 to M do   {计算1~N –1段的最大分值 }

       for J := 1 to N - 1 do begin

         Read(K);

         if K > Score[J] then Score[J] := K;

       end;

     Close(Input);

   end;

 

 procedure Out;

   begin

     Assign(Output, Outp);

     Rewrite(Output);

     Writeln(Best);

     Close(Output);

   end;

 

 procedure Main;

   var

     I: Integer;

     Sum: Longint; {当前游览路线的总分值}

   begin

     {最佳游览路线的总分值和当前游览路线的总分值初始化}

     Best := 0;

     Sum := 0;

     for I := 1 to N - 1 do begin  {顺序枚举游览路线的总分值}

       Inc(Sum, Score[I]); {统计当前游览路线的总分值}

       if Sum > Best then Best := Sum; {若当前最佳,则记下}

       if Sum < 0 then Sum := 0;         {若总分值<0,则考虑一条新路线}

     end;

   end;

 

 begin

   Init; {输入数据}

   Main; {主过程}

   Out; {输出}

 end.

 

 

 

 

 

 

 

 

 

转载于:https://www.cnblogs.com/lj_cherish/archive/2010/10/11/1848236.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值