第十二章 搜索法Word格式.docx
《第十二章 搜索法Word格式.docx》由会员分享,可在线阅读,更多相关《第十二章 搜索法Word格式.docx(43页珍藏版)》请在冰豆网上搜索。
forai←ai1toaikdo
foran←an1toankdo
if状态(a1,…,ai,…,an)满足检验条件
then输出问题的解;
由此可见,枚举的次数为
。
枚举法的优点:
⑴由于枚举算法一般是现实生活中问题的“直译”,因此比较直观,易于理解;
⑵由于枚举算法建立在考察大量状态、甚至是穷举所有状态的基础上,所以算法的正确性比较容易证明。
枚举法的缺点:
枚举算法的效率取决于枚举状态的数量以及单个状态枚举的代价,因此效率比较低。
当然,由于模型建立的不同、信息提取量的不同,同一个问题可以有多个枚举算法,效率也可能有所不同,甚至可能有很大的差异。
一、“直译”的枚举算法
将自然语言描述的问题“翻译”成算法过程,即为“直译”。
现实生活中的许多问题可以“直译”成枚举算法。
例如古代著名的“百鸡百钱”(公鸡一只5文钱,母鸡一只3文钱,小鸡3只一文钱。
要求计算一百钱可买3种鸡的数量)问题就是一个典型的枚举问题。
状态:
3种鸡的数量x,y,z
每个状态元素的取值范围:
1≤x≤
-2,1≤y≤
-2,z=100-x-y
约束条件:
5*x+3*y+z/3=100
由此“直译”成如下枚举算法
forx←1to18do
fory←1to31do
begin
z←100-x-y;
if5*x+3*y+z/3=100then输出公鸡x只,母鸡y只,小鸡z只;
end;
{for}
从上例可以看出,能够被“直译”的问题一般具有下列特征
⑴建立在离散模型上;
⑵状态的数量和枚举单个状态的代价确定;
⑶枚举算法的时间复杂度为一个多项式;
下面,我们再来看一个较为复杂的问题是如何“直译”成枚举算法的
【例题12.1.1】跳远
在水平面上整齐的放着n个正三角形,相邻两个三角形的底边之间无空隙,如下图所示。
一个小孩子想站在某个三角形i的顶端,跳到三角形j的顶端上(i<
j)。
他总是朝着斜向45度的方向起跳,且初始水平速度v不超过一个给定值v0。
在跳跃过程中,由于受到重力作用(忽略空气阻力),小孩子将沿着抛物线行进,水平运动方程为x=x0+vt,竖直运动方程为y=y0+vt–0.5gt2,运动轨迹是一条上凸的抛物线。
取g=10.0,(x0,y0)是起跳点坐标。
请编程求出他从每个位置起跳能到达的最远三角形的编号。
注意:
跳跃过程中不许碰到非起点和终点的其他三角形。
输入
第一行为两个正整数n,v0(3≤n≤10,1≤v0≤100),表示三角形的个数和最大水平初速度。
第二行有n个正整数li(1≤li≤20),表示从左到右各个三角形的边长。
输出
输出仅一行,包括n-1个数,表示从三角形1,2,3…n-1的顶点出发能到达的最右的三角形编号。
如果从某三角形出发无法达到任何三角形,相应的数为0。
分析:
本题的基本思想是枚举。
对于每一个起跳点i,依次判断点i+1,i+2…n能否跳到。
起跳点i和i点后的点j
1≤i≤n-1,i+1≤j≤n
约束条件的分析:
判断小孩能否从i点跳到j点的方法如下:
设起点和终点间的水平距离为l、垂直距离为h。
则由物理知识(已在题目中给出)有:
t=l/v
h=vt–5t2=l–5*
因此,v=sqrt(5*l*l/(l-h))。
当然,这个v不一定符合要求,它需要满足两个条件。
⑴它不能大于极限速度v0,即必须有v≤v0
⑵跳跃过程中不得碰到其他三角形。
条件⑵的判断要复杂一些。
好在题目中提示:
轨迹是一条上凸的曲线,因此只要顶端顶点在抛物线下方,则整条线段都在抛物线下方。
这样,我们依次判断起点和终点之间的各个三角形顶点k,看它是否在抛物线下。
如何判断顶点k是否在抛物线下呢?
我们可以算出到达时间t0=dx/v(其中dx为起点到顶点k的水平坐标增量),然后算出该时刻的竖直坐标增量vt0–0.5t02。
如果此增量大于起点到顶点k的竖直坐标增量,则抛物线在上方。
只有起点和终点之间任何一个三角形的顶点不在抛物线下方,则跳远不能完成。
我们在枚举过程中不断将小孩所能跳到的点j调整为best。
枚举结束后,best即为试题要求的最远点。
var
len:
array[1..20]oflongint;
{三角形的边长序列}
x,y:
array[1..20]ofdouble;
{三角形顶端顶点的坐标序列}
l,h,t,v,v0:
double;
ok:
boolean;
{跳跃成功标志}
i,j,k,n,best:
integer;
begin
read(n,v0);
{输入三角形的个数和最大水平初速度}
fori←1tondoread(len[i]);
{输入从左到右各个三角形的边长}
x[1]←len[1]/2;
{计算每一个三角形顶端顶点的坐标}
y[1]←len[1]*sqrt(3)/2;
fori←2tondo
begin←
x[i]←x[i-1]+len[i-1]/2+len[i]/2;
y[i]←len[i]*sqrt(3)/2;
fori←1ton-1do{依次计算每一个三角形所能到达的最远点}
best←0;
{从三角形i出发能到达的最右的三角形编号初始化}
forj←i+1tondo{依次枚举右方的每一个三角形}
l←x[j]-x[i];
{计算三角形i与三角形j的两个顶端顶点的水平距离和垂直距离}
h←y[j]-y[i];
ifl<
hthenbreak;
{若起跳角度超过45度,则无法从三角形i起跳}
v←sqrt(5*l*l/(l-h));
{计算即时速度v}
ifv>
v0thenbreak;
{若大于极限速度v0,则无法从三角形i起跳}
ok←true;
fork←i+1toj-1do{判断跳跃过程中是否碰到其他三角形}
t←(x[k]-x[i])/v;
{计算到达三角形k的时间}
if(v*t-5*t*t)-(y[k]-y[i])<
1e-6then
begin{如果该时刻的竖直坐标增量大于起点到顶点k的竖直坐标增量,则抛物线在上方}
ok←false;
break;
{then}
ifokthenbest←j{若跳远成功,则三角形j为目前三角形i所能到达的最远点,否则跳远不能完成}
elsebreak;
write(best,'
'
);
(输出从三角形i的顶点出发所能到达的最右的三角形编号)
writeln;
end.{main}
二、枚举算法的优化
枚举算法的时间复杂度可以用状态总数*考察单个状态的耗时来表示,因此优化主要是
⑴减少状态总数(即减少枚举变量和枚举变量的值域)
⑵降低单个状态的考察代价
优化过程从几个方面考虑。
具体讲
⑴提取有效信息,即在枚举问题给出的浩瀚信息中,取其对解题有直接帮助的精华,弃其无用的糟粕。
这样做,会对减少状态总数有很大影响;
⑵减少重复计算。
虽然从表面上看,各个状态是独立的,但是不同状态在考察过程中可能有一部分相同的内容。
如果让互相联系的状态共同“分担”考察费用,可以降低时间复杂度;
⑶有的枚举问题的状态是复合型的,由多个子状态组成。
这时可以考虑将原问题化为更小的问题。
表面上看是一个量的变化,但如果充分利用问题的信息,可能对算法产生质的变化。
⑷根据问题的性质进行截枝,特别是最优化枚举问题,往往可以根据问题性质排除一些“显然”
不可能的状态,从而达到减少状态总数的效果;
⑸引进其他算法。
一个可“直译”成枚举算法的问题,其解题思路可以从枚举开始,并不一定非得以枚举结束,很多枚举问题可以通过引进其他高效算法达到解题的目的。
1.
下面,我们来看一个改进的实例
【例题12.1.2】立方体问题
现有一个棱长为n的立方体,可以分成n3个1*1*1的单位立方体。
每个单位立方体都有一个整数值。
n3个单位立方体的数和不会超过longint范围。
现在要求在这个立方体找到一个包含完整单位立方体的长方体,使得该长方体内所有单位立方体的数和最大。
输入:
n(1≤n≤20)
n个n*n的数字矩阵,每个数字矩阵代表一层,每个数字代表一个单位立方体的整数值,-999≤单位立方体的整数值≤999
输出:
长方体的数和
该题是一道枚举题。
我们用长方体中对角线的两个坐标描述状态(x1,y1,z1,x2,y2,z2),其中,该长方体水平面的左上角为(x1,y1),右下角为(x2,y2);
上平面的z轴坐标为z1,下底面的z轴坐标为z2。
显然1≤x1,y1,z1,x2,y2,z2≤n。
“直译”枚举的过程如下
forx1←1tondo{枚举所有可能的平面}
forx2←1tondo
fory1←1tondo
fory2←1tondo
forz1←1tondo{枚举所有可能的上平面和下底面}
forz2←1tondo
考察状态(x1,y1,z1,x2,y2,z2);
考察状态(x1,y1,z1,x2,y2,z2)的任务是计算长方体的体积,并调整最优解。
设map为立方体对应的三维矩阵;
sum为当前长方体的体积;
best为最优解。
考察过程如下
sum←0;
forx←x1tox2do{计算长方体的体积}
fory←y1toy2do
forz←z1toz2dosum←sum+map[x,y,z];
{调整最优解}
ifsum>
bestthenbest←sum;
这个算法相当粗糙,枚举状态的费用为O(n9),每个状态的考察费用为O(n3),使得整个算法的时间复杂度高达O(n9)。
根据题目对n的要求,无法令人满意。
以下进行优化改进
1、从减少重复计算入手
记录先前考察的结果。
在统计长方体2时,只要将长方体1的统计结果加上长方体3就可以了,而不必按上述算法那样重新进行计算。
forx1←1tondo{枚举所有可能的水平面}
forz1←1tondo{枚举上平面的z轴坐标}
{长方体的体积初始化}
forz2←1tondo{枚举下底面的z轴坐标}
考察过程改为
forx←x1tox2do{计算长方体的体积}
fory←y1toy2dosum←sum+map[x,y,z2];
ifsum>
由于利用了计算出的结果,整个算法的时间复杂度降为O(n8)。
2、提取恰当的信息
上述考察实际上求出z轴坐标为z2的平面中矩形(x1,y1,x2,y2)的数和。
我们将这个数和记为value(a)
value(A)=value(ABCD)+value(B)-value(BC)-value(BD)
这就启发我们用另一种方法表示立方体的信息:
设rec[x,y,z]表示z轴坐标为z的水平面中矩形(1,1,x,y)的数和。
z轴坐标为z的水平面中左上角为(x1,y1)、右下角为(x2,y2)的矩阵的数和为rec[x2,y2,z]+rec[x1,y1,z]-rec[x2,y1,z]-rec[x1,y2,z]
Rec数组可以在输入数据的同时计算
fillchar(rec,size(rec),0);
{rec数组初始化}
forz←1tondo{逐层输入信息}
forx←1tondo{逐行输入z平面的信息}
fory←1tondo{逐列输入z平面上x行的信息}
read(map[x,y,z]);
{输入z平面上(x,y)中的数}
if(x=1)and(y=1){计算z平面上以(1,1)为左上角、(x,y)为右下角的矩形的数和}
thenrec[1,1,z]←map[1,1,z]
elseify=1thenrec[x,y,z]←rec[x-1,n,z]+map[x,y,z]
elserec[x,y,z]←rec[x,y-1,z]+map[x,y,z];
readln;
这样,考察过程就可以改为
sum←sum+rec[x2,y2,z2]+rec[x1,y1,z2]-rec[x2,y1,z2]-rec[x1,y2,z2];
虽然状态数仍为O(n6),但单个状态的考察费用降为O
(1),因此时间复杂度降为O(n6)。
3、将原问题转化为规模更小的子问题
在“直译”枚举算法中
forx1←1tondo
可以看作从大正方体中挖出一个高为n的长方体,该该长方体水平面的左上角为(x1,y1),右下角为(x2,y2)。
接下来
forz1←1tondo
是在大的长方体中“截出”一个待考察的小的长方体。
这是一个降维的过程。
由于状态考察费用已经是O
(1),所以继续优化只能从减少被考察的状态数着手。
是否有“显然”不是解的状态呢?
观察下图
如果长方体a的数和是负数,则长方体a的计算结果废弃,考察长方体b-a。
因为长方体b的数和=长方体b-a的数和+长方体a的数和,由于长方体a的数和为负,长方体b-a的数和一定大于等于长方体b的数和。
由此可见,在累计长方体数和的时候,只要由上而下地枚举长方体下底面的z轴坐标即可。
total(z)——以z轴坐标为z的平面为下底面的长方体的最大数和
total(z)=
其中rec[x2,y2.,z]+rec[x1-1y1-1,z]-rec[x2,y1-1,z]-rec[x-1-1,y2.,z]表示z平面上以(x1,y1)为左上角、(x2,y2)为右下角的矩形的数和。
由此得出算法
forx1←1tondo{枚举所有可能的子平面}
total←0;
{长方体b(该长方体的平面以(x1,y1)为左上角、(x2,y2)为右下上角)的最大数和初始化}
forz←1tondo{枚举长方体b下底面的z轴坐标}
begin
total←max{total,0}+rec[x2,y2,z]+rec[x1-1,y1-1,z]-rec[x2,y1-1,z]-rec[x-1-1,y2,z];
{计算以z为下底面的长方体b的最大数和}
iftotal>
bestthenbest←total;
end;
这一改进使得考察的状态整数降为n5,单个状态的考察费用依然保持O
(1)不变,最终将枚举的时间复杂度降到了O(n5)。
12.2回溯法
回溯法也是搜索算法中的一种控制策略,但与枚举法不同的是,它是从初始状态出发,运用题目给出的条件、规则,按照深度优秀搜索的顺序扩展所有可能情况,从中找出满足题意要求的解答。
回溯法是求解特殊型计数题或较复杂的枚举题中使用频率最高的一种算法。
一、回溯法的基本思路
何谓回溯法,我们不妨通过一个具体实例来引出回溯法的基本思想及其在计算机上实现的基本方法。
【例题12.2.1】n皇后问题
一个n×
n(1≤n≤100)的国际象棋棋盘上放置n个皇后,使其不能相互攻击,即任何两个皇后都不能处在棋盘的同一行、同一列、同一条斜线上,试问共有多少种摆法?
n
所有分案。
每个分案为n+1行,格式:
方案序号
以下n行。
其中第i行(1≤i≤n)行为棋盘i行中皇后的列位置。
在分析算法思路之前,先让我们介绍几个常用的概念:
1、状态(state)
状态是指问题求解过程中每一步的状况。
在n皇后问题中,皇后所在的行位置i(1≤i≤n)即为其时皇后问题的状态。
显然,对问题状态的描述,应与待解决问题的自然特性相似,而且应尽量做到占用空间少,又易于用算符对状态进行运算。
2、算符(operater)
算符是把问题从一种状态变换到另一种状态的方法代号。
算符通常采用合适的数据来表示,设为局部变量。
n皇后的一种摆法对应1..n排列方案(a1,…,an)。
排列中的每个元素ai对应i行上皇后的列位置(1≤i≤n)。
由此想到,在n皇后问题中,采用当前行的列位置i(1≤i≤n)作为算符是再合适不过了。
由于每行仅放一个皇后,因此行攻击的问题自然不存在了,但在试放当前行的一个皇后时,不是所有列位置都适用。
例如(l,i)位置放一个皇后,若与前1..l-1行中的j行皇后产生对角线攻击(|j-l|=|aj-i|)或者列攻击(i≠aj),那么算符i显然是不适用的,应当舍去。
因此,不产生对角线攻击和列攻击是n皇后问题的约束条件,即排列(排列a1,…,ai,…,aj,…,an)必须满足条件(|j-i|≠|aj-ai|)and(ai≠aj)(1≤i,j≤n)。
3、解答树(analytictree)
现在让我们先来观察一个简单的n皇后问题。
设n=4,初始状态显然是一个空棋盘。
此时第一个皇后开始从第一行第一列位置试放,试放的顺序是从左至右、自上而下。
每个棋盘由4个数据表征相应的状态信息(见下图):
(×
×
)
其中第i(1≤i≤4)个数据指明当前方案中第i个皇后置放在第i行的列位置。
若该数据为0,表明所在行尚未放置皇后。
棋盘状态的定义如下
var
stack:
array[1‥4]ofinteger;
{stack[i]为i行皇后的列位置}
从初始的空棋盘出发,第1个皇后可以分别试放第1行的4个列位置,扩展出4个子结点。
在上图中,结点右上方给出按回溯法扩展顺序定义的结点序号。
现在我们也可以用相同方法找出这些结点的第二行的可能列位置,如此反复进行,一旦出现新结点的四个数据全非空,那就寻到了一种满足题意要求的摆法。
当尝试了所有可能方案,即获得了问题的解答,于是得到了下列图形。
该图形象一棵倒悬的树。
其初始结点v1叫根结点,而最下端的结点v3、v5、v9、v13、v16、v17称为叶结点,其中2个数据全非零的叶结点,亦即本题的目标结点。
由根结点到每一个目标结点之间,揭示了一种成功摆法的形成过程。
显然,4皇后问题存在由v9、v13表示的二种方案。
上图被称作解答树。
树中的每一结点都是当前方案中满足约束条件的元素状态。
除了根结点、叶结点以外的结点都称作分枝结点。
分枝结点愈接近根结点者,辈分愈高;
反之,愈远离根结点者,辈分愈低。
上图中结点v7是结点v8的父结点(又称前件),结点v13是结点v12的子结点(又称后件)。
某结点所拥有的子结点的个数称作该结点的次数。
显而易见,所有叶结点的次数为0。
树中各结点次数最大值,被称作为该树的次数。
算符的个数即为结解答树的次数。
由上图可见,4皇后的解答树是4次树。
一棵树中的某个分枝结点也可视作为“子根”,以该结点为根的树则称作“子树”。
由以上讨论可以看出解答树的结构:
1、初始状态构成(主)树的根结点。
对应于n皇后来说,初始时的空棋盘即为根结点;
2、除根结点以外,每个结点都具有一个、且只有一个父结点。
对应于n皇后问题来说,置放i行皇后的子结点,只有在置放了前i-1行皇后的一个父结点基础上产生;
3、每个非根结点都有一条路径通往根结点,其路径长度(代价)定义为这条路径的边数。
对应于n皇后来说,当前行序号即为路径代价。
当路径代价为n+1时,说明n个皇后已置放完毕,一种成功的摆法产生。
有了以上的基础知识和对n皇后问题的初步分析,我们已经清楚地看到,求解n皇后问题,无非就是做两件事:
1、从左至右逐条树枝地构造和检查解答树t;
2、检查t的结点是否对应问题的目标状态;
上述两件事同时进行。
为了加快检查速度,一般规定:
1、再扩展一个分枝结点前进行检查,只要它不满足约束条件,则不再构造以它为根的子树;
2、已处理过的结点若以后不会再用,则不必保留。
即回溯过程中经过的结点不再保留。
例如在上图中,当我们求出第一种摆法v1-v2-v3后,由于皇后置放第三行任何列位置都会产生攻击,因此舍弃该摆法,开始寻求第二种摆法。
从上图可看出,第二条路径为