原文地址http://princetonboy.ycool.com/post.2805302.html
【摘要】本文讨论了搜索算法中“剪枝”这一常见的优化技巧. 首先由回溯法解决迷宫问题展开论述,介绍了什么是剪枝; 而后分析剪枝的三个原则正确、准确、高效,并分别就剪枝的两种思路:可行性剪枝及最优性剪枝,结合例题作进一步的阐述; 最后对剪枝优化方法进行了一些总结.
【关键字】搜索、优化、剪枝、时间复杂度
引
论
在竞赛中
,
我们有时会碰到一些题目
,
它们既不能通过建立数学模型解决
,
又没有现成算法可以套用
,
或者非遍历所有状况才可以得出正确结果
.
这时
,
我们就必须采用搜索算法来解决问题
.
搜索算法按搜索的方式分有两类
,
一类是深度优先搜索
,
一类是广度优先搜索
.
我们知道
,
深度搜索编程简单
,
程序简洁易懂
,
空间需求也比较低
,
但是这种方法的时间复杂度往往是指数级的
,
倘若不加优化
,
其时间效率简直无法忍受
;
而广度优先搜索虽然时间复杂度比前者低一些
,
但其庞大的空间需求量又往往让人望而却步
.
所以
,
对程序进行优化
,
就成为搜索算法编程中最关键的一环
.
本文所要讨论的便是搜索算法中优化程序的一种基本方法
“
剪枝
”.
什么是剪枝
相信刚开始接触搜索算法的人
,
都做过类似迷宫这样的题目吧
.
我们在
“
走迷宫
”
的时候
,
一般回溯法思路是这样的
:
1、
这个方向有路可走
,
我没走过
2、
往这个方向前进
3、
是死胡同
,
往回走
,
回到上一个路口
4、
重复第一步
,
直到找着出口
这样的思路很好理解
,
编程起来也比较容易
.
但是当迷宫的规模很大时
,
回溯法的缺点便暴露无遗
:
搜索耗时极巨
,
无法忍受
.
我们可不可以在向某个方向前进时
,
先一步判断出这样走会不会走到死胡同里呢
?
这样一来
,
搜索的时间不就可以减少了吗?
答案是
:
可以的
.
剪枝的概念
,
其实就跟走迷宫避开死胡同差不多
.
若我们把搜索的过程看成是对一棵树的遍历
,
那么剪枝顾名思义
,
就是将树中的一些
“
死胡同
”,
不能到达我们需要的解的枝条
“
剪
”
掉
,
以减少搜索的时间
.
搜索算法
,
绝大部分需要用到剪枝
.
然而
,
不是所有的枝条都可以剪掉
,
这就需要通过设计出合理的判断方法
,
以决定某一分支的取舍
.
在设计判断方法的时候
,
需要遵循一定的原则
.
剪枝的原则
1、
正确性
正如上文所述
,
枝条不是爱剪就能剪的
.
如果随便剪枝
,
把带有最优解的那一分支也剪掉了的话
,
剪枝也就失去了意义
.
所以
,
剪枝的前提是一定要保证不丢失正确的结果
.
2、
准确性
在保证了正确性的基础上
,
我们应该根据具体问题具体分析
,
采用合适的判断手段
,
使不包含最优解的枝条尽可能多的被剪去
,
以达到程序
“
最优化
”
的目的
.
可以说
,
剪枝的准确性
,
是衡量一个优化算法好坏的标准
.
3、
高效性
设计优化程序的根本目的
,
是要减少搜索的次数
,
使程序运行的时间减少
.
但为了使搜索次数尽可能的减少
,
我们又必须花工夫设计出一个准确性较高的优化算法
,
而当算法的准确性升高
,
其判断的次数必定增多
,
从而又导致耗时的增多
,
这便引出了矛盾
.
因此
,
如何在优化与效率之间寻找一个平衡点
,
使得程序的时间复杂度尽可能降低
,
同样是非常重要的
.
倘若一个剪枝的判断效果非常好
,
但是它却需要耗费大量的时间来判断、比较
,
结果整个程序运行起来也跟没有优化过的没什么区别
,
这样就太得不偿失了
.
综上所述
,
我们可以把剪枝优化的主要原则归结为六个字
:
正确、准确、高效
.
剪枝算法按照其判断思路可大致分成两类
:
可行性剪枝及最优性剪枝
.
下面分别结合例题对这两种方法进行阐述
.
可行性剪枝
这个方向可不可以走
?
走下去会不会碰到死胡同
?
这就是对某一枝条进行可行性剪枝的简要判断过程
.
我们现在来看这样一道题
.
问题简述
:
一个规则矩形网络状的城市
,
城市中心坐标为(
0,0
)
.
城市包含
M
个无法通行的路障
(M<=50),
采用如下规则游历城市
:
第一步走
1
格
,
第二步走
2
格
,
依此类推
,
第
N
步走
n
格
(N<=20),
除了第一步有四个方向可走
,
其余各步必须在前一步基础上左转或右转
90
度
,
最后回到出发点(
0,0
)
.
对于给定的
N
、
M,
编程求出所有可行的路径
.
这道题为
ACM
竞赛中
“
黄金图形
”
一题的简化
,
曾在
GDKOI98
中出为
“
数学家旅游
”
一题
.
书中的解答采用了简单的回溯法
,
原因是该题的本身就已经有很强的剪枝判断了
.
那么我们先来分析一下用回溯法解题的思路
:
用
x,y
两个变量存储当前坐标
,
每一步对
x,y
的值进行修改
,
没有遇到障碍就继续走
,
走完
n
步看看有没有回到(
0,0
)
,
没有的话回溯搜索
,
直到找完所有路径
.
接着
,
我们来看看这种算法的时间复杂度
.
一共走
n
步
,
每步要搜索四个方向
,
假设在最坏的情况下
,
没有任何障碍物
,
那么它的时间复杂度应该为
:O(4
n
).
很明显
,
这样的算法效率并不会很高
,
所以我们必须对程序进行剪枝
,
在未走完
n
步之前就提早判断出这样的走法是否可行
.
当走到第
o
步时
,
假设当前坐标为(
xo,yo
)
,
那么离(
0,0
)的最远距离就应该是
Max(xo, yo),
而剩下的
n-o
步可以走的最远距离则是
(o+1)+(o+2)+……+n
.
所以
,
若
(o+1)+(o+2)+……+n
的话
,
就表示就算现在
“
回头
”
也没办法到达出发点了
,
也就是说这条分支即便再搜索下去也找不出解来
,
这时我们已经可以舍弃这一分支而回溯了
.
这样剪枝似乎已经不错了
,
但是
,
它的效果只有当数据较大时
,
才能体现得明显
.
除了上述的优化
,
还有没有其他的方法呢
?
我们可以这样想
,
这个城市是规则矩形网络状的
,
东、南、西、北四个方向都是对称的
.
打个比方说
,
与(
1,0
)这个点对称的可以有(
-1,0
)
,
(
0,1
)
,
(
0,-1
)这三点
.
那可否设想
,
当从一个方向出发
,
寻找到一个解之后
,
将这个解旋转
90
o
、
180
o
、
270
o
,
不就得出其余三个解了么
?
这样岂不是节省了
3/4
的搜索次数
?
由这个设想出发
,
我们可以设计出下面的优化
:
忽略所有的障碍物
,
第一步固定走方向
a
(比如东面)
,
在这个基础上搜索路径
,
每找到一条路径都将其余三个
“
对称路径
”
一起判断
,
看看有没有经过障碍物
,
若没有则该路径为解之一
.
通过以上分析
,
我们已经可以编出一个效率较高的搜索程序
.
请看下表
:
“
黄金图形
”
的测试情况(单位
:
秒)
测试结果分析
:
1.
普通回溯法
,
在处理比较小的数据时
,
耗时还是比较低的
,
但当规模扩大到一定程度时
,
其时间复杂度呈指数级上升
,
因此竞赛时应尽量避免使用单纯的不加优化的回溯法
.
2.
采用第一种剪枝方法
,
当数据较小时与普通回溯法耗时相当
,
数据规模逐渐增大时
,
与回溯法的耗时差距便逐渐拉开
,
因为剪枝得当
,
搜索次数比不加优化时至少减小了一半
.
3.
用对称性来使时间复杂度减少一个指数级
,
从表中可以明显看出优化后的程序与完全不优化的耗时简直不可同日而语
,
与前一剪枝方法比较
,
按照剪枝原则中准确性原则来判断
,
这种方法比前者要好
.
4.
综合两种剪枝方法
,
准确性得到提高
,
耗时非常少
.
为了明显比较出各种算法的优劣
,
我将
N
值提高到
24,
结果综合优化的程序只需
21
秒便出解
,
耗时为普通回溯法十分之一
.
5.
这两种剪枝
,
以及综合的剪枝方法
,
都遵循了正确性的原则
.
它们之间的差异主要是在准确性与高效性两点上
.
可以说
,
最后一种优化算法综合了前两种
,
既提高了准确性
,
又保证了高效性
,
使得两种剪枝优势互补
,
取得了非常优秀的效果
.
竞赛中搜索程序常常使用不只一种的优化方法
,
所要求达到的就是这种效果
.