a算法的实现
Ⅰ C++ A* 算法实现中,怎样便捷地实现“搜索节点在OPEN表和CLOSED表之间 切换”
你是在用A*算法做八数码问题么。
比如你要在open表中删除一个节点,然后放到close表中,你可以反过来做,
先找到这个结点,在close表中插入,然后再在open表中删除
我把我写的windows下的代码贴过来你看看,顺便说一句,open表和close表都要经常做删除节点的操作,所以用vector(线性表)其实性能不好,我用的List。
Ⅱ 什么是 a算法a* 算法有什么特点
A*算法:A*(A-Star)算法是一种静态路网中求解最短路径最有效的直接搜索方法。估价值与实际值越接近,估价函数取得就越好
A* (A-Star)算法是一种静态路网中求解最短路最有效的直接搜索方法。
注意是最有效的直接搜索算法。之后涌现了很多预处理算法(ALT,CH,HL等等),在线查询效率是A*算法的数千甚至上万倍。
公式表示为: f(n)=g(n)+h(n),
其中 f(n) 是从初始点经由节点n到目标点的估价函数,
g(n) 是在状态空间中从初始节点到n节点的实际代价,
h(n) 是从n到目标节点最佳路径的估计代价。
保证找到最短路径(最优解的)条件,关键在于估价函数f(n)的选取:
估价值h(n)<= n到目标节点的距离实际值,这种情况下,搜索的点数多,搜索范围大,效率低。但能得到最优解。并且如果h(n)=d(n),即距离估计h(n)等于最短距离,那么搜索将严格沿着最短路径进行, 此时的搜索效率是最高的。
如果 估价值>实际值,搜索的点数少,搜索范围小,效率高,但不能保证得到最优解。
Ⅲ 如何实现apriori算法
java">importjava.util.HashMap;
importjava.util.HashSet;
importjava.util.Iterator;
importjava.util.Map;
importjava.util.Set;
importjava.util.TreeMap;
/**
*<B>关联规则挖掘:Apriori算法</B>
*
*<P>按照Apriori算法的基本思想来实现
*
*@authorking
*@since2013/06/27
*
*/
publicclassApriori{
privateMap<Integer,Set<String>>txDatabase;//事务数据库
privateFloatminSup;//最小支持度
privateFloatminConf;//最小置信度
privateIntegertxDatabaseCount;//事务数据库中的事务数
privateMap<Integer,Set<Set<String>>>freqItemSet;//频繁项集集合
privateMap<Set<String>,Set<Set<String>>>assiciationRules;//频繁关联规则集合
publicApriori(
Map<Integer,Set<String>>txDatabase,
FloatminSup,
FloatminConf){
this.txDatabase=txDatabase;
this.minSup=minSup;
this.minConf=minConf;
this.txDatabaseCount=this.txDatabase.size();
freqItemSet=newTreeMap<Integer,Set<Set<String>>>();
assiciationRules=newHashMap<Set<String>,Set<Set<String>>>();
}
/**
*扫描事务数据库,计算频繁1-项集
*@return
*/
publicMap<Set<String>,Float>getFreq1ItemSet(){
Map<Set<String>,Float>freq1ItemSetMap=newHashMap<Set<String>,Float>();
Map<Set<String>,Integer>candFreq1ItemSet=this.getCandFreq1ItemSet();
Iterator<Map.Entry<Set<String>,Integer>>it=candFreq1ItemSet.entrySet().iterator();
while(it.hasNext()){
Map.Entry<Set<String>,Integer>entry=it.next();
//计算支持度
Floatsupported=newFloat(entry.getValue().toString())/newFloat(txDatabaseCount);
if(supported>=minSup){
freq1ItemSetMap.put(entry.getKey(),supported);
}
}
returnfreq1ItemSetMap;
}
/**
*计算候选频繁1-项集
*@return
*/
publicMap<Set<String>,Integer>getCandFreq1ItemSet(){
Map<Set<String>,Integer>candFreq1ItemSetMap=newHashMap<Set<String>,Integer>();
Iterator<Map.Entry<Integer,Set<String>>>it=txDatabase.entrySet().iterator();
//统计支持数,生成候选频繁1-项集
while(it.hasNext()){
Map.Entry<Integer,Set<String>>entry=it.next();
Set<String>itemSet=entry.getValue();
for(Stringitem:itemSet){
Set<String>key=newHashSet<String>();
key.add(item.trim());
if(!candFreq1ItemSetMap.containsKey(key)){
Integervalue=1;
candFreq1ItemSetMap.put(key,value);
}
else{
Integervalue=1+candFreq1ItemSetMap.get(key);
candFreq1ItemSetMap.put(key,value);
}
}
}
returncandFreq1ItemSetMap;
}
/**
*根据频繁(k-1)-项集计算候选频繁k-项集
*
*@paramm其中m=k-1
*@paramfreqMItemSet频繁(k-1)-项集
*@return
*/
publicSet<Set<String>>aprioriGen(intm,Set<Set<String>>freqMItemSet){
Set<Set<String>>candFreqKItemSet=newHashSet<Set<String>>();
Iterator<Set<String>>it=freqMItemSet.iterator();
Set<String>originalItemSet=null;
while(it.hasNext()){
originalItemSet=it.next();
Iterator<Set<String>>itr=this.getIterator(originalItemSet,freqMItemSet);
while(itr.hasNext()){
Set<String>identicalSet=newHashSet<String>();//两个项集相同元素的集合(集合的交运算)
identicalSet.addAll(originalItemSet);
Set<String>set=itr.next();
identicalSet.retainAll(set);//identicalSet中剩下的元素是identicalSet与set集合中公有的元素
if(identicalSet.size()==m-1){//(k-1)-项集中k-2个相同
Set<String>differentSet=newHashSet<String>();//两个项集不同元素的集合(集合的差运算)
differentSet.addAll(originalItemSet);
differentSet.removeAll(set);//因为有k-2个相同,则differentSet中一定剩下一个元素,即differentSet大小为1
differentSet.addAll(set);//构造候选k-项集的一个元素(set大小为k-1,differentSet大小为k)
if(!this.has_infrequent_subset(differentSet,freqMItemSet))
candFreqKItemSet.add(differentSet);//加入候选k-项集集合
}
}
}
returncandFreqKItemSet;
}
/**
*使用先验知识,剪枝。若候选k项集中存在k-1项子集不是频繁k-1项集,则删除该候选k项集
*@paramcandKItemSet
*@paramfreqMItemSet
*@return
*/
privatebooleanhas_infrequent_subset(Set<String>candKItemSet,Set<Set<String>>freqMItemSet){
Set<String>tempSet=newHashSet<String>();
tempSet.addAll(candKItemSet);
Iterator<String>itItem=candKItemSet.iterator();
while(itItem.hasNext()){
Stringitem=itItem.next();
tempSet.remove(item);//该候选去掉一项后变为k-1项集
if(!freqMItemSet.contains(tempSet))//判断k-1项集是否是频繁项集
returntrue;
tempSet.add(item);//恢复
}
returnfalse;
}
/**
*根据一个频繁k-项集的元素(集合),获取到频繁k-项集的从该元素开始的迭代器实例
*@paramitemSet
*@paramfreqKItemSet频繁k-项集
*@return
*/
privateIterator<Set<String>>getIterator(Set<String>itemSet,Set<Set<String>>freqKItemSet){
Iterator<Set<String>>it=freqKItemSet.iterator();
while(it.hasNext()){
if(itemSet.equals(it.next())){
break;
}
}
returnit;
}
/**
*根据频繁(k-1)-项集,调用aprioriGen方法,计算频繁k-项集
*
*@paramk
*@paramfreqMItemSet频繁(k-1)-项集
*@return
*/
publicMap<Set<String>,Float>getFreqKItemSet(intk,Set<Set<String>>freqMItemSet){
Map<Set<String>,Integer>candFreqKItemSetMap=newHashMap<Set<String>,Integer>();
//调用aprioriGen方法,得到候选频繁k-项集
Set<Set<String>>candFreqKItemSet=this.aprioriGen(k-1,freqMItemSet);
//扫描事务数据库
Iterator<Map.Entry<Integer,Set<String>>>it=txDatabase.entrySet().iterator();
//统计支持数
while(it.hasNext()){
Map.Entry<Integer,Set<String>>entry=it.next();
Iterator<Set<String>>kit=candFreqKItemSet.iterator();
while(kit.hasNext()){
Set<String>kSet=kit.next();
Set<String>set=newHashSet<String>();
set.addAll(kSet);
set.removeAll(entry.getValue());//候选频繁k-项集与事务数据库中元素做差运算
if(set.isEmpty()){//如果拷贝set为空,支持数加1
if(candFreqKItemSetMap.get(kSet)==null){
Integervalue=1;
candFreqKItemSetMap.put(kSet,value);
}
else{
Integervalue=1+candFreqKItemSetMap.get(kSet);
candFreqKItemSetMap.put(kSet,value);
}
}
}
}
Ⅳ A*算法java实现
代码实现(Java)
1. 输入
(1) 代表地图二值二维数组(0表示可通路,1表示路障)
int[][] maps = {
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0 },
{ 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0 },
{ 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 }
};123456789123456789
(2) 按照二维数组的特点,坐标原点在左上角,所以y是高,x是宽,y向下递增,x向右递增,我们将x和y封装成一个类,好传参,重写equals方法比较坐标(x,y)是不是同一个。
public class Coord
{
public int x;
public int y;
public Coord(int x, int y)
{
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object obj)
{
if (obj == null) return false;
if (obj instanceof Coord)
{
Coord c = (Coord) obj;
return x == c.x && y == c.y;
}
return false;
}
}2223
(3) 封装路径结点类,字段包括:坐标、G值、F值、父结点,实现Comparable接口,方便优先队列排序。
public class Node implements Comparable
{
public Coord coord; // 坐标
public Node parent; // 父结点
public int G; // G:是个准确的值,是起点到当前结点的代价
public int H; // H:是个估值,当前结点到目的结点的估计代价
public Node(int x, int y)
{
this.coord = new Coord(x, y);
}
public Node(Coord coord, Node parent, int g, int h)
{
this.coord = coord;
this.parent = parent;
G = g;
H = h;
}
@Override
public int compareTo(Node o)
{
if (o == null) return -1;
if (G + H > o.G + o.H)
return 1;
else if (G + H < o.G + o.H) return -1;
return 0;
}
}
(4) 最后一个数据结构是A星算法输入的所有数据,封装在一起,传参方便。:grin:
public class MapInfo
{
public int[][] maps; // 二维数组的地图
public int width; // 地图的宽
public int hight; // 地图的高
public Node start; // 起始结点
public Node end; // 最终结点
public MapInfo(int[][] maps, int width, int hight, Node start, Node end)
{
this.maps = maps;
this.width = width;
this.hight = hight;
this.start = start;
this.end = end;
}
}
2. 处理
(1) 在算法里需要定义几个常量来确定:二维数组中哪个值表示障碍物、二维数组中绘制路径的代表值、计算G值需要的横纵移动代价和斜移动代价。
public final static int BAR = 1; // 障碍值
public final static int PATH = 2; // 路径
public final static int DIRECT_VALUE = 10; // 横竖移动代价
public final static int OBLIQUE_VALUE = 14; // 斜移动代价12341234
(2) 定义两个辅助表:Open表和Close表。Open表的使用是需要取最小值,在这里我们使用Java工具包中的优先队列PriorityQueue,Close只是用来保存结点,没其他特殊用途,就用ArrayList。
Queue openList = new PriorityQueue(); // 优先队列(升序)
List closeList = new ArrayList();1212
(3) 定义几个布尔判断方法:最终结点的判断、结点能否加入open表的判断、结点是否在Close表中的判断。
/**
* 判断结点是否是最终结点
*/
private boolean isEndNode(Coord end,Coord coord)
{
return coord != null && end.equals(coord);
}
/**
* 判断结点能否放入Open列表
*/
private boolean canAddNodeToOpen(MapInfo mapInfo,int x, int y)
{
// 是否在地图中
if (x 0 || x >= mapInfo.width || y 0 || y >= mapInfo.hight) return false;
// 判断是否是不可通过的结点
if (mapInfo.maps[y][x] == BAR) return false;
// 判断结点是否存在close表
if (isCoordInClose(x, y)) return false;
return true;
}
/**
* 判断坐标是否在close表中
*/
private boolean isCoordInClose(Coord coord)
{
return coord!=null&&isCoordInClose(coord.x, coord.y);
}
/**
* 判断坐标是否在close表中
*/
private boolean isCoordInClose(int x, int y)
{
if (closeList.isEmpty()) return false;
for (Node node : closeList)
{
if (node.coord.x == x && node.coord.y == y)
{
return true;
}
}
return false;
}353637383940414243444546
(4) 计算H值,“曼哈顿” 法,坐标分别取差值相加
private int calcH(Coord end,Coord coord)
{
return Math.abs(end.x - coord.x) + Math.abs(end.y - coord.y);
}12341234
(5) 从Open列表中查找结点
private Node findNodeInOpen(Coord coord)
{
if (coord == null || openList.isEmpty()) return null;
for (Node node : openList)
{
if (node.coord.equals(coord))
{
return node;
}
}
return null;
}
(6) 添加邻结点到Open表
/**
* 添加所有邻结点到open表
*/
private void addNeighborNodeInOpen(MapInfo mapInfo,Node current)
{
int x = current.coord.x;
int y = current.coord.y;
// 左
addNeighborNodeInOpen(mapInfo,current, x - 1, y, DIRECT_VALUE);
// 上
addNeighborNodeInOpen(mapInfo,current, x, y - 1, DIRECT_VALUE);
// 右
addNeighborNodeInOpen(mapInfo,current, x + 1, y, DIRECT_VALUE);
// 下
addNeighborNodeInOpen(mapInfo,current, x, y + 1, DIRECT_VALUE);
// 左上
addNeighborNodeInOpen(mapInfo,current, x - 1, y - 1, OBLIQUE_VALUE);
// 右上
addNeighborNodeInOpen(mapInfo,current, x + 1, y - 1, OBLIQUE_VALUE);
// 右下
addNeighborNodeInOpen(mapInfo,current, x + 1, y + 1, OBLIQUE_VALUE);
// 左下
addNeighborNodeInOpen(mapInfo,current, x - 1, y + 1, OBLIQUE_VALUE);
}
/**
* 添加一个邻结点到open表
*/
private void addNeighborNodeInOpen(MapInfo mapInfo,Node current, int x, int y, int value)
{
if (canAddNodeToOpen(mapInfo,x, y))
{
Node end=mapInfo.end;
Coord coord = new Coord(x, y);
int G = current.G + value; // 计算邻结点的G值
Node child = findNodeInOpen(coord);
if (child == null)
{
int H=calcH(end.coord,coord); // 计算H值
if(isEndNode(end.coord,coord))
{
child=end;
child.parent=current;
child.G=G;
child.H=H;
}
else
{
child = new Node(coord, current, G, H);
}
openList.add(child);
}
else if (child.G > G)
{
child.G = G;
child.parent = current;
// 重新调整堆
openList.add(child);
}
}
}85960618596061
(7) 回溯法绘制路径
private void drawPath(int[][] maps, Node end)
{
if(end==null||maps==null) return;
System.out.println("总代价:" + end.G);
while (end != null)
{
Coord c = end.coord;
maps[c.y][c.x] = PATH;
end = end.parent;
}
}12345678910111234567891011
(8) 开始算法,循环移动结点寻找路径,设定循环结束条件,Open表为空或者最终结点在Close表
public void start(MapInfo mapInfo)
{
if(mapInfo==null) return;
// clean
openList.clear();
closeList.clear();
// 开始搜索
openList.add(mapInfo.start);
moveNodes(mapInfo);
}
/**
* 移动当前结点
*/
private void moveNodes(MapInfo mapInfo)
{
while (!openList.isEmpty())
{
if (isCoordInClose(mapInfo.end.coord))
{
drawPath(mapInfo.maps, mapInfo.end);
break;
}
Node current = openList.poll();
closeList.add(current);
addNeighborNodeInOpen(mapInfo,current);
}
}
单元和区域和数值,,,中的最大
Ⅳ A*算法优化
A算法是游戏中路径搜索的常见算法。Dijkstra是最短路径的经典算法,A算法的思路基本上和Dijkstra算法一致,在Dijkstra算法的基础上增加了启发函数,也就是:
f(n) = g(n) + h(n)
其中,n是路径上某一点,g(n)是从出发点到该点的cost,h(n)是关于该点的启发函数,通常是对从该点到目标花费的一个估计,例如到目标的直线距离或者曼哈顿距离。 A算法每次选择f(n)最小的点,然后更新所有g(n)。
如果你明白Dijkstra算法,那么在这里h(n) = 0 的话,A算法就和Dijkstra算法一样了。
本文不详细讲解A算法,需要详细了解A算法的具体过程的,参见以下两篇文章:
理解A*算法的具体过程
A*算法详解
A*算法优化的关键在于h(n)的选择。 一个启发函数h(n)被称为admissible的,是指h(n)的估计,不会超过节点N到目标的实际花费。
如果h(x)满足以下条件,h(x)被称为单调的(monotone, or consistent)。 对于任意一条边(x,y),
h(x) <= d(x,y) + h(y)
其中d(x,y)是(x,y)的长度
如果满足这个条件,就意味着没有任何节点需要被处理多次,也就是说,在Dijkstra算法中,新加入一个节点会导致已添加节点中cost降低的情况不会存在,也就不需要去更新已添加节点(称为close set)。
如果一个启发函数是单调的,那么该启发函数一定是admissible的。如果该启发函数是admissible的,那么可以证明A*在同类算法中搜寻到最短的路径。
问题出在这里:如果我们更在意的是搜索的时间空间花费,而不是最优结果,那么A*算法就有优化空间。所以我们放松要求,修改我们的启发函数,使得我们搜寻到的路径不会比最佳路径差太多,就是优化算法,称为ε-admissible算法。
有多种ε-admissible算法,在此只举例最简单直接的一种: 加权A*(静态加权)算法。
假如ha(n)是一个admissible的启发函数,我们选取新的启发函数hw(n) = ε ha(n),其中ε>1 作为启发函数。就可以在某种程度上进行优化。 下图1是使用ha(n)作为启发式算法,下图2是使用hw(n)作为启发式算法,其中ε取5.
图1:ha(x)作为启发算法
图2:hn(x)作为启发算法
可以看出,ha(n)可以找到最小路径,但是多了许多无用的搜索;而hw(n)找到的不是最优路径,但是减少了大量无用搜索。
其他的优化算法思路类似都是在于启发函数的选择。详见参考文献。
参考文献:
https://en.wikipedia.org/wiki/A*_search_algorithm#Admissibility_and_optimality https://en.wikipedia.org/wiki/Consistent_heuristic
Ⅵ A*算法现实应用的实际意义
A*算法在人工智能中是一种典型的启发式搜索算法,为了说清楚A*算法,我看还是先说说何谓启发式算法。
一、何谓启发式搜索算法
在说它之前先提提状态空间搜索。状态空间搜索,如果按专业点的说法就是将问题求解过程表现为从初始状态到目标状态寻找这个路径的过程。通俗点说,就是在解一个问题时,找到一条解题的过程可以从求解的开始到问题的结果(好象并不通俗哦)。由于求解问题的过程中分枝有很多,主要是求解过程中求解条件的不确定性,不完备性造成的,使得求解的路径很多这就构成了一个图,我们说这个图就是状态空间。问题的求解实际上就是在这个图中找到一条路径可以从开始到结果。这个寻找的过程就是状态空间搜索。
常用的状态空间搜索有深度优先和广度优先。广度优先是从初始状态一层一层向下找,直到找到目标为止。深度优先是按照一定的顺序前查找完一个分支,再查找另一个分支,以至找到目标为止。这两种算法在数据结构书中都有描述,可以参看这些书得到更详细的解释。
前面说的广度和深度优先搜索有一个很大的缺陷就是他们都是在一个给定的状态空间中穷举。这在状态空间不大的情况下是很合适的算法,可是当状态空间十分大,且不预测的情况下就不可取了。他的效率实在太低,甚至不可完成。在这里就要用到启发式搜索了。
启发式搜索就是在状态空间中的搜索对每一个搜索的位置进行评估,得到最好的位置,再从这个位置进行搜索直到目标。这样可以省略大量无畏的搜索路径,提到了效率。在启发式搜索中,对位置的估价是十分重要的。采用了不同的估价可以有不同的效果。我们先看看估价是如何表示的。
启发中的估价是用估价函数表示的,如:
f(n) = g(n) + h(n)
其中f(n)是节点n的估价函数,g(n)实在状态空间中从初始节点到n节点的实际代价,h(n)是从n到目标节点最佳路径的估计代价。在这里主要是h(n)体现了搜索的启发信息,因为g(n)是已知的。如果说详细点,g(n)代表了搜索的广度的优先趋势。但是当h(n)>>g(n)时,可以省略g(n),而提高效率。这些就深了,不懂也不影响啦!我们继续看看何谓A*算法。
二、初识A*算法
启发式搜索其实有很多的算法,比如:局部择优搜索法、最好优先搜索法等等。当然A*也是。这些算法都使用了启发函数,但在具体的选取最佳搜索节点时的策略不同。象局部择优搜索法,就是在搜索的过程中选取“最佳节点”后舍弃其他的兄弟节点,父亲节点,而一直得搜索下去。这种搜索的结果很明显,由于舍弃了其他的节点,可能也把最好的节点都舍弃了,因为求解的最佳节点只是在该阶段的最佳并不一定是全局的最佳。最好优先就聪明多了,他在搜索时,便没有舍弃节点(除非该节点是死节点),在每一步的估价中都把当前的节点和以前的节点的估价值比较得到一个“最佳的节点”。这样可以有效的防止“最佳节点”的丢失。那么A*算法又是一种什么样的算法呢?其实A*算法也是一种最好优先的算法。只不过要加上一些约束条件罢了。由于在一些问题求解时,我们希望能够求解出状态空间搜索的最短路径,也就是用最快的方法求解问题,A*就是干这种事情的!我们先下个定义,如果一个估价函数可以找出最短的路径,我们称之为可采纳性。A*算法是一个可采纳的最好优先算法。A*算法的估价函数可表示为:
f'(n) = g'(n) + h'(n)
这里,f'(n)是估价函数,g'(n)是起点到终点的最短路径值,h'(n)是n到目标的最断路经的启发值。由于这个f'(n)其实是无法预先知道的,所以我们用前面的估价函数f(n)做近似。g(n)代替g'(n),但g(n)>=g'(n)才可(大多数情况下都是满足的,可以不用考虑),h(n)代替h'(n),但h(n)<=h'(n)才可(这一点特别的重要)。可以证明应用这样的估价函数是可以找到最短路径的,也就是可采纳的。我们说应用这种估价函数的最好优先算法就是A*算法。哈!你懂了吗?肯定没懂!接着看!
举一个例子,其实广度优先算法就是A*算法的特例。其中g(n)是节点所在的层数,h(n)=0,这种h(n)肯定小于h'(n),所以由前述可知广度优先算法是一种可采纳的。实际也是。当然它是一种最臭的A*算法。
再说一个问题,就是有关h(n)启发函数的信息性。h(n)的信息性通俗点说其实就是在估计一个节点的值时的约束条件,如果信息越多或约束条件越多则排除的节点就越多,估价函数越好或说这个算法越好。这就是为什么广度优先算法的那么臭的原因了,谁叫它的h(n)=0,一点启发信息都没有。但在游戏开发中由于实时性的问题,h(n)的信息越多,它的计算量就越大,耗费的时间就越多。就应该适当的减小h(n)的信息,即减小约束条件。但算法的准确性就差了,这里就有一个平衡的问题。
Ⅶ A*算法介绍
姓名:车文扬 学号:16020199006
【嵌牛导读】:A*算法的逐步详解
【嵌牛鼻子】:启发式算法
【嵌牛提问】:A*算法的原理是什么?
【嵌牛正文】:
A*算法
路径规划是指的是机器人的最优路径规划问题,即依据某个或某些优化准则(如工作代价最小、行走路径最短、行走时间最短等),在工作空间中找到一个从起始状态到目标状态能避开障碍物的最优路径。机器人的路径规划应用场景极丰富,最常见如游戏中NPC及控制角色的位置移动,网络地图等导航问题,小到家庭扫地机器人、无人机大到各公司正争相开拓的无人驾驶汽车等。
目前路径规划算法分为:
A*算法原理:
在计算机科学中,A*算法作为Dijkstra算法的扩展,因其高效性而被广泛应用于寻路及图的遍历,如星际争霸等游戏中就大量使用。在理解算法前,我们需要知道几个概念:
搜索区域(The Search Area):图中的搜索区域被划分为了简单的二维数组,数组每个元素对应一个小方格,当然我们也可以将区域等分成是五角星,矩形等,通常将一个单位的中心点称之为搜索区域节点(Node)。
开放列表(Open List):我们将路径规划过程中待检测的节点存放于Open List中,而已检测过的格子则存放于Close List中。
父节点(parent):在路径规划中用于回溯的节点,开发时可考虑为双向链表结构中的父结点指针。
路径排序(Path Sorting):具体往哪个节点移动由以下公式确定:F(n) = G + H 。G代表的是从初始位置A沿着已生成的路径到指定待检测格子的移动开销。H指定待测格子到目标节点B的估计移动开销。
启发函数(Heuristics Function):H为启发函数,也被认为是一种试探,由于在找到唯一路径前,我们不确定在前面会出现什么障碍物,因此用了一种计算H的算法,具体根据实际场景决定。在我们简化的模型中,H采用的是传统的曼哈顿距离(Manhattan Distance),也就是横纵向走的距离之和。
如下图所示,绿色方块为机器人起始位置A,红色方块为目标位置B,蓝色为障碍物。
我们把要搜寻的区域划分成了正方形的格子。这是寻路的第一步,简化搜索区域。这个特殊的方法把我们的搜索区域简化为了2 维数组。数组的每一项代表一个格子,它的状态就是可走(walkalbe)或不可走(unwalkable) 。现用A*算法寻找出一条自A到B的最短路径,每个方格的边长为10,即垂直水平方向移动开销为10。因此沿对角移动开销约等于14。具体步骤如下:
从起点 A 开始,把它加入到一个由方格组成的open list(开放列表) 中,这个open list像是一个购物清单。Open list里的格子是可能会是沿途经过的,也有可能不经过。因此可以将其看成一个待检查的列表。查看与A相邻的8个方格 ,把其中可走的 (walkable) 或可到达的(reachable) 方格加入到open list中。并把起点 A 设置为这些方格的父节点 (parent node) 。然后把 A 从open list中移除,加入到close list(封闭列表) 中,close list中的每个方格都是不需要再关注的。
如下图所示,深绿色的方格为起点A,它的外框是亮蓝色,表示该方格被加入到了close list 。与它相邻的黑色方格是需要被检查的,他们的外框是亮绿色。每个黑方格都有一个灰色的指针指向他们的父节点A。
下一步,我们需要从open list中选一个与起点A相邻的方格。但是到底选择哪个方格好呢?选F值最小的那个。我们看看下图中的一些方格。在标有字母的方格中G = 10 。这是因为水平方向从起点到那里只有一个方格的距离。与起点直接相邻的上方,下方,左方的方格的G 值都是10 ,对角线的方格G 值都是14 。H值通过估算起点到终点( 红色方格) 的Manhattan 距离得到,仅作横向和纵向移动,并且忽略沿途的障碍。使用这种方式,起点右边的方格到终点有3 个方格的距离,因此H = 30 。这个方格上方的方格到终点有4 个方格的距离( 注意只计算横向和纵向距离) ,因此H = 40 。
比较open list中节点的F值后,发现起点A右侧节点的F=40,值最小。选作当前处理节点,并将这个点从Open List删除,移到Close List中。
对这个节点周围的8个格子进行判断,若是不可通过(比如墙,水,或是其他非法地形)或已经在Close List中,则忽略。否则执行以下步骤:
若当前处理节点的相邻格子已经在Open List中,则检查这条路径是否更优,即计算经由当前处理节点到达那个方格是否具有更小的 G值。如果没有,不做任何操作。相反,如果G值更小,则把那个方格的父节点设为当前处理节点 ( 我们选中的方格 ) ,然后重新计算那个方格的 F 值和 G 值。
若当前处理节点的相邻格子不在Open List中,那么把它加入,并将它的父节点设置为该节点。
按照上述规则我们继续搜索,选择起点右边的方格作为当前处理节点。它的外框用蓝线打亮,被放入了close list 中。然后我们检查与它相邻的方格。它右侧的3个方格是墙壁,我们忽略。它左边的方格是起点,在close list 中,我们也忽略。其他4个相邻的方格均在open list 中,我们需要检查经由当前节点到达那里的路径是否更好。我们看看上面的方格,它现在的G值为14 ,如果经由当前方格到达那里,G值将会为20( 其中10为从起点到达当前方格的G值,此外还要加上从当前方格纵向移动到上面方格的G值10) ,因此这不是最优的路径。看图就会明白直接从起点沿对角线移动到那个方格比先横向移动再纵向移动要好。
当把4个已经在open list 中的相邻方格都检查后,没有发现经由当前节点的更好路径,因此不做任何改变。接下来要选择下一个待处理的节点。因此再次遍历open list ,现在open list中只有7 个方格了,我们需要选择F值最小的那个。这次有两个方格的F值都是54,选哪个呢?没什么关系。从速度上考虑,选择最后加入open list 的方格更快。因此选择起点右下方的方格,如下图所示。
接下来把起点右下角F值为54的方格作为当前处理节点,检查其相邻的方格。我们发现它右边是墙(墙下面的一格也忽略掉,假定墙角不能直接穿越),忽略之。这样还剩下 5 个相邻的方格。当前方格下面的 2 个方格还没有加入 open list ,所以把它们加入,同时把当前方格设为他们的父亲。在剩下的 3 个方格中,有 2 个已经在 close list 中 ( 一个是起点,一个是当前方格上面的方格,外框被加亮的 ) ,我们忽略它们。最后一个方格,也就是当前方格左边的方格,检查经由当前方格到达那里是否具有更小的 G 值。没有,因此我们准备从 open list 中选择下一个待处理的方格。
不断重复这个过程,直到把终点也加入到了open list 中,此时如下图所示。注意在起点下方2 格处的方格的父亲已经与前面不同了。之前它的G值是28并且指向它右上方的方格。现在它的G 值为20 ,并且指向它正上方的方格。这是由于在寻路过程中的某处使用新路径时G值更小,因此父节点被重新设置,G和F值被重新计算。
那么我们怎样得到实际路径呢?很简单,如下图所示,从终点开始,沿着箭头向父节点移动,直至回到起点,这就是你的路径。
A*算法总结:
1. 把起点加入 open list 。
2. 重复如下过程:
a. 遍历open list ,查找F值最小的节点,把它作为当前要处理的节点,然后移到close list中
b. 对当前方格的 8 个相邻方格一一进行检查,如果它是不可抵达的或者它在close list中,忽略它。否则,做如下操作:
□ 如果它不在open list中,把它加入open list,并且把当前方格设置为它的父亲
□ 如果它已经在open list中,检查这条路径 ( 即经由当前方格到达它那里 ) 是否更近。如果更近,把它的父亲设置为当前方格,并重新计算它的G和F值。如果你的open list是按F值排序的话,改变后你可能需要重新排序。
c. 遇到下面情况停止搜索:
□ 把终点加入到了 open list 中,此时路径已经找到了,或者
□ 查找终点失败,并且open list 是空的,此时没有路径。
3. 从终点开始,每个方格沿着父节点移动直至起点,形成路径。