本文介绍的搜索算法主要指:广度优先搜索,深度优先搜索,以及在此基础上优化得来的
A*算法,分支限界算法。如有错误欢迎指正。
为了便于描述,搜索算法适用解决在一张有权无向图中,找到从原点到终点的最短路径。
Wait_arr[]数组存放待扩展的节点。
初始化:把初始节点root加入到wait_arr[]数组中
While( wait_arr[]数组不为空 ){
从wait_arr[]数组中取出左边第一个节点n
把取出节点扩展出邻接子节点
For( 遍历筛选每个邻接子节点 ){
If( 子节点没有出现在path中 ),留下这个子节点,然后记录这个子节点的父亲是节点n
}
If( 还存在子节点 )
子节点加入到wait_arr[]中。
}
注:由于每个子节点都保留了它的父亲节点,所以很容易逆推出到达该子节点的路径path
深度优先和广度优先的区别,从上面的通式中可以解释为红字部分的不同:
深度优先搜索:邻接子节点加入wait_arr[]数组的最左边,所以每次扩展的节点都是新的节点。
广度优先搜索:邻接子节点加入wait_arr[]数组的最右边,所以每次扩展的节点都是老一辈的节点。
A*算法实际上是模拟这么一个过程,在广场上,有一些遮挡物,人要从B点越过遮挡物到达出口E点,应该怎么做呢?人每一次可以选择向前,向后,向左,向右四种走法。但是人比较聪明,每一次选择自己记忆中直线距离离E点最近的那个方向迈步,如果不行再回溯试其他的方向。这就是A*算法。
A*算法和之前的朴素的搜索算法有什么区别和联系呢?
估价函数
f(n) = h(n) + g(n); 其中f(n)是代表当前节点n的估计代价,g(n)是从源节点到当前节点的已知代价,h(n)是当前节点到目标节点的估计代价。
A*算法的通式:
A*算法的通式 1:
Wait_arr[]数组存放待扩展的节点。
初始化:把初始节点root加入到wait_arr[]数组中
While( wait_arr[]数组不为空 ){
从wait_arr[]数组中取出左边第一个节点n
If( 是目标节点 ) break;
把取出节点扩展出邻接子节点
For( 遍历筛选每个邻接子节点 ){
If( 子节点没有出现在path中 ),留下这个子节点,然后记录这个子节点的父亲是节点n
}
If( 还存在子节点 )
子节点加入到wait_arr[]中。并计算估值函数f(n)。
把所有的点按照估值函数排序。
}
A*算法通式 2:
Open表 存放待扩展节点
Close表 存放访问过的节点
初始化:把初始节点root加入open表中
While( open表不空 ){
从open表中取出左边第一个节点,加入到close表中
If( 是目标节点 ) 输出路径,break;
把取出节点扩展出邻接子节点
for( 遍历筛选每个邻接子节点 ){
子节点中记录下父节点
求出子节点估值函数f(n)
If( 子节点不在open表中 也不在close表中 ){
加入open表
}else if( 子节点在open表中 ){
If( 新的估值函数比open表中原有子节点的更优 ){
Open表中原来的子节点被替换
}
} else{ //在close表中
If( 新的估值函数比close表中原有子节点的更优 ){//可以保证不会出现回路,不用判断这个子节点是否曾经出现在path中
Close表中原来的子节点被删除
Open表中加入新的子节点
}
}
}
把open表按照估价函数从优到劣排序。
}
通式1和通式2都可以得到正确解
通式2 相较于 通式1 有什么优点?
1. 通式1如果只维护一条路径(就是单纯记录扩展节点的父节点),在算法执行过程
中红色路径是先前的较短路径,扩展到a点后发现c点的估价函数更优,转而扩展c得到c的子节点b (毕竟b不曾出现在蓝色路径上)。但实际上蓝色路径比最左边红色那一段路径长,这就会使得起始点到b点的路径被覆盖为蓝色路径(因为b的父节点会被替换为c)。 然而通式2只有在蓝色路径比最左边一小段红色路径短的时候才会将b的父节点替换为c。
进一步我们可以想见,对于广度优先搜索,最优优先搜索都会面临路径被覆盖的问题,而深度优先搜索则不会,所以如果要输出所有路径,优先使用深度优先搜索(如果想要优化,可以结合分支限界法剪枝)
A*算法的相关定理:
A*算法能否找出最优解,取决于估价函数f(n)中的h(n)部分。
假设:F(n) = H(n) + g(n) H(n)是从当前节点到目标节点的实际路径长(当然这是目前无法得知的)
如果总有H(n) >= h(n),那么找到的路径一定是最短路径。
证明:假如最短路径p的长度是s,那么由于总有H(n) >= h(n),所以F(n) >=f(n)
假如,找到的路径pp长度是ss > s,那么还未搜索到的最短路径的f(n) <= s < ss,不满足A*算法每次扩展f(n)最小的节点的原则,矛盾。
在满足H(n) >= h(n)的情况下,h(n)越大,也就说明估值函数越精确,需要回溯的机会就越小,效率就越高。
相较于朴素的搜索算法,A*算法无法寻找出所有最优解。
A*算法例子,小人从左边一点到右边一点,要越过障碍物,估值函数中的h(n)是当前点到右边那一点的欧几里得距离,算法大概的路线是如下图所示,红色,蓝色是走岔的路径,黄色是最终修正得到的路径。
分支限界法
和A*算法有相通之处,都有估值函数,而且形式也是相同的:
f(n) = h(n) + g(n)
F_upper(n) = up_h(n) + g(n)
假如我们要找图中最短的从原点到终点的路径,那么我们就需要维护一个最短路径的上界(最短路径最长可能是多少),估值函数是最优解的下界,一般这个上界初始化是由贪心算法得到的。
分支限界算法基本流程(要求输出所有线路,从深度优先搜索中改编)
Wait_arr[]数组存放待扩展的节点。
用贪心算法算出最优解的上界upper。
初始化:把初始节点root加入到wait_arr[]数组中
While( wait_arr[]数组不为空 ){
从wait_arr[]数组中取出左边第一个节点n
If( 是目标节点 ){
输出路径
If(f(n)
} else{
If( 当前节点f(n)>upper ){
剪枝
} else{
If( F_upper(n) < upper ){ 更新upper }
把取出节点扩展出可扩展的邻接子节点
For( 遍历筛选每个邻接子节点 ){
If( 子节点没有出现在path中 ){
留下这个子节点,然后记录这个子节点的父亲是节点n
计算该子节点的f(n),F_upper(n)
子节点加入到wait_arr[]左边。
}
}
}
}
}