路径导航与启发式搜索

路径导航与启发式搜索

问题介绍

介绍需要求解的问题

随着生活水平的不断发展,我们出行的需求越来越高,需要到达的目的地也越来越远,很多地方都是我们不熟悉的地方。在那些地方怎么才能从一个点到达另一个点?在这么多可能的路径中哪一条才是最短的?或者说,车流量最少的、速度最快的、花费时间最少的、途径收费项目最少的……

这样的问题,在现实生活中,我们成为路径导航问题,或者是寻路问题等。

模型的建立

现在把问题抽象成一个长、宽都是100的正方形,在这个正方形中依次排布了100×100的单位边长为1的小正方形,小正方形表示的是每个点。用2种不同的颜色表示每个点的含义:例如,黑色表示这是一个障碍物,不能通行;白色表示这个点是可以通行的道路。

不失一般性,上面的模型也适用于现实中的导航。100×100的正方形,可以类似地扩展成200×200、1000×800。同时,道路就用白色的点表示,建筑物是黑色的点,河流是黑色的点,但是河流上的桥梁是白色的点……

在这样一张地图上,给定一个起点,给定一个终点,需要找到一条从起点到终点的合法路径,并尽可能使得这条路是最短的。我们定义最短,意思是经过的步数最少,此时每经过一个小正方形,权重加1。

不失一般性,这样找到的路径在现实中也是有意义的。例如,在上面的求解中,每经过一个小正方形,权重加1,如果在现实生活中,我们想要找到一条花费时间最少的路(尽管这条路不是最短的),那么我们可以根据不同路口的车流量不同,对相应的小正方形赋予不同的权重;又例如,如果我们不想走高速,那么可以把高速路对应的小正方形的花销定义为无穷大……

为什么要采用我们介绍的方法求解

很容易想到的是,我们生活的世界如此庞大,“条条大路通罗马”,不可能枚举出所有的可能的路径,然后排个序,找到最短的。

甚至,就是对于上面抽象出的这个100×100的小正方形,其中存在的路径可能也太多了,即使在计算机的帮助下,枚举所有可能,然后找到最短路,这效率也非常低。

所以我们需要用到启发式算法、局部搜索算法。

启发式搜索是利用问题拥有的启发信息来引导搜索,达到减少搜索范围、降低问题复杂度的目的。

启发式搜索中,定义了一个评价函数对各个子结点进行计算,其目的就是用来估算出“有希望”的结点来。

要对OPEN表进行排序的时候,就按照这种“希望”进行排序。最有希望通向目标结点的待扩展结点会被优先扩展。

程序设计与算法分析

一般图搜索框架重塑

在这个问题中,采用的算法框架仍旧是一般图搜索框架,这在上一次作业《皇后问题》中已经给出了详细的算法说明和完整代码。

这里只简要重塑这个框架。

while (true) {
   
	if (open.isEmpty())
		return false;
	Node currentNode = open.poll();
	closed.add(currentNode);
	if (isTarget(currentNode)) {
   
		latest = currentNode;
		return true;
	}
	ArrayList<Node> m = expand(currentNode);
	if (m.isEmpty()) 
		continue;
	else 
		setPointer(m, currentNode);
}

需要修改的地方是设置父节点时候,要注意对OPEN表和CLOSED表的维护,其中对OPEN表排序需要按照启发式函数的评估值来排序。

由于每次都是评估值最小的排在最前面,所以其实可以在加入到OPEN表的时候就直接加入到恰当的位置,于是,OPEN表可以用优先级队列PriorityQueue来实现。

if (!open.contains(node) && !closed.contains(node)) {
   
	
} else if (open.contains(node)) {
   
	if (newDissipative < node.getDissipative()) {
   
		
	}
} else if (newDissipative < node.getDissipative()) {
   
	
}

数据存放形式与数据结构定义

• OPEN表:用于存放刚生成的节点

• CLOSED表:用于存放将要扩展或已扩展的节点

• Node:表示结点

• parent:指向父节点

数据域成员声明

• source:起点

• terminal:终点

• maze:地图,值为true表示可以通行,值为false表示这是障碍物

• result:最终找到的路径,以链表的形式返回

• progress:找路期间曾经试图访问过的点,以链表的形式返回

方法声明

/**
 * 评估函数的h(x)的值
 * @param source 起点
 * @param terminal 终点
 * @param parent 当前的点的父亲
 * @param node 当前的点
 * @return h(x)的值
 */
private double heuristic(Node source, Node terminal, Node parent, Node node) 

/**
 * 获得给定的点的邻居,邻居的定义是周围8个点
 * @param node 给定的点
 * @return 给定点的邻居
 */
private ArrayList<Node> getNeighbours(Node node) 

/**
 * 判断当前点是不是合法,合法的定义是不超过地图边界,且不是障碍物
 * @param x 横坐标
 * @param y 纵坐标
 * @return 如果合法,返回true,否则,false
 */
private boolean isValid(int x, int y) 

/**
 * 判断当前点是不是终点
 * @param node 当前点
 * @return 如果已经到了终点,返回true,否则,false
 */
private boolean isTarget(Node node) 

/**
 * 以链表的形式返回最终找到的路径
 * @return 最终找到的路径
 */
public LinkedList<Node> getResult() 

/**
 * 以链表的形式获得曾经试图访问过的点
 * @return 曾经试图访问过的点
 */
public LinkedList<Node> getProgress()

图形用户界面相关、交互相关

为了方便程序的调试,以及为了比较三种算法之间的优劣,我顺手写了图形用户界面。

因为这部分内容与本次作业的核心算法关系不大,所以下面不再详细给出这部分的数据定义、算法描述,只简要做出如下说明:

黑色:障碍,表示不能通过的地方

白色:道路,表示可以走的地方

红色:最终的路径

蓝色:曾经试图寻找的点,蓝色区域越大,说明搜索效率越低,找了那么多才找到这条路,蓝色区域越小说明搜索效率越高

算法

题目描述中有一些地方有歧义

为了更加清晰地表示下面的3种算法,做如下约定:

1.输入一定合法

不论是地图还是起点终点,输入的格式和数据一定合法,不存在非法数据或者错误格式。

2.下标从0开始

所描述的点,例如(5,4)表示的是第6行第5列。

3.8个方向是等价的

不存在“上、下、左、右”比“左上、左下、右上、右下”要优先的情况

4.走1步的花销定义为1

不论这一步是水平还是垂直走,还是斜着走,只要是走1步,花销就是1

从(0,0)走到(1,1),是右下,这里花销不是 2 \sqrt{2} 2 ,仍取1

5.从起点到终点的最优路线只看花销

只要花销是一样的,那么这几条路只是走法上的不同,在优劣程度上认为是一样

例如,下面2种走法,看作是等价的,区别只是走法的不同,而没有优劣之分,因为他们的花销都是5

起点 途径 途径 途径 途径 终点
**** 途径 途径 **** **** ****
起点 **** **** 途径 途径 终点

最短路径算法

算法描述

首先看Dijkstra算法的描述:

这个算法是通过为每个顶点 v 保留目前为止所找到的从s到v的最短路径来工作的。初始时,原点 s 的路径权重被赋为 0 (d[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把d[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大,即表示我们不知道任何通向这些顶点的路径(对于所有顶点的集合 V 中的任意顶点 v, 若 v 不为 s 和上述 m 之一, d[v] = ∞)。当算法结束时,d[v] 中存储的便是从 s 到 v 的最短路径,或者如果路径不存在的话是无穷大。——该段引用自Wikipedia

那么就可以得到算法的主要思想。

首先从起点出发,试着往周围的8个点走,这时候这8个点都是1步就能到达的。之后,取这8个点的其中一个(因为这8个点距离起点都是1,所以都是最短的),继续找它的周围8个点,那么这些点的都是距离起点需要2步才能到达的。

每次都取待扩展的结点中,距离起点最短的结点往外扩展,这样搜索出来的路可以保证是最短路径。

在算法的实现过程中,如果一个点周围的8个点,有障碍物,或者已经超出了地图的边界,那么直接丢弃。

伪代码表示

于是,可以得到Dijkstra算法的伪代码描述。

对于图G中的每一个点V
初始化d[v] = Infinity,previous[v] = undefined   
d[s] = 0
S = empty
Q = 所有的V
while Q 非空
u = 取出(Q)中最小的点
把u加入到S
对于u能够走到的每一个v
if d[v] > d[u] + w(u,v)
d[v] = d[u] + w(u,v),previous[v] = u
流程图

从这里可以看到,如果用一般图搜索框架来实现Dijkstra算法,用OPEN表存放所有的待扩展的结点,每次取出最小值,就是一个以当前“最短值”作为标准的优先级队列,而每次取出的u,相当于是更新父节点,并且维护OPEN表和CLOSED表。

image-20210328201448802

image-20210328201502641

那么对于一般图搜索框架,需要修改以下部分。

for (Node node : m) {
   
	double newDissipative = 计算出新的评估值;
	if (!open.contains(node) && !closed.contains(node)) {
   
		node.setDissipative(newDissipative);
		open.add(node);
		node.setParent(parent);
	} else if (open.contains(node)) {
   
		if (newDissipative < node.getDissipative()) {
   
			node.setDissipative(newDissipative);
			node.setParent(parent);
		}
	} else if (newDissipative < node.getDissipative()) {
   
		node.setDissipative(newDissipative);
		node.setParent(parent);
		closed.remove(node);
		open.add(node);
	}
}

在这里,新的评估值就是 parent.getDissipative()+1

A*算法

算法描述

启发式搜索算法A,一般简称为A算法,是一种典型的启发式搜索算法。

其基本思想是:定义一个评价函数f,对当前的搜索状态进行评估,找出一个最有希望的结点来扩展。

评价函数的形式如下:

f ( n ) = g ( n ) + h ( n ) f(n)=g(n)+h(n) f(n)=g(n)+h(n)

在算法 A A A的评价函数中,使用的启发函数 h ( n ) h(n) h(n)是处在 h ∗ ( n ) h^*(n) h(n)的下界范围,即满足 h ( n ) ≤ h ∗ ( n ) h(n)≤h^*(n) h(n)h(n)时,则把这个算法称为算法 A ∗ A^* A

A ∗ A^* A算法实际上是分支界限和动态规划原理及使用下界范围的 h h h相结合的算法。

首先令 g ( n ) g(n)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凝神长老

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值