搜索是人工智能的基本问题。在程序设计中,许多问题的求解都需要利用到搜索技术,它是利用计算机解题的一个重要手段。
问题的状态可以用图来表示,而问题的求解则往往是从状态图中寻找某个状态,或是寻找从一个状态到另一个状态的路径。这一求解的过程可能并不象解一个一元二次方程那样有现成的方法,它需要逐步探索与总是问题有关的各种状态,这即是搜索。
本章将介绍广度优先搜索和深度优先搜索及其递归程序的实现。
所谓"深度"是对产生问题的状态结点而言的,"深度优先"是一种控制结点扩展的策略,这种策略是优先扩展深度大的结点,把状态向纵深发展。深度优先搜索也叫做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、迷宫问题:在一个迷宫中找出从入口到出口的一条步数最少的通路。