v←sqrt(5*l*l/(l-h));{计算即时速度v}
ifv>v0thenbreak;{若大于极限速度v0,则无法从三角形i起跳}
ok←true;
fork←i+1toj-1do{判断跳跃过程中是否碰到其他三角形}
begin
t←(x[k]-x[i])/v;{计算到达三角形k的时间}
if(v*t-5*t*t)-(y[k]-y[i])<1e-6then
begin{如果该时刻的竖直坐标增量大于起点到顶点k的竖直坐标增量,则抛物线在上方}
ok←false;
break;
end;{then}
end;{for}
ifokthenbest←j{若跳远成功,则三角形j为目前三角形i所能到达的最远点,否则跳远不能完成}
elsebreak;
end;{for}
write(best,'');(输出从三角形i的顶点出发所能到达的最右的三角形编号)
end;{for}
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{枚举所有可能的水平面}
forx2←1tondo
fory1←1tondo
fory2←1tondo
forz1←1tondo{枚举上平面的z轴坐标}
begin
sum←0;{长方体的体积初始化}
forz2←1tondo{枚举下底面的z轴坐标}
考察状态(x1,y1,z1,x2,y2,z2);
end;{for}
考察过程改为
forx←x1tox2do{计算长方体的体积}
fory←y1toy2dosum←sum+map[x,y,z2];
ifsum>bestthenbest←sum;{调整最优解}
由于利用了计算出的结果,整个算法的时间复杂度降为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平面的信息}
begin
fory←1tondo{逐列输入z平面上x行的信息}
begin
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];
end;{for}
readln;
end;{for}
这样,考察过程就可以改为
sum←sum+rec[x2,y2,z2]+rec[x1,y1,z2]-rec[x2,y1,z2]-rec[x1,y2,z2];
ifsum>bestthenbest←sum;
虽然状态数仍为O(n6),但单个状态的考察费用降为O
(1),因此时间复杂度降为O(n6)。
3、将原问题转化为规模更小的子问题
在“直译”枚举算法中
forx1←1tondo
forx2←1tondo
fory1←1tondo
fory2←1tondo
可以看作从大正方体中挖出一个高为n的长方体,该该长方体水平面的左上角为(x1,y1),右下角为(x2,y2)。
接下来
forz1←1tondo
forz2←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{枚举所有可能的子平面}
forx2←1tondo
fory1←1tondo
fory2←1tondo
begin
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;{for}
end;{for}
这一改进使得考察的状态整数降为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后,由于皇后置放第三行任何列位置都会产生攻击,因此舍弃该摆法,开始寻求第二种摆法。
从上图可看出,第二条路径为