c++搜索剪枝常见方法与技巧

目录

      

            搜索剪枝常见方法与技巧

关键字  搜索方法,剪枝

摘要

正文

  小结

程序

参考书目


      

            搜索剪枝常见方法与技巧

关键字  搜索方法,剪枝

摘要

搜索是计算机解题中常用的方法,它实质上是枚举法的应用。由于它相当于枚举法,所以其效率是相当地的。因此,为了提高搜索的效率,人们想出了很多剪枝的方法,如分枝定界,启发式搜索等等。在竞赛中,我们不仅要熟练掌握这些方法,而且要因地制宜地运用一些技巧,以提高搜索的效率。

正文

搜索的效率是很低的,即使剪枝再好,也无法弥补其在时间复杂度上的缺陷。因此,在解题中,除非其他任何方法都行不通,才可采用搜索。

既然采用了搜索,剪枝就显得十分的必要,即使就简简单单的设一个槛值,或多加一两条判断,就可对搜索的效率产生惊人的影响。例如N后问题,假如放完皇后再判断,则仅仅只算到7,就开始有停顿,到了8就已经超过了20秒,而如果边放边判断,就算到了10,也没有停顿的感觉。所以,用搜索就一定要剪枝。

剪枝至少有两方面,一是从方法上剪枝,如采用分枝定界,启发式搜索等,适用范围比较广;二是使用一些小技巧,这类方法适用性虽不如第一类,有时甚至只能适用一道题,但也十分有效,并且几乎每道题都存在一些这样那样的剪枝技巧,只是每题有所不同而已。

问题一:(最短编号序列)

表A和表B各含k(k<=20)个元素,元素编号从1到k。两个表中的每个元素都是由0和1组成的字符串。(不是空格)字符串的长度<=20。例如下表的A和B两个表,每个表都含3个元素(k=3)。

    元素编号

     字符串

       1

      1

       2

      10111

       3

      10

表A 表B

    元素编号

     字符串

       1

      111

       2

      10

       3

      0

对于表A和表B,存在一个元素编号的序列2113,分别用表A中的字符串和表B 中的字符串去置换相应的元素编号,可得相同的字符串序列101111110,见下表。

  元素编号序列

    2

    1

    1

    3

  用表A的字符串替换

   10111

    1

    1

    10

  用表B的字符串替换

    10

   111

   111

    0

对表A和表B,具有上述性质的元素编号序列称之为S(AB)。对于上例S(AB)=2113。

编写程序:从文件中读入表A和表B的各个元素,寻找一个长度最短的具有上述性质的元素编号序列S(AB)。(若找不到长度<=100的编号序列,则输出“No Answer”。

对于这道题,因为表A和表B不确定,所以不可能找到一种数学的方法。因为所求的是最优解,而深度优先搜索很容易进入一条死胡同而浪费时间,所以必须采用广度优先搜索的方法。但是,广度优先搜索也有其缺陷。当表A和表B中的元素过多是,扩展的结点也是相当多的,搜索所耗费的时间也无法达到测试的要求。为了解决这个问题,就必须对搜索的算法加以改进。分枝限界似乎不行,因为无法确定代价。而且,由于目标不确定,也无法设定估价函数。但是,因为此题的规则既可以正向使用,又可以逆向使用,于是便可以采用双向搜索。

在大方法确定后,算法的框架就已经基本形成,但即使如此,算法也还有可改进的地方。

  1. 存储当前的A串和B串是很费空间的,但因为A串和B串的大部分相同,故只需记录不同部分,并作个标记。再换成动态存储。
  2. 为了保证两个方向扩展结点的速度相对平衡,可以采取每次扩展结点数较少的方向,而不是两方向轮流扩展。

如此一来,搜索的效率就比单纯的广度优先搜索有了明显的提高。

(附程序sab.pas)

有时,搜索也会有不同的搜索方法(如多处理机调度问题),也会产生不同的效率。

问题二:(任务安排)

N个城市,若干城市间有道路相连,一辆汽车在城市间运送货物,总是从城市1出发,又回到城市1。该车每次需完成若干个任务,每个任务都是要求该车将货物从一个城市运送至另一个。例如若要完成任务2->6,则该车一次旅程中必含有一条子路径。先到2,再到6。

如下图所示,如果要求的任务是2->3,2->4,3->1,2->5,6->4,则一条完成全部任务的路径是1->2->3->1->2->5->6->4->1。

c++搜索剪枝常见方法与技巧_第1张图片

c++搜索剪枝常见方法与技巧_第2张图片

c++搜索剪枝常见方法与技巧_第3张图片

c++搜索剪枝常见方法与技巧_第4张图片

c++搜索剪枝常见方法与技巧_第5张图片

c++搜索剪枝常见方法与技巧_第6张图片

c++搜索剪枝常见方法与技巧_第7张图片

c++搜索剪枝常见方法与技巧_第8张图片

c++搜索剪枝常见方法与技巧_第9张图片

c++搜索剪枝常见方法与技巧_第10张图片

c++搜索剪枝常见方法与技巧_第11张图片

 4

1   2    6

  5

  3 7

    编程由文件读入道路分布的领接矩阵,然后对要求完成的若干任务,寻找一条旅行路线,使得在完成任务最多的前提下,经过的城市总次数最少。如上例中经过城市总次数为8,城市1和2各经过2次均以2次计(起点不计),N<60。

这道题,因为很难找到数学规律,便只有采用搜索的方法。

首先,第一感觉便是:从城市i出发,便搜索所有相邻的城市,再根据当前所处的城市,确定任务的完成情况,从中找到最优解。这种搜索的效率是极低的,其最大原因就在于:目标不明确。

根据题意,我们只需到达需上货和下货的城市,其它的城市仅作为中间过程,而不应作为目标。因此,首先必须确定可能和不可能完成的任务,然后求出任意两城市间的最短路径。在搜索时,就只需考虑有货要上的城市,或者是要运到该城市的货全在车上,其它不须考虑。同时,还可以设定两个简单的槛值。如果当前费用+还需达的城市>=当前最优解,或当前费用+返回城市1的费用>=当前最优解,则不需继续往下搜索。

这种方法与第一感的方法有天壤之别。(附程序travell.pas)

问题三:(多处理机调度问题)

设定有若干台相同的处理机P1,P2      Pn,和m个独立的作业J1,J2      jm,处理机以互不相关的方式处理作业,现约定任何作业可以在任何一台处理机上运行,但未完工之前不允许中断作业,作业也不能拆分成更小的作业,已知作业Ji需要处理机处理的时间为Ti(i=1,2      m)。编程完成以下两个任务:

任务一:假设有n台处理机和m个作业及其每一个作业所需处理的时间Ti存放在文件中,求解一个最佳调度方案,使得完成这m个作业的总工时最少并输出最少工时。

任务二:假设给定作业时间表和限定完工时间T于文件中,求解在限定时间T内完成这批作业所需要最少处理机台数和调度方案。

此题有两种搜索方法:

方法一:按顺序搜索每个作业。当搜索一个作业时,将其放在每台处理机搜索一次。

方法二:按顺序搜索每台处理机。当搜索一台处理机时,将每个作业放在上面搜索一次。

对比上述两种方法,可以发现:方法二较方法一更容易剪枝。

下面是两中方法剪枝的对照:

对于方法一:只能根据目前已确定的需时最长的处理机的耗时与目前最佳解比较。

对于方法二:可约定Time[1]>Time[2]>Time[3]>       >Time[n](Time[i]表示第i台处理机的处理时间),从而可以设定槛值:如当前处理机的处理时间>=目前最佳解,或剩下的处理机台数×上一台处理机的处理时间<剩余的作业需要的处理时间,则回溯。因为在前面的约束条件下,已经不可能有解。

    因此,从上面的比较来看,第二种方法显然是比第一种要好的。下面就针对第二种方法更深一层的进行探讨。

   对于任务一,首先可以用贪心求出Time[1]的上界。然后,还可以Time[1]的下界,UP(作业总时间/处理机台数)。(UP表示大于等于该小数的最小整数)。搜索便从上界开始,找到一个解后,若等于下界即可停止搜索。

(附程序jobs_1.pas)

    对于任务二,可采用深度+可变下界。下界为:UP(作业总时间/限定时间),即至少需要的处理机台数。并设定Time[1]的上界为T。

(附程序jobs_2.pas)

  小结

搜索的使用相当广泛,几乎每题都可以采用搜索的方法。虽然如此,搜索也切不可滥用。只有当问题无规律可寻时,才可用搜索。一旦确定了使用搜索,就一定要想办法对其进行剪枝。无论是采用剪枝的常见方法,还是用一些搜索的小技巧,虽都无法降低搜索的时间复杂度,却总还是大有裨益的。

程序

1. 最短编号序列:sab.pas

program sab;

type aa=string[100];

     ltype=record

                 f:integer;  {父指针}

                 k,d,la,lb:shortint;

   {k--剩余串标志,d--序列中元素的编号,la,lb--A,B两串的串长}

                 st:^aa;  {剩余串}

     end;

const maxn=1300;

var t,h:array[0..1] of integer;  {h--队首指针,t--队尾指针,0表示正向,1表示逆向}

    p:array[0..1,1..maxn] of ltype;  {p[0]--正向搜索表,p[1]--逆向搜索表}

    strs:array[1..2,1..20] of string[20];  {strs[1]--表A元素,strs[2]--表B元素}

    n:integer;   {表A和表B的元素个数}

procedure readp;  {读入数据}

var f:text;

    st:string;

    i,j:integer;

begin

     write('File name:');

     readln(st);

     assign(f,st);

     reset(f);

     readln(f,n);

     for i:=1 to n do

         readln(f,strs[1,i]);

     for i:=1 to n do

         readln(f,strs[2,i]);

     close(f);

end;

procedure print(q,k:integer);  {从k出发,输出沿q方向搜索的元素编号}

begin

     if k<>1 then begin

        if q=1 then

           writeln(p[q,k].d);

        print(q,p[q,k].f);

        if q=0 then

           writeln(p[q,k].d);

     end;

end;

procedure check(q:shortint);  {判断两方向是否重合,q表示刚产生结点的方向的相反方向}

var i:integer;

begin

     for i:=1 to t[1-q]-1 do

         if (p[q,t[q]].k<>p[1-q,i].k) and (p[q,t[q]].st^=p[1-q,i].st^) and

            (p[q,t[q]].la+p[1-q,i].la<=100) and (p[q,t[q]].lb+p[1-q,i].lb<=100)

            then begin

                      if q=0 then

                         begin

                              print(0,t[q]);

                              print(1,i);

                         end

                      else begin

                                print(0,i);

                                print(1,t[q]);

                           end;

                      halt;

            end;

end;

procedure find(q:shortint); {沿q方向扩展一层结点}

var i:integer;

    sa,sb:aa;

begin

     for i:=1 to n do

         if (p[q,h[q]].la+length(strs[1,i])<=100) and

            (p[q,h[q]].lb+length(strs[2,i])<=100) then

            begin

                 sa:='';sb:='';

                 if p[q,h[q]].k=1

                    then sa:=p[q,h[q]].st^

                    else sb:=p[q,h[q]].st^;

                 if q=0 then  {沿不同方向将编号为i的元素加到序列中}

                    begin

                         sa:=sa+strs[1,i];

                         sb:=sb+strs[2,i];

                         while (sa<>'') and (sb<>'') and (sa[1]=sb[1]) do

                               begin

                                    delete(sa,1,1);

                                    delete(sb,1,1);

                               end

                    end

                 else begin

                           sa:=strs[1,i]+sa;sb:=strs[2,i]+sb;

                           while (sa<>'') and (sb<>'') and

                                 (sa[length(sa)]=sb[length(sb)]) do

                                 begin

                                      delete(sa,length(sa),1);

                                      delete(sb,length(sb),1);

                                 end;

                      end;

                 if (sa='') or (sb='') then  {生成一个新的结点}

                    with p[q,t[q]] do

                         begin

                              f:=h[q];d:=i;

                              la:=p[q,h[q]].la+length(strs[1,i]);

                              lb:=p[q,h[q]].lb+length(strs[2,i]);

                              new(st);

                              if sa='' then

                                 begin

                                      k:=2;st^:=sb

                                 end

                              else begin

                                        k:=1;st^:=sa;

                                   end;

                              check(q);

                              inc(t[q]);

                         end;

            end;

     inc(h[q]);

end;

begin

     readp;

     h[0]:=1;h[1]:=1;

     t[0]:=2;t[1]:=2;

     new(p[0,1].st);p[0,1].st^:='';

     new(p[1,1].st);p[1,1].st^:='';

     {队列初始化}

     while (h[0]

           if t[0]

              then find(0)

              else find(1);

     writeln('No answer!');

end.

  1. 任务安排:travell.pas

program travell;

var path,  {path[i,j]--以i为起点第j个运输终点}

    next,  {next[i,j]--从i到j的最短路径中,i顶点的下一个顶点}

    dist,  {dist[i,j]--从i到j的最短路径长度}

    road:array[1..60,1..60] of integer;  {道路的邻接矩阵}

    head,  {head[i]--以i为起点的任务数}

tail,  {tail[i]--0表示以i为终点无任务或已完成}

   {1表示以i为终点的任务的所有顶点都在完成任务路径中}

   {k+1表示以i为终点的所有任务,还有k个顶点未到达}

    arrive:array[1..60] of integer;  {arrive[i]--顶点i的经过次数}

    d,  {完成任务路径}

    bestd:array[1..100] of integer;  {当前最佳完成任务路径}

    left,  {必经结点个数}

    cost,  {当前代价}

    mincost,  {最佳完成任务代价}

    s,  {经过顶点数}

    bests,  {最佳完成任务所经过的顶点数}

    m,  {任务数}

    n:integer;  {城市数}

procedure findshortest;  {求任意两点间的最短路径}

var i,j,k:integer;

begin

     for i:=1 to n do

         for j:=1 to n do

             if road[i,j]=1 then

                begin

                     dist[i,j]:=1;

                     next[i,j]:=j

                end

             else dist[i,j]:=100;

     for k:=1 to n do

         for i:=1 to n do

             for j:=1 to n do

                 if dist[i,k]+dist[k,j]

                    begin

                         dist[i,j]:=dist[i,k]+dist[k,j];

                         next[i,j]:=next[i,k];

                    end;

end;

procedure init;  {读入数据并初始化数据}

var i,j,k:integer;

    st:string;

    f:text;

begin

     write('File name:');

     readln(st);

     assign(f,st);

     reset(f);

     readln(f,n);

     for i:=1 to n do

         for j:=1 to n do

             read(f,road[i,j]);

     findshortest;

     readln(f,m);

     for i:=1 to m do

         begin

              read(f,j,k);

              if (dist[1,j]<100) and (dist[1,k]<100) then

                 begin

                      inc(head[j]);

                      inc(tail[k]);

                      path[j,head[j]]:=k;

                 end;

         end;

     close(f);

     for i:=1 to m do

         if tail[i]>0 then

            inc(tail[i]);

     for i:=1 to head[1] do

         dec(tail[path[1,i]]);

     head[1]:=0;inc(s);d[s]:=1;left:=0;

     cost:=0;mincost:=maxint;

     for i:=2 to n do

         if (head[i]>0) or (tail[i]>0) then

            inc(left);

end;

procedure try;  {搜索过程}

var i,j,k:integer;

    p:boolean;

begin

     if (cost+left>=mincost) or (cost+dist[1,d[s]]>=mincost) then exit;

     if left=0 then  {是否完成了所有任务}

        begin

             mincost:=cost+dist[1,d[s]];

             bestd:=d;

             bests:=s;

             inc(bests);bestd[bests]:=1;

             exit;

        end;

     for i:=2 to n do

         if (head[i]>0) or (tail[i]=1) then  {如果去i顶点有必要}

            begin

                 inc(cost,dist[d[s],i]);

                 inc(arrive[i]);

                 inc(s);

                 d[s]:=i;

                 if arrive[i]=1 then  

{如果i顶点第一次到达,则所有以i为起点的任务的终点tail值减1}

                    for j:=1 to head[i] do

                        dec(tail[path[i,j]]);

                 k:=head[i];

                 head[i]:=0;

                 if tail[i]=1 then  

 {如果完成了以i为终点的所有任务,该点则不需再经过}

                    begin

                         p:=true;

                         dec(tail[i]);

                    end

                    else p:=false;

                 if tail[i]=0 then  

                    dec(left);

                 try;

   {恢复递归前的数据}

                 if tail[i]=0 then

                    inc(left);

                 if true then

                    inc(tail[i]);

                 head[i]:=k;

                 if arrive[i]=1 then

                    for j:=1 to head[i] do

                        inc(tail[path[i,j]]);

                 dec(s);

                 dec(arrive[i]);

                 dec(cost,dist[d[s],i]);

         end;

end;

procedure show(i,j:integer);  {输出从i到j的最短路径}  

begin

     while i<>j do begin

           write('-->',next[i,j]);i:=next[i,j];

     end;

end;

procedure print;  {输出最佳任务安排方案}

var i:integer;

begin

     write(1);

     for i:=1 to bests-1 do

         show(bestd[i],bestd[i+1]);

     writeln;

     writeln('Min cost=',mincost);

end;

begin

     init;

     try;

     print;

end.

  1. 多处理机调度问题:

任务一:jobs_1.pas

program jobs_1;

const maxn=100;  {处理机的最多数目}

      maxm=100;  {作业的最多数目}

var

   t:array[1..maxm] of timeint;   {t[i]--处理作业i需要的时间}

   time,        {time[i]--第i台处理机的处理时间}

   l, {l[i]--第i台处理机处理作业的数目}

   l1:array[0..maxn] of timeint; {l1[i]--目前最优解中第i台处理机处理作业的数目}

   a, {a[i,j]--第i台处理机处理的第j个作业耗费的时间}

   a1:array[1..maxn,1..maxm] of integer;

   {a1[i,j]--目前最优解中第i台处理机处理的第j个作业耗费的时间}

   done:array[1..maxm] of boolean; {done[i]--true表示作业i已完成,false表示未完成}

   least, {处理时间的下界}

   i,j,k,n,m,

   min, {目前最优解的处理时间}

   rest:integer; {剩余作业的总时间}

procedure print;  {输出最优解}

var i,j:integer;

begin

     for i:=1 to n do

         begin

              write(i,':');

              for j:=1 to l1[i] do

                  write(a1[i,j]:4);

              writeln;

         end;

     writeln('T0=',time[0]+1);

end;

procedure readp;  {读入数据}

var

   f:text;

   st:string;

   i,j,k:integer;

begin

     write('File name:');readln(st);

     assign(f,st);reset(f);

     readln(f,n,m);

     for i:=1 to m do

  begin

          read(f,t[i]);inc(rest,t[i]);

         end;

     close(f);

     least:=(rest-1) div n+1;  {定下界}

     for i:=1 to m-1 do  {排序}

         for j:=i+1 to m do

             if t[j]>t[i] then

                begin k:=t[i];t[i]:=t[j];t[j]:=k end;

end;

procedure try(p,q:integer);  {从p--m中选取作业放到处理机q上}

var j:integer;

    z:boolean;

begin

     

     z:=true;

     for j:=p to m do

         if not done[j] and (time[q]+t[j]<=time[q-1]) then  {选择合适的作业}

            begin

                 z:=false;done[j]:=true;

                 inc(l[q]);a[q,l[q]]:=t[j];inc(time[q],t[j]);dec(rest,t[j]);

                 try(j+1,q);

                 dec(l[q]);dec(time[q],t[j]);inc(rest,t[j]);done[j]:=false;

   if time[1]>time[0] then exit;

   {找到解后退回处理机1,需更新time[1],使之减小到time[0]}

   {2--n台处理机就不需再搜索}

            end;

     if z and ((n-q)*time[q]>=rest) then  {如果处理机q已无法放任何作业}

        if rest=0 then  {找到一组解}

           begin

                a1:=a;l1:=l;

                time[0]:=time[1]-1;

                if time[1]=least then

                   begin print;halt end;

           end

        else if q

                try(1,q+1);

end;

begin

     readp;

     fillchar(time,sizeof(time),0);

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

     fillchar(l1,sizeof(l1),0);

     fillchar(l,sizeof(l),0);

     for i:=1 to m do  {贪心求上界}

         begin

              k:=1;

              for j:=2 to n do

                  if time[j]

              time[k]:=time[k]+t[i];

              l1[k]:=l1[k]+1;

              a1[k,l1[k]]:=t[i];

         end;

     min:=time[1];time[0]:=min-1;

     for i:=2 to n do

         if time[i]>min then min:=time[i];

     if min=least then  {如上下界相等}

        begin

             print;

             halt;

        end;

     fillchar(time,sizeof(time),0);

     time[0]:=min-1;  {将上界减1,以找到更优的解}

     try(1,1);

     print;

end.

 任务二:jobs_2.pas

program jobs_2;

const maxn=100;  {处理机的最多数目}

      maxm=100;  {作业的最多数目}

var

   t:array[1..maxm] of timeint;   {t[i]--处理作业i需要的时间}

   time,        {time[i]--第i台处理机的处理时间}

   l, {l[i]--第i台处理机处理作业的数目}

   l1:array[0..maxn] of timeint; {l1[i]--目前最优解中第i台处理机处理作业的数目}

   a, {a[i,j]--第i台处理机处理的第j个作业耗费的时间}

   a1:array[1..maxn,1..maxm] of integer;

   {a1[i,j]--目前最优解中第i台处理机处理的第j个作业耗费的时间}

   done:array[1..maxm] of boolean; {done[i]--true表示作业i已完成,false表示未完成}

   least, {处理时间的下界}

   i,j,k,tmax,m,

   min, {目前最优解的处理时间}

   rest:integer; {剩余作业的总时间}

procedure print;  {输出最优解}

var i,j:integer;

begin

     for i:=1 to least do

         begin

              write(i,':');

              for j:=1 to l1[i] do

                  write(a1[i,j]:4);

              writeln;

         end;

     writeln('Min=',least);

end;

procedure readp;  {读入数据}

var

   f:text;

   st:string;

   i,j,k:integer;

begin

     write('File name:');readln(st);

     assign(f,st);reset(f);

     readln(f,tmax,m);

     for i:=1 to m do begin

         read(f,t[i]);inc(rest,t[i]);

     end;

     close(f);

     least:=(rest-1) div tmax+1;  {确定下界}

     for i:=1 to m-1 do  {排序}

         for j:=i+1 to m do

             if t[j]>t[i] then

                begin k:=t[i];t[i]:=t[j];t[j]:=k end;

end;

procedure try(p,q:integer); {从p--m中选取作业放到处理机q上}

var j:integer;

    z:boolean;

begin

     z:=true;

     for j:=p to m do

         if not done[j] and (time[q]+t[j]<=time[q-1]) then  {找到合适的作业}

            begin

                 z:=false;done[j]:=true;

                 inc(l[q]);a[q,l[q]]:=t[j];inc(time[q],t[j]);dec(rest,t[j]);

                 try(j+1,q);

                 dec(l[q]);dec(time[q],t[j]);inc(rest,t[j]);done[j]:=false;

            end;

     if z and ((least-q)*time[q]>=rest) then  {如果处理机q已无法放任何作业}

        if rest=0 then  {找到最优解}

           begin

                a1:=a;l1:=l;

              print;halt

           end

        else if q

                try(1,q+1);

end;

begin

     readp;

     for i:=1 to m do  {判断无解情况,即某一任务所需时间超过规定时间}

         if t[i]>tmax then

            begin writeln('No answer!');exit end;

     repeat

           fillchar(time,sizeof(time),0);

           fillchar(l,sizeof(l),0);

           fillchar(l1,sizeof(l1),0);

           for i:=1 to m do  {贪心求上界}

               begin

                    k:=1;

                    for j:=2 to least do

                        if time[j]

                    time[k]:=time[k]+t[i];

                    l1[k]:=l1[k]+1;

                    a1[k,l1[k]]:=t[i];

               end;

           min:=time[1];

           for i:=2 to least do

               if time[i]>min then min:=time[i];

           if min=least then  {如果贪心得出了最优解}

              begin

                   print;halt;

              end;

           fillchar(time,sizeof(time),0);

           time[0]:=tmax;

           try(1,1);

           inc(least);  {下界加一}

     until least>m;

     print;

end.

参考书目

  1. 《国际国内青少年信息学(计算机)奥林匹克竞赛试题解析(1994-1995》,吴文虎,王建德主编,清华大学出版社。
  2. 《奥林匹克计算机(信息学)入门》,江文哉主编,上海交通大学出版社。

你可能感兴趣的:(c++)