搜索是人工智能的基本问题。在程序设计中,许多问题的求解都需要利用到搜索技术,它是利用计算机解题的一个重要手段。

问题的状态可以用图来表示,而问题的求解则往往是从状态图中寻找某个状态,或是寻找从一个状态到另一个状态的路径。这一求解的过程可能并不象解一个一元二次方程那样有现成的方法,它需要逐步探索与总是问题有关的各种状态,这即是搜索。

本章将介绍广度优先搜索和深度优先搜索及其递归程序的实现。

 

第一节 深度优先搜索

    所谓"深度"是对产生问题的状态结点而言的,"深度优先"是一种控制结点扩展的策略,这种策略是优先扩展深度大的结点,把状态向纵深发展。深度优先搜索也叫做DFS法(Depth First Search)。

例一、设有一个4*4的棋盘,用四个棋子布到格子中,要求满足以下条件:

      (1)任意两个棋子不在同一行和同一列上;

      (2)任意两个棋子不在同一条对角线上。

      试问有多少种棋局,编程把它们全部打印出来。

解:PASCAL程序:

Program lt9_1_1;

uses crt;

const n=4;

var a:array[1..n] of integer;

    total:integer;

 

function pass(x,y:integer):boolean;

  var i,j:integer;

  begin

    pass:=true;

    for i:=1 to x-1 do

      if (a[i]=y) or (abs(i-x)=abs(a[i]-y)) then

         begin pass:=false;exit;end;

  end;

 

procedure print;

  var i,j:integer;

  begin

    inc(total);

    writeln('[',total,']');

    for i:=1 to n do

      begin

        for j:=1 to n do

          if j=a[i] then write('O ')

             else write('* ');

        writeln;

      end;

  end;

 

procedure try(k:integer);

  var i:integer;

  begin

    for i:=1 to n do

      if pass(k,i) then

         begin

           a[k]:=i;

           if k=n then print

              else try(k+1);

           a[k]:=0;

         end;

  end;

 

begin

  clrscr;

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

  total:=0;

  try(1);

end.

    分析:这里要求找出所有满足条件的棋局,因此需要穷举所有可能的布子方案,可以按如下方法递归产生:

    令D为深度,与棋盘的行相对应,初始时D=1;

    Procedure try(d:integer);

    begin

      for i:=1 to 4 do

        if 第i个格子满足条件 then

           begin

             往第d行第i列的格子放入一枚棋子;

             如果d=4则得一方案,打印

             否则试探下一行,即try(d+1);

             恢复第d行第i列的格子递归前的状态;

         end;

    end;

    这种方法是某一行放入棋子后,再试探下一行,将问题向纵深发展;若本行试探完毕则回到上一行换另一种方案。这样必定可穷举完所有可能的状态。从本题可以看出,前面所说的递归回溯法即体现了深度优先搜索的思想。上面对深度优先算法的描述就是回溯法常见的模式。

 

例二、在6*6的方格中,放入24个相同的小球,每格放一个,要求每行每列都有4个小球(不考虑对角线),编程输出所有方案。

解:Pascal程序:

Program lx9_1_2;

uses crt;

const n=6;

var map:array[1..n,1..n] of boolean;

    a:array[1..n] of integer;

    total:longint;

 

procedure print;

  var i,j:integer;

  begin

    inc(total);gotoxy(1,3);

    writeln('[',total,']');

    for i:=1 to n do

      begin

        for j:=1 to n do

          if map[i,j] then write('* ')

             else write('O ');

        writeln;

      end;

  end;

 

procedure try(k:integer);

  var i,j:integer;

  begin

    for i:=1 to n-1 do

      if a[i]<2 then begin

         map[k,i]:=true;

         inc(a[i]);

         for j:=i+1 to n do

           if a[j]<2 then begin

              map[k,j]:=true;

              inc(a[j]);

              if k=n then print

                 else try(k+1);

              map[k,j]:=false;

              dec(a[j]);

           end;

         map[k,i]:=false;

         dec(a[i]);

      end;

  end;

 

begin

  clrscr;

  fillchar(map,sizeof(map),false);

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

  try(1);

end.

分析:本题实际上是例一的变形;

     (1)把枚举每行每列四个小球转化成为每行每列填入2个空格;

     (2)用两重循环实现往一行中放入两个空格;

     (3)用数组B记录搜索过程中每列上空格的个数;

     (4)本题利用深度搜索求解时要注意及时回溯,以提高效率,同时要注意退出递归时全局变量的正确恢复。

 

例三、跳马问题:在半张中国象棋盘上,有一匹马自左下角往右上角跳,今规定只许往右跳,不许往左跳,图(A)给出的就是一种跳行路线。编程计算共有多少种不同的跳行路线,并将路线打印出来。

解:PASCAL程序:

Program lt9_1_2;

uses crt;

const d:array[1..4,1..2] of shortint=((2,1),(1,2),(-1,2),(-2,1));

var a:array[1..10,1..2] of shortint;

    total:integer;

 

function pass(x,y,i:integer):boolean;

  begin

    if (x+d[i,1]<0) or (x+d[i,1]>4) or (y+d[i,2]>8)

       then pass:=false else pass:=true;

  end;

 

procedure print(k:integer);

  var i:integer;

  begin

    inc(total);

    write('[',total,'] : (0,0)');

    for i:=1 to k do

      write('->(',a[i,1],',',a[i,2],')');

    writeln;

  end;

 

procedure try(x,y,k:integer);

  var i:integer;

  begin

    for i:=1 to 4 do

      if pass(x,y,i) then

         begin

           a[k,1]:=x+d[i,1];a[k,2]:=y+d[i,2];

           if (a[k,1]=4) and (a[k,2]=8) then print(k)

              else try(a[k,1],a[k,2],k+1);

         end;

  end;

 

begin

  clrscr;

  total:=0;

  try(0,0,1);

  writeln('Press any key to exit..。');

  repeat until keypressed;

end.

分析:(1)这里可以把深度d定为马跳行的步数,马的位置可以用它所在的行与列表示因此初始时马的位置是(0,0);

     (2)位置在(x,y)上的马可能四种跳行的方向,如图(B),这四种方向,可以按x,y的增量分别记为(2,1),(1,2),(-1,2),(-2,1)

     (3)一种可行的跳法是指落下的位置应在棋盘中。

 

练习一:

1、有一括号列S由N个左括号和N个右括号构成,现定义好括号列如下:

  (1) 若A是好括号列,则(A)也是;

  (2) 若A和B是好括号列,则AB也是好的。

 例如:(()(()))是好的,而(()))(()则不是,现由键盘输入N,求满足条件的所的好括号列,并打印出来。

解:Pacal程序:

Program lx9_1_1;

uses crt;

var n:integer;

    total:longint;

 

procedure try(x,y:integer;s:string);

  var i:integer;

  begin

    if (x=n) and (y=n) then begin

       inc(total);writeln('[',total,'] ',s);

    end

    else begin

      if x<n then try(x+1,y,s+'(');

      if y<x then try(x,y+1,s+')');

    end;

  end;

 

begin

  clrscr;

  write('N=');readln(n);

  total:=0;try(0,0,'');

end.

分析:从好括号列的定义可知,所谓的"好括号列"就是我们在表达式里所说的正确匹配的括号列,其特点是:从任意的一个位置之前的右括号的个数不能超过左括号的个数。由这个特点,可以构造一个产生好括号列的方法:用x,y记录某一状态中左右括号的个数;若左括号的个数小于N(即x<N),则可加入一个左括号;若右括号的个数小于左括号的个数,则可加入一个右括号,如此重复操作,直至产生一个好括号列。

2、排列组合问题:从数码1-9中任选N个不同的数作不重复的排列(或组合),求出所有排列(或组合)的方案及总数。

3、迷宫问题:在一个迷宫中寻找从入口(最左上角的格子)到出口(最右下角的格子)的路径。

4、填数游戏一:以下列方式向5*5的矩阵中填入数字。设数字i(1<=i<=25)己被置于座标位置(x,y),则数字i+1的座标位置应为(z,w),(z,w)可根据下列关系由(x,y)算出。

  (1) (z,w)=(x±3,y)

  (2) (z,w)=(x,y±3)

  (3) (z,w)=(x±2,y±2)

  例如数字1的起始位置座标被定为(2,2),则数字2的可能位置座标是:(5,2),(2,5),或(4,4)。编写一个程序,当数字1被指定于某一起始位置时,列举其它24个数字应在的位置,列举出该条件下的所有可有的方案。

解:同例二类似,只不过方向增量变为(3,0),(-3,0),(0,3),(0,-3),(2,2),(2,-2),(-2,2),(-2,-2)。

Pascal程序:

Program lx9_1_3;

uses crt;

const n=5;

      d:array[1..8,1..2] of shortint=((3,0),(-3,0),(0,3),(0,-3),

                                      (2,2),(2,-2),(-2,2),(-2,-2));

var x0,y0:byte;

    a:array[1..n,1..n] of byte;

    total:longint;

 

procedure print;

  var i,j:integer;

  begin

    inc(total);

    gotoxy(1,3);

    writeln('[',total,']');

    for i:=1 to n do

      begin

        for j:=1 to n do

          write(a[i,j]:3);

        writeln;

      end;

  end;

 

procedure try(x,y,k:byte);

  var i,x1,y1:integer;

  begin

    for i:=1 to 8 do

      begin

        x1:=x+d[i,1];y1:=y+d[i,2];

        if (x1>0) and (y1>0) and (x1<=n)

           and (y1<=n) and (a[x1,y1]=0) then

             begin

               a[x1,y1]:=k;

               if k=n*n then print

                  else try(x1,y1,k+1);

               a[x1,y1]:=0;

             end;

      end;

  end;

 

begin

  clrscr;

  write('x0,y0=');readln(x0,y0);

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

  total:=0;a[x0,y0]:=1;

  try(x0,y0,2);

  writeln('Total=',total);

  writeln('Press any key to exit..。');

  repeat until keypressed;

end.

5、填数游戏二。有一个M*N的矩阵,要求将1至M*N的自然数填入矩阵中,满足下列条件:

  (1)同一行中,右边的数字比左边的数字大;

  (2)同一列中,下面的数字比上面的数字大。

  打印所有的填法,并统计总数。

解:Pascal程序:

$Q-,R-,S-

Program lx9_1_4;

uses crt;

const m=3;n=6;

var a:array[0..m,0..n] of integer;

    used:array[1..m*n] of boolean;

    total:longint;

 

procedure print;

  var i,j:integer;

  begin

    inc(total);gotoxy(1,3);

    writeln('[',total,']');

    for i:=1 to m do

      begin

        for j:=1 to n do

          write(a[i,j]:3);

        writeln;

      end;

  end;

 

procedure try(x,y:integer);

  var i:integer;

  begin

    for i:=x*y to m*n-(m-x+1)*(n-y+1)+1 do

      if not used[i] and (i>a[x-1,y]) and (i>a[x,y-1]) then

         begin

           a[x,y]:=i;used[i]:=true;

           if i=m*n-1 then print

              else begin

                if y=n then try(x+1,1)

                   else try(x,y+1);

              end;

           used[i]:=false;

         end;

  end;

 

begin

  clrscr;

  fillchar(used,sizeof(used),false);

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

  a[1,1]:=1;a[m,n]:=m*n;

  used[1]:=true;used[m*n]:=true;

  try(1,2);

  writeln('Total=',total);

end.

分析:本题可以将放入格子中的数字的个数作为深度,先往格子(1,1)放第一个数,然后依次往格子(1,2),(1,3),...,(m,n-1),(m,n)填数字,每填一个数时应如何判断该数是否满足条件,做到及时回溯,以提高搜索的效率是非常关键的。为此需要认真研究题目的特点。根据题意可以知道:在任何一个K*L的格子里,最左上角的数字必定是最小的,而最右下角的数字必定是最大的,故有:

    (1)格子(1,1)必定是填数1。格子(m,n)必定填数m*n;

    (2)若A是格子(x,y)所要填入数,则有:x*y<=A<=m*n-(m-x+1)*(n-y+1)+1;

6、反幻方:在3*3的方格中填入1至9,使得横,竖,对角上的数字之和都不相等。下图给出的是一例。请编程找出所有可能的方案。

    ┌─┬─┬─┐

    │1 │2 │3 │

    ├─┼─┼─┤

    │4 │5 │8 │

    ├─┼─┼─┤

    │6 │9 │7 │

    └─┴─┴─┘

         图 一

分析:

(1)深度优先搜索。用一个二维数组A来存储这个3*3的矩阵。

(2)用x表示行,y表示列,搜索时应注意判断放入格子(x,y)的数码是否符合要求;

   (a)如果y=3,就计算这一行的数码和,其值存放在A[x,4]中,如果该和己出现过,则回溯;

   (b)如果x=3,则计算这一列的数码和,其值存放在A[4,y]中,并进行判断是否需要回溯;

   (c)如果x=3,y=1还应计算从左下至右上的对角线的数码和;

   (d)如果x=3,y=3还应计算从左上至右下的对角线的数码和。

为了提高搜索速度,可以求出本质不同的解,其余的解可以由这些本质不同的解通过旋转和翻转得到。为了产生非本质解,搜索时做如下规定:

  (a)要求a[1,1]<a[3,1]<a[1,3]<a[3,3];

  (b)要求a[2,1]>a[1,2].

解:略

7、将M*N个0和1填入一个M*N的矩阵中,形成一个数表A,

       │a11  a12 ...  a1n │                 

    A=│a21  a22 ...  a2n │                  

       │....…             │                 

       │am1 am2 ... amn │                 

  数表A中第i行和数的和记为ri(i=1,2,...,m),它们叫做A的行和向量,数表A第j列的数的和记为qj(j=1,2,...,m),它们叫做A的列和向量。现由文件读入数表A的行和列,以及行和向量与列和向量,编程求出满足条件的所的数表A。

分析:本题是将例题一一般化,将若干个1放入一个M*N的方阵中,使得每行和每列上的1的个数满足所给出的要求。

思路:(1)应该容易判断,若r1+r2+...+rn<>q1+q2+...+qm,则问题无解;

     (2)将放入1的个数看做是深度,1的位置记为(x,y),其中x代表行,y代表列,第一个1应从(1,1)开始试探;

     (3)往k行放入一个1时,若前一个1的位置是(k,y),则它的位置应在第k行的y+1列至(m-本行还应放入1的个数+1)这个范围内进行试探;若这一列上己放入1的个数小于qy,则该格子内放入一个1,并记录下来;否则换一个位置试探。

Program lx9_1_6;

uses crt;

const max=20;

var m,n,s1,s2:integer;

    map:array[1..max,1..max] of 0..1;

    a,b,c,d:array[1..max] of integer;

    total:longint;

 

procedure error;

  begin

    writeln('NO ANSWER!');

    writeln('Press any key to exit..。');

    repeat until keypressed;

    halt;

  end;

 

procedure init;

  var f:text;

      fn:string;

      i,j:integer;

  begin

    write('Filename:');readln(fn);

    assign(f,fn);reset(f);

    readln(f,m,n);s1:=0;s2:=0;

    for i:=1 to m do

      begin read(f,a[i]);s1:=s1+a[i];end;

    for i:=1 to n do

      begin read(f,b[i]);s2:=s2+b[i];end;

    close(f);

    if s1<>s2 then error;

    fillchar(map,sizeof(map),0);

    fillchar(c,sizeof(c),0);

    fillchar(d,sizeof(d),0);

  end;

 

procedure print;

  var i,j:integer;

  begin

    inc(total);gotoxy(1,3);

    writeln('[',total,']');

    for i:=1 to m do

      begin

        for j:=1 to n do

          write(map[i,j]:3);

        writeln;

      end;

  end;

 

procedure try(x,y,t:integer);

  var i,j:integer;

  begin

    for i:=y+1 to n-(a[x]-c[x])+1 do

      if (map[x,i]=0) and (d[i]<b[i]) then

         begin

           map[x,i]:=1;inc(c[x]);inc(d[i]);

           if t=s1 then print

              else if (x<=m) then begin

                if c[x]=a[x] then try(x+1,0,t+1)

                   else try(x,i,t+1);

              end;

           map[x,i]:=0;dec(c[x]);dec(d[i]);

         end;

  end;

 

begin

  clrscr;

  init;

  try(1,0,1);

  if total=0 then writeln('NO ANSWER!');

end.

 

第二节 广度优先搜索

    广度优先是另一种控制结点扩展的策略,这种策略优先扩展深度小的结点,把问题的状态向横向发展。广度优先搜索法也叫BFS法(Breadth First Search),进行广度优先搜索时需要利用到队列这一数据结构。

例一、分油问题:假设有3个油瓶,容量分别为10,7,3(斤)。开始时10斤油瓶是满的,另外两个是空的,请用这三个油瓶将10斤油平分成相等的两部分。

解:PASCAL程序:

Program lt9_2_2;

uses crt;

const max=3000;

      v:array[1..3] of byte=(10,7,3);

 

type node=record

       a:array[1..3] of byte;

       ft:integer;

     end;

var o:array[1..max] of node;

    h,t,i,j,k:integer;

 

function is_ans(h:integer):boolean;

  begin

    with o[h] do

      if ord(a[1]=5)+ord(a[2]=5)+ord(a[3]=5)=2

         then is_ans:=true else is_ans:=false;

  end;

 

function new_node(t:integer):boolean;

  var r:integer;

  begin

    r:=t;

    repeat

      dec(r);

      with o[t] do

        if (a[1]=o[r].a[1]) and (a[2]=o[r].a[2]) and (a[3]=o[r].a[3])

           then begin new_node:=false;exit;end;

    until (r=1);

    new_node:=true;

  end;

 

procedure print(r:integer);

  var b:array[1..30] of integer;

      t,l,p:integer;

  begin

    t:=0;

    while r>0 do

      begin

        inc(t);b[t]:=r;

        r:=o[r].ft;

      end;

    gotoxy(1,1);

    writeln('':5,v[1]:5,v[2]:5,v[3]:5);

    writeln('--------------------');

    for l:=t downto 1 do

      begin

        write('[',t-l:2,'] ');

        for p:=1 to 3 do

          write(o[b[l]].a[p]:5);

        writeln;

      end;

    halt;

  end;

 

begin

  clrscr;

  with o[1] do

    begin

      a[1]:=10;a[2]:=0;a[3]:=0;

    end;

  h:=1;t:=2;

  repeat

    if is_ans(h) then print(h);

    for i:=1 to 3 do

      if o[h].a[i]>0 then

        for j:=1 to 3 do

          if (i<>j) and (o[h].a[j]<v[j]) then

             begin

               for k:=1 to 3 do

                 o[t].a[k]:=o[h].a[k];

               with o[t] do

                 begin

                   ft:=h;

                   if o[h].a[i]>(v[j]-a[j]) then

                     begin

                       a[i]:=o[h].a[i]+a[j]-v[j];a[j]:=v[j];

                     end

                   else begin

                     a[j]:=a[j]+o[h].a[i];a[i]:=0;

                   end;

                 end;

               if new_node(t) then inc(t);

             end;

    inc(h);

  until (h>t);

  writeln('NO ANSWER!');

end.

 

例二、中国盒子问题:给定2*N个盒子排成一行,其中有N-1个棋子A和N-1个棋子B,余下是两个连续的空格,

如下图,是N=5的一种布局。                                                                

     ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐                                       

     │A │B │B │A │  │  │A │B │A │B │                                       

     └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘                                        

 移子规则为:任意两个相邻的棋子可移到空格中,且这两个棋子的次序保持不变。               

 目标:全部棋子A移到棋子B的左边。                                                      

 

练习二:

1、跳棋:跳棋的原始状态如下图(A),目标状态如下图(B),其中0代表空格,W代表白子,B代表黑子,跳棋的规则是:(1)任一个棋子可以移到空格中去;(2)任一个棋子可以跳过1个或两个棋子移到空格中去。试编一个程序,用最少的步数将原始状态移成目标状态。

  ┌─┬─┬─┬─┬─┬─┬─┐        ┌─┬─┬─┬─┬─┬─┬─┐

  │  │B │B │B │W │W │W │        │  │W │W │W │B │B │B │

  └─┴─┴─┴─┴─┴─┴─┘        └─┴─┴─┴─┴─┴─┴─┘

            图 A                                   图 B

2、七数码问题:

   在3*3的棋盘上放有7个棋子,编号分别为1到7,余下两个是空格。与空格相邻的一个棋子可以移到空格中,每移动一次算一步,现任给一个初始状态,要求用最少的步数移成下图所示的目标状态。

     ┌─┬─┬─┐

     │1 │2 │3 │

     ├─┼─┼─┤

     │4 │5 │6 │

     ├─┼─┼─┤

     │7 │  │  │

     └─┴─┴─┘

3、N个钱币摆放一排,有的钱币正面朝上(记为1),有的钱币正面朝下(记为0),每次可以任意改变K个钱币的状态,即正反面互换。编一个程序判定能否在有限的步数内使得所有钱币正面朝上,若能,请给出步数最少的方案。

4、下图A所示的是一个棋盘,放有编号为1到5的5个棋子,如果两个格子中没有线分隔,就表示这两个格子是相通的,编一个程序,用最少的步数将图A的状态移成图B所示的状态(一次移动一子,无论多远算一步)。

     ┌─┬─┬─┐                       ┌─┬─┬─┐   

     │           │                                   │   

┌─┼─┼  ┼─┼─┐               ┌─┼─┼  ┼─┼─┐

│5    4   3   2   1 │               │1    2    3   4   5 │

└─┴─┴─┴─┴─┘               └─┴─┴─┴─┴─┘

      图   A                                  图  B5、迷宫问题:在一个迷宫中找出从入口到出口的一条步数最少的通路。

posted on 2011-06-02 14:18  shallyzhang  阅读(200)  评论(0)    收藏  举报