搜索(计算机编程算法pascal)

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

搜索
搜索算法是人工智能中的一种基本方法,利用计算机的高性能来有目的、有方法地穷举一个问题的部分或所有的可能情况,从而求出问题的解的一种方法。

在建立搜索算法时,首先需要关注的问题是以什么作为状态,这些状态之间有什么样的联系。

其实,在这样的思考过程中,我们已经不知不觉地将一个具体的问题抽象成一个图论的模型—树,即搜索算法的使用第一步在于搜索树的建立。

由上图可以知道,这样形成的一棵树叫搜索树。

初始状态对应着根结点,目标状态对应着目标结点。

排在前的结点叫父结点,其后的结点叫子结点,同一层中的结点是兄弟结点,由父结点产生的子结点叫扩展。

完成搜索的过程就是找到一条从根结点到目标结点的路径,找出一个最优的解。

这种搜索算法的实现类似于图或树的遍历,通常可以有两种不同的实现方法:深度优先搜索(DFS)和广度优先搜索(BFS)。

深度优先搜索(DFS)
一、DFS的含义
深度优先搜索(DFS,Depth First Search)是一种常见的搜索方法。

常用来求一解、全部解或者是最优解。

它所遵循的搜索策略是尽可能“深”的搜索,在搜索树的每一层始终先只扩展一个子结点,不断地向纵深前进直到不能再前进(到达叶子结点或受到深度限制)时,才从当前结点返回到父亲结点,沿另一个方向又继续前进,如此反复,直到求得最优解。

这种方法的搜索树是从树根开始,一枝一枝逐渐形成的,即在搜索的过程中产生搜索树。

在深度优先搜索过程中,常用作标记的方法记录访问过的状态,这种处理方法使得深度优先搜索法与回溯没什么区别了,DFS与回溯成为同一算法的两个不同的名称。

在下面搜索树中,搜索顺序如下:
深度优先搜索顺序如下:A→B→D→H→I→E→J→C→F→G
二、DFS算法的程序
深度优先搜索算法可以简单地表述为“能深则深,不能深则回”的策略,具体表现在以下三个方面:
1.搜索策略——能深则深
如果扩展到当前i阶段的一个k方案(树中的结点)符合条件,则扩展(用入栈)表示,并继续按纵深方向向下一个结点扩展(符合递归的思路),表示阶段号的参数i表现为从小到大的变化趋势。

2.控制策略——不能深则回
如果当前要扩展的结点不符合条件,则立即放弃此后的结点扩展(此处的扩展即为搜素,用出栈表示,阶段号i则体现为由大变小这样一种变化),换另一种方案扩展。

回的时候常会遇见两种情况:
1)一种情况是i阶段的所有方案已经试探过,此时只能回到i-1阶段,在i-1阶段换一种方案试探,i-1阶段的搜索方式依次类推;
2)另一种情况是i阶段还有试探方案,则在i阶段换另一种方案,在程序中常用穷举i阶段的方案来实现。

3.数据结构
常用栈来保存搜索过程中的状态和路径,所需空间大小为搜索所需最长路径的长度。

根据以上描述,每个阶段的扩展方式是一样的,常用递归程序描述扩展第i 阶段:
procedure DFS (i:integer); { 对第i阶段进行扩展试探}
begin
for r:=1 to maxr do { 穷举当前阶段所有可能的决策k(方案、结点)} if 子结点符合条件then
begin
扩展子结点(入栈)
if 子结点是目标结点then 输出结果
else 调用DFS (i+1)
栈顶元素出栈{ 回溯}
end;
end;
或者:
procedure DFS (i:integer); { 对第i阶段进行扩展试探}
var k:integer;
begin
if 所有阶段都已经求解then
begin
比较最优解并保存;
回溯;
End
Else
Begin
穷举当前阶段所以可能的方案k(方案、结点)
If k方案可行then
Begin
记录状态;
DFS (i+1) (能深则深)
状态恢复(不能深则回)
End
End;
end;
【注意】
1.DFS常用来求一解、全部解或最优解
2.对于上述算法可以归结为以下几点:
①边界条件②搜索范围③可行性判断④目标状态
三、DFS的应用
对于可以用深度优先搜索算法解决的题目,可以按部就班,运用填充教学同模式教学的思想构建出程序框架。

同时,在解题的实际过程中必须运用同中求异的变式思维,根据不同的题目,对程序框架进行适当调整、优化等,从而改进时间和空间的效率。

【求所有解——程序模式的运用】
【构造子串】生成长度为n的子串,其字符从26个英文字母的前p(p≦26)个字母中选取,使得没有相邻的子序列相等。

例如p=3,n=5时:
abcba 满足条件
ababc 不满足条件
【输入】n p
【输出】所有满足条件的子串及总数
【分析】
状态at:待扩展的字母序号at。

实际上字串s亦参与了递归运算,但是由于该变量的存储量太大,因此我们将s设为全局变量;
边界条件和目标状态:at=n+1;产生一个满足条件的字串
搜索范围(可选方案):'a'≤ch≤chr(ord('a')+p-1):第at位置可填字母
约束条件(可行性判断):当前字符串没有相邻字串相等的情况Var n,p:integer; {字符串长度和可选字母的个数}
total:longint; {满足条件的字串数}
maxchr:char; {可选字母集中的最大字母}
s:string; {满足条件的字串}
procedure solve(at:integer);
var ch:char;
i:integer;
begin
if at=n+1 then {若产生了一个满足条件的字串,则输出} begin
writeln(s);
inc(total); {回溯}
exit;
end;
for ch:='a' to maxchr do {搜索每一个可填字母}
begin
s:=s+ch;
i:=1;
while(i<=at div 2)and(copy(s,length(s)-i+1,i)<>
copy(s,length(s)-2*i+1,i)) do {检查当前字串是否符合条件}
inc(i);
if i>at div 2 then
solve(at+1); {若当前字串符合条件,则递归扩展} delete(s,length(s),1); {恢复填前的字串}
end;
end;
begin
readln(n,p); {输入字串长度和前缀长短}
maxchr:=chr(ord('a')+p-1); {计算可选字母集中的最大字母}
s:=''; {满足条件的字串初始化为空,字符串数为0}
total:=0;
solve(1); {从第一个字母开始递归搜索}
writeln(total);
end.
四、DFS的优化
优化经常从最优性剪枝、可行性剪枝两方面来考虑,当可行性与最优性都不明显时可以采用预处理方式来实现程序的优化。

1.剪枝优化
所谓剪枝,就是通过某种判断条件,避免一些不必要的遍历过程。

形象地说,就是剪去了搜索树中的某些“枝条”。

下图是一个求最短路径扩展的搜索树,描述了剪枝的过程。

当叶子结点D以找到了一个值为30的最短路径,这时在搜索到G(50),H(35),J(30)时,其路径长度已经大于或等于当时最优值,因此再搜索下去毫无意义,其下的结点都可以剪除,此时叫最优剪枝;同理,如果G(50),H(35),J(30)这些结点已不符合约束条件,那么再搜索下去也毫无意思,此时的剪枝叫可行性剪枝。

【售货员的难题】
某乡有n个村庄(1<n<40),有一个售货员,他要到各个村庄去售货,各村庄之间的路程s(0<s<1000)是已知的,且A村到B村与B村到A村的路大多不同。

为了提高效率,他从商店出发到每个村庄一次,然后返回商店所在的村,假设商店所在的村庄为1,他不知道选择什么样的路线才能使所走的路程最短。

请你帮他选择一条最短的路。

【输入】村庄数n和各村之间的路程(均是整数)。

【输出】最短的路程。

【算法分析】
本题属最优化问题,采用试探的深度优先搜索策略。

题目给定的村庄数不多(n<40),所以可以用回溯的方法,当村庄数n比较大时,这种方法就不太适用了。

在中间搜索过程中(不是目标状态),假如已经找到了一组比最小值大的解,那么它的最终结果一定大于先前找到的最小值,但是计算机仍然会义无反顾地去搜索比它更“劣”的其他解。

为了避免出现这种情况,我们需要在搜索的过程中进行最优性剪枝。

程序如下:
var i,j,n,min:integer;
a:array[1..40,1..40]of integer; {储存图}
v:array[1..40]of boolean; {判断该点是否访问过}
procedure dfs(step,line,m:longint); {step步数,line当前扩展到的结点} var i:integer;
begin
if step =n then { 到达终点}
begin
if m+a[line,1]<min then min:=m+a[line,1]
end
else
for i:=2 to n do
if(a[line,i]>0)and v[i] then {当前点未访问}
begin
v[i]:=false;
if m+a[line,i]<min then {最优剪枝}
dfs(step +1,i,m+a[line,i]);
v[i]:=true; {恢复递归前的值}
end;
end;
begin
readln(n);
for i:=1 to n do
for j:=1 to n do
read(a[i,j]);
fillchar(v,sizeof(v),true);
min:=maxint;
v[1]:=false;
dfs(1,1,0);
writeln(min);
end.
【导游】(07年宁波市中小学程序设计决赛题guide.pas)宁波市的中小学生们在镇海中学参加程序设计比赛之余,热情的主办方邀请同学们参观镇海中学内的各处景点,已知镇海中学内共有n处景点。

现在有n位该校的学生志愿承担和讲解任务。

每个学生志愿者对各个景点的熟悉程序是不同的,如何将n为导游分配至n处景点,使得总的熟悉程度最大呢?要求每个景点处都有一个学生导游。

【输入】
输入文件guide.in有若干行;
第一行只有一个正整数n,表示有n个景点和n个学生导游。

第二行至第n+1行共n行,每行有n个以空格分隔的正整数。

第i+1行的第j个数k(1≤k≤1000),表示第i个学生导游对景点j的熟悉程度为k
【输出】
输出文件guide.out 只有一行,该行只有一个正整数,表示求得的熟悉程序之和的最大值。

【样例说明】
第1个学生负责第3个景点,第2个学生负责第1个景点,第
3个学生负责第2个景点时,熟悉程度综合为24,达到最大值。

【数据范围】
50%的数据,1≤n≤9
100%的数据,1≤n≤17
【分析】采用方法:深度优先搜索+调整剪枝。

问题的实质就是在搜索二维数组中的值,要求每行每列只能取一个值,然后求该数值的和,找出其中最大的和。

根据样例,首先建立一棵搜索树。

根据以样例建立的搜索树可以看出,可以采用深度优先搜索遍历搜索树的方法求得该问题的解,但是根据数据限制“100%”的数据1≤n≤17,采用遍历全部的方式肯定会超时。

变通思维方式,题意中最大景点的值是1000,转换二维数组的值,用1000减去该值,变求二维数组中最大的值为求最小值,然后在深度遍历的过程中,比较所获取的和值,和当前已获得的最小值比较,小则递归处理下一个数值,大或者等于则剪枝回溯,直到depth>n
var a:array[1..20,1..20]of longint;
f:array[1..20] of boolean; {用于标识景点、导游是否被分配过}
n,i,j,ans:longint;
procedure dfs(i,s:longint);
var j:longint;
begin
if i>n then { 当完成一次导游搜索组合,得出当前最佳的熟悉程度值} begin
if ans>s then ans:=s; exit;
end;
if s>=ans then exit; { 当前得到的熟悉程度值大于先前最佳熟悉程度值时则剪枝}
for j:=1 to n do { 按导游对各景点的熟悉程度搜索}
if f[j] then { 假如该景点未被分配过导游}
begin
f[j]:=false; {标识景点使用标识}
dfs(i+1,s+a[i,j]); {累加景点熟悉程度值,并进入下一层景点熟悉程度的搜索}
f[j]:=true; {完成以该景点为前提的搜索,恢复现场,为同层下一景点搜索做准备}
end;
end;
begin
readln(n);
for i:=1 to n do
for j:=1 to n do
begin
read(a[i,j]);
a[i,j]:=1000-a[i,j]; {变求最大值为最小值}
end;
fillchar(f,sizeof(f),true);
ans:=maxlongint;
dfs(1,0); {深度搜索调用}
writeln(n*1000-ans); {求得最大熟悉程度的值}
end.
【数的拆分】(snumber.pas)
对于正整数n,输出其和等于n且满足以下限制条件的所有正整数的和式,以及和式的总数。

组成和式的数字自左向右构成一个非递增的序列。

如n=4,程序输出为:
4=4
4=3+1
4=2+2
4=2+1+1
4=1+1+1+1
5 【输入】
输入文件snumber.in仅一行,该行只有一个正整数n(1≤n≤50)
【输出】
输出文件snumber.out 包含若干行,最后一行输出和式的数目,除此之外,前面每一行输出一个和式,组成和式的数字自左向右构成一个非递增的序列,不同行的和式先按照等号右边的第一个数字降序排列,若第一个数字相同,则按第二个数字降序排列,依次类推,直到输出所有和式为止。

【输入输出样例】
【分析】采用方法:深度优先搜索+调整剪枝。

问题的实质就是对输入的n进行拆分。

根据样例建立一棵如下图所示的搜索树。

由样例图可知,每一组数据存在两个数字,前一个数字为依次需要拆分的数字,另一个为下一拆分起点最大数值(该数值在取值过程中需要进行适当的调整,
与先前所拆分的数值进行比较,取两者较小数值为再次拆分对象,即为下一层搜索对象的起始值,目的避免重复拆分数值,进行适度调整减枝),然后对该数值进行递归处理,判断剩余的数值是否为0,是则打印,并回溯至上一层,继续该层中其他数值的拆分;不是则按序穷举当前层的所有取值,并递归深搜下一层剩余数值的拆分。

以上图拆分5=2+2+1和5=2+1+1+1为例,首先在完成前一组数值拆分恢复现场,回溯穷举至2(第一层),得第一个拆分数值2,余下的数值5-2,即为3,剩下数值3与已拆分前一个所得数值比较取较小值(避免5再次拆分成2和3),获得拆分数值穷举范围即2-1。

进入第二层搜索,首先是穷举2,获得第二个拆分数值2,计算剩余数值得1,然后递归调拆分余下的数值1.首先判断剩下的数值是否为0(拆分完毕),不是,该数值与前一个拆分所得的数值2比较取小数值得1,获得该数的数值拆分范围为1-1.进入第三层搜索,即第三个拆分数值1,同时剩下的数值减去该数值得0,递归调用,拆分余下的数值0,在递归调用过程中,发现剩余数值为0,递归调用结束,即打印该拆分数即5=2+2+1,恢复现场至上一层,得剩余数值为1,再次恢复现场至上一层(第二层),得剩余数值为3,穷举下一个拆分值1,据悉完成5=1+1+1+1+1的拆分。

var a:array[0..50]of integer; {用于存放拆分的数值}
left,n:integer; {left用于存放n拆分以后所剩下的值}
total:longint; {left用于存放n拆分以后所剩下的值}
procedure dfs(k:integer);
var i,min:integer;
begin
if left=0 then
begin
write(n,'=',a[1]);
total:=total+1;
for i:=2 to k-1 do
write('+',a[i]);
writeln;
end
else
begin
if left<a[k-1] then min:=left
else min:=a[k-1];
for i:=min downto 1 do
begin
a[k]:=i;
left:=left-i;
dfs(k+1);
left:=left+i;
end;
end;
end;
begin
assign(input,'snumber.in'); reset(input);
assign(output,'snumber.out'); rewrite(output);
readln(n);
a[0]:=n;
left:=n;
total:=0;
dfs(1);
writeln(total);
close(input);
close(output);
end.
【棋盘】(checker.pas)
检查一个如下的6*6的跳棋棋盘,有6个棋子被放置在棋盘上,使得每行、每列只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子,如下图所示:
上面的布局可以用序列2 4 6 1 3 5 来描述,第i个数字表示在第i行的相应位置有一个棋子,如下:
行号 1 2 3 4 5 6
列号 2 4 6 1 3 5
这只是跳棋放置的一个解。

请编程找出所有跳棋放置的解,并以上面的序列方法输出解(按字典顺序排列)。

请输出前3个解,最后一行是解的总个数。

【输入】
输入文件checker.in仅一个数字n(6≤n≤13)表示棋盘是n*n大小的。

【输出】
输出文件checker.out 前三行为前三个解,每个解的两个数字之间用一个空格隔开,第四行只有一个数字,表示解的总数。

【输入输出样例】
【分析】解决方法:深搜+优化
问题的实质就是类似八皇后问题。

首先建立一棵搜索树,如图所示以6*6的跳棋棋盘建立的搜索树,在深度搜索解的过程中最为主要的是检查判断,即检查同一行、同一列、对角线是否有其他棋子存在,为此可以通过建立标志数组b[i]、c[i-k]和d[k+i]。

数组b[i]控制同一列只能有一个棋子存在;c[i-k]控制“\”对角线是否有棋子存在,在同一“\”对角线上其行列坐标之差是相等的;d[k+i]控制“/”对角线是否有棋子存在,在同一“/”对角线上其行列坐标之和是相等的。

这样在放置下一个棋子的时候,判断需要放置棋子位置的所在的列、对角线上是否有棋子,即该棋子的列、对角线标志值是否为初始值“0”,是则可以放置该棋子,然后递归调用,直到depth>n,回溯;反之,枚举同一行中的其他列放置位置。

继续判断,一旦发现该行不存在合适的棋子放置位置时,同样,回溯,修改上一行中棋子放置位置,直到遍历棋盘中的所有的位置。

【优化】可以考虑对称方法,如果n是偶数,第一行枚举前一半的数,这样每搜索出一种方案后,
沿中线对称过来又是一种方案,并且因为每一行的数小于一半,对称过来的方案总大于原方案,即在搜索树中后被搜索到,不会出现重复的情况。

如果n是奇数,现在中间行和中间列放之,并且位置都不超过半数(<n div 2),且中间行大于中间列,这样每搜索出一种方案,把它对称旋转后一共有8中方案,
因为中间行和中间列不出现重复,所有8种方案不重复。

这样只需枚举原来的
1
8即可。

var a:array[-50..50]of longint;
b,c,d:array[-50..50]of 0..1;
n,t:longint;
procedure dfs(k:longint);
var i,j:longint;
end. begin
if k>n then
begin
if t<3 then
begin
write(a[1]);
for j:=2 to n do write(' ',a[j]);
writeln;
end;
t:=t+1;
exit;
end;
for i:=1 to n do
if (b[i]=0)and(c[i-k]=0)and(d[k+i]=0) then
begin
a[k]:=i; b[i]:=1;c[i-k]:=1;d[k+i]:=1;
dfs(k+1);
b[i]:=0; c[i-k]:=0;d[i+k]:=0;
end;
end;
begin
assign(input,'checker.in'); reset(input);
assign(output,'checker.out'); rewrite(output);
readln(n);
fillchar(b,sizeof(b),0);
fillchar(c,sizeof(b),0);
fillchar(d,sizeof(b),0);
dfs(1);
writeln(t);
close(input); close(output);。

相关文档
最新文档