我们先修改一下之前的Dijkstra的实现,让它变得更像A*的结构。然后,我们再把Dijkstra改成A*。要想对A*有更深的理解,应该先学习Dijkstra。其实A*是Dijkstra的一种改进。
1.修改一下Dijkstra的实现
回顾一下之前Dijkstra的实现。Dijkstra需要从一个表Q中选出一个路径代价最小的顶点。之前我们的实现是,一开始就把所有的顶点都放入这个表Q中。仔细想下就会发现,那些被初始化为路径代价最大值0x0FFFFFFF的顶点是不可能会被选中的,对于这些顶点不需要遍历。从表中取出的路径代价最小的顶点,取出一个就表示从起点找到了到这个顶点的最短路径,这些顶点不需要再放回列表中。
我们可以对Dijkstra做这样一个小小的优化,虽然还是O(N^2),时间复杂度没有改变:
一开始只把起始顶点放入表中。
如果松弛成功,就把边终点指向的顶点放入表中。
这样做的话,Relax就要返回结果了。
实现代码如下:
- void Dijkstra::Execute( const Graph& Graph , const string& VetexId )
- {
- m_Ret.PathTree.clear( ) ;
- const auto& Vertexes = Graph.GetVertexes( ) ;
- Vertex* pVertexStart = Vertexes.find( VetexId )->second ;
- vector< Vertex* > Q ;
- // 初始化顶点
- for ( auto& it : Vertexes )
- {
- it.second->PathfindingData.Cost = 0x0FFFFFFF ;
- pVertexStart->PathfindingData.pParent = 0 ;
- }
- // 初始化起始顶点
- pVertexStart->PathfindingData.Cost = 0 ;
- pVertexStart->PathfindingData.pParent = 0 ;
- // 把起始顶点放入列表中
- Q.push_back( pVertexStart ) ;
- pVertexStart->PathfindingData.Flag = true ;
- for ( ; Q.size() > 0 ; )
- {
- // 选出最小路径估计的顶点
- auto v = ExtractMin( Q ) ;
- v->PathfindingData.Flag = false ;
- // 对所有的出边进行“松弛”
- const auto& EO = v->GetEdgesOut( ) ;
- for ( auto& it : EO )
- {
- Edge* pEdge = it.second ;
- Vertex* pVEnd = pEdge->GetEndVertex( ) ;
- bool bRet = Relax( v , pVEnd , pEdge->GetWeight( ) ) ;
- // 如果松弛成功,加入列表中。
- if ( bRet && pVEnd->PathfindingData.Flag == false )
- {
- Q.push_back( pVEnd ) ;
- pVEnd->PathfindingData.Flag = true ;
- }
- }
- // end for
- }
- // end for
- }
Dijkstra要比BFS聪明,BFS只是“盲目地”从队列中取出元素出来扩展,Dijkstra则知道每次应该选取路径代价最短的节点扩展。
2.A*算法
Dijkstra比BFS聪明,A*则比Dijkstra更聪明,运行更快。A*通过一个叫“启发式函数”的东东来改进扩展规则,它会尽量避免扩展其他无用的顶点,它的目标就是朝着目的地直奔而去的。这样说,好像A*长了眼睛一样能看到当前位置距离目标点还有多远。A*和上面的Dijkstra最大的区别就是有“眼睛”:启发式函数。
启发式函数会告诉A*应该优先扩展哪个顶点。启发式函数是怎么回事呢?公式表示是:F = G + H。简单地说,就是:当前顶点的路径代价(G) + 当前顶点距离目标顶点估计花费的代价(F)
之前对Dijkstra做修改优化,就是为了让它更加像A*算法。这里,把Dijkstra的启发式数据从选拥有最小路径代价的顶点改成选拥有最小的F(启发式函数的值)的顶点就变成了A*。估价函数H怎么设计呢?这里取顶点到目标顶点的距离即可。
我们需要对上面的Dijkstra和数据结构做如下改造:
1.顶点类的寻路数据结构体增加一个Heuristic字段。该字段用于A*算法,保存启发式函数计算出来的值。如下所示:
- class Vertex
- {
- // ... 省略了一些无关函数和字段
- // 和以前一样
- public :
- // 寻路算法需要的数据
- struct Pathfinding
- {
- // 顶点的前驱顶点。
- Vertex * pParent ;
- // 路径代价估计
- int Cost ;
- // 标识符
- int Flag ;
- // 启发式函数的计算出来的值
- int Heuristic ;
- Pathfinding( )
- {
- pParent = 0 ;
- Cost = 0 ;
- Flag = 0 ;
- Heuristic = 0 ;
- }
- }
- PathfindingData ;
- }
2.Dijkstra的Relax松弛函数,改成限制启发式函数F的值。如果计算出来的F值小于这个顶点原先的F值,就更新该顶点的父节点、实际路径代价、F值。
3.每次循环都判断下,找出来的最小F值的顶点是不是目标顶点。如果是目标顶点,说明找到了路径,算法结束。
用在这里的A*伪代码如下:
- AStar( 图G,起始顶点S,目标顶点T)
- {
- 把起点S放入Open表中
- while( Open表不为空)
- {
- 从Open表中取出估价值F最小的顶点v
- 标记v不在Open表中
- if( v 等于 目标顶点T)
- {
- // 找到了路径
- retrun ;
- }
- foreach( v的所有出边的终点顶点vEnd )
- {
- Relax( v , vEnd , 边的权值 )
- if( Relax松弛成功 且 顶点vEnd不在Open表中 )
- {
- 把vEnd放入Open表中 ;
- 标记vEnd在Open表中 ;
- }
- }
- }
- }
- bool Relax( 顶点from , 顶点to , 边上的权值 )
- {
- // A*启发式函数计算 F = G + H
- G = 顶点from的路径代价 + 边上的权值 ;
- H = 顶点to到目标顶点T的估计代价 ;
- F = G + H ;
- if( F < 顶点to的F估价值)
- {
- 记录to的父路径为from ;
- 顶点to的路径代价值更新为G ;
- 顶点to的启发式估价值F更新为F ;
- return true ;
- }
- return false ;
- }
可以看到,A*和我们改造的Dijkstra算法,是很像的。如果我们让A*的启发式函数F=G+H的H一直返回0,那就是一个Dijkstra。这里要把A*改成Dijkstra只需要修改一句话。
下面是我实现的A*算法。
AStar.h
- #pragma once
- #include "GraphPathfinding.h"
- #include <functional>
- class AStar : public GraphPathfinding
- {
- public:
- AStar( );
- ~AStar( );
- public :
- // 估计顶点到目标顶点的代价
- std::function<int( const Vertex* pVCurrent , const Vertex* pVTarget ) > Estimate ;
- public:
- virtual void Execute( const Graph& Graph , const string& VetexId ) override ;
- private :
- // 抽出最小路径估值的顶点
- inline Vertex* ExtractMin( vector< Vertex* >& Q ) ;
- // 松弛
- inline bool Relax( Vertex* v1 , Vertex* v2 , int Weight ) ;
- public:
- void SetTarget( Vertex* pVTarget ) { m_pVTarget = pVTarget ; }
- private:
- Vertex* m_pVTarget ;
- };
AStar.cpp
- #include "AStar.h"
- AStar::AStar( )
- {
- }
- AStar::~AStar( )
- {
- }
- void AStar::Execute( const Graph& Graph , const string& VetexId )
- {
- const auto& Vertexes = Graph.GetVertexes( ) ;
- Vertex* pVertexStart = Vertexes.find( VetexId )->second ;
- vector< Vertex* > Q ;
- // 初始化顶点
- for ( auto& it : Vertexes )
- {
- Vertex* pV = it.second ;
- pV->PathfindingData.Cost = 0 ;
- pV->PathfindingData.pParent = 0 ;
- pV->PathfindingData.Heuristic = 0x0FFFFFFF ;
- pV->PathfindingData.Flag = false ;
- }
- // 初始化起始顶点
- pVertexStart->PathfindingData.pParent = 0 ;
- pVertexStart->PathfindingData.Cost = 0 ;
- pVertexStart->PathfindingData.Heuristic = Estimate( pVertexStart , m_pVTarget ) ;
- // 把起始顶点放入列表中
- Q.push_back( pVertexStart ) ;
- pVertexStart->PathfindingData.Flag = true ;
- for ( ; Q.size( ) > 0 ; )
- {
- // 选出最小路径估计的顶点
- auto v = ExtractMin( Q ) ;
- v->PathfindingData.Flag = false ;
- if ( v == m_pVTarget )
- {
- return ;
- }
- // 对所有的出边进行“松弛”
- const auto& EO = v->GetEdgesOut( ) ;
- for ( auto& it : EO )
- {
- Edge* pEdge = it.second ;
- Vertex* pVEnd = pEdge->GetEndVertex( ) ;
- bool bRet = Relax( v , pVEnd , pEdge->GetWeight( ) ) ;
- // 如果松弛成功,加入列表中。
- if ( bRet && pVEnd->PathfindingData.Flag == false )
- {
- Q.push_back( pVEnd ) ;
- pVEnd->PathfindingData.Flag = true ;
- }
- }
- // end for
- }
- // end for
- }
- Vertex* AStar::ExtractMin( vector< Vertex* >& Q )
- {
- Vertex* Ret = 0 ;
- Ret = Q[ 0 ] ;
- int pos = 0 ;
- for ( int i = 1 , size = Q.size( ) ; i < size ; ++i )
- {
- if ( Ret->PathfindingData.Heuristic > Q[ i ]->PathfindingData.Heuristic )
- {
- Ret = Q[ i ] ;
- pos = i ;
- }
- }
- Q.erase( Q.begin( ) + pos ) ;
- return Ret ;
- }
- bool AStar::Relax( Vertex* v1 , Vertex* v2 , int Weight )
- {
- // 这里就是启发式函数
- int G = v1->PathfindingData.Cost + Weight ; // 取得从V1到V2的实际路径代价
- int H = Estimate( v2 , m_pVTarget ) ; // 估计V2到目标节点的路径代价
- int nHeuristic = G + H ; // 实际 + 估算 = 启发式函数的值
- // 如果从此路径达到目标会被之前计算的更短,就更新
- if ( nHeuristic < v2->PathfindingData.Heuristic )
- {
- v2->PathfindingData.Cost = G ;
- v2->PathfindingData.pParent = v1 ;
- v2->PathfindingData.Heuristic = nHeuristic ;
- return true ;
- }
- return false ;
- }
H函数(估计当前顶点到目标顶点的代价)”外包“到外部执行了。因为AStart类是不知道MapWalkVertex顶点类的存在的。为什么要”外包“执行,而不是在AStar类中做呢?如果要在AStar类中做,就需要知道每个顶点的几何位置,而顶点的几何位置是Cocos2D-x的Node类的属性。AStar类不应该和其他东西耦合,为了”独立“,”通用“,计算H就用观察者模式思想,”外包“执行了。
AStar类的使用,如下:
- // A*的H估价函数
- auto Estimate = [ ]( const Vertex* pVCurrent , const Vertex* pVTarget )->int
- {
- MapWalkVertex * pMwv1 = ( MapWalkVertex* )pVCurrent->UserData.find( "mwv" )->second ;
- MapWalkVertex * pMwv2 = ( MapWalkVertex* )pVTarget->UserData.find( "mwv" )->second ;
- Point v = pMwv1->getPosition( ) - pMwv2->getPosition( ) ;
- int H = v.getLength( ) ;
- return H ;
- } ;
- AStar AStar ;
- // 设置目的顶点
- AStar.SetTarget( pVertexTarget ) ;
- // 设置H估价函数
- AStar.Estimate = Estimate ;
- // 开始执行
- AStar.Execute( *m_pGraph , pMwvStart->GetGraphVertex( )->GetId( ) ) ;
OK ,A* 完成了。测试运行一下:
经过测试。我们的A*能找到最短路径。并且执行速度比Dijkstra和Spfa都快。
4.简要总结Djikstra,SPFA,A*算法
Dijsktra : 选出一个具有最小路径代价的顶点,松弛其所有的边。
SPFA : 用一个队列存放顶点,从队列中取出队头顶点,松弛其所有边,如果松弛成功,边上顶点入队。
A* : 是Djikstra的改进版。选出具有启发式函数值最小的顶点,松弛其所有的边。
4.本文源代码工程下载:
http://download.youkuaiyun.com/detail/stevenkylelee/7734787
转自:http://blog.youkuaiyun.com/stevenkylelee/article/details/38456419