• Cocos2d-x 地图行走的实现3:A*算法


      本文乃Siliphen原创,转载请注明出处:http://blog.csdn.net/stevenkylelee


      上一节《Cocos2d-x 地图行走的实现2:SPFA算法》:

      http://blog.csdn.net/stevenkylelee/article/details/38440663


      假设读者忘记了之前我们的Dijkstra的实现。请顺藤摸瓜翻到第一节文章回想一下。为什么要这样做呢?由于本节要讲的A*算法事实上是Dijkstra的一种改进,仅仅有理解了Dijkstra才干更好地理解A*。


      本节。我们先改动一下之前的Dijkstra的实现,让它变得更像A*的结构。然后。我们再把Dijkstra改成A*。


    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 。道理非常easy, H = 0 ,那就是 F = G + 0 ,F 直接等于 G 了,选拥有最小启示式函数值F的顶点就变成了选拥有最小路径代价的顶点,可见失去估价函数H的 A* 就和 Dijkstra 是一样的。所以,在选顶点方面,优化Dijkstra的方案也是优化A*的方案。


      Dijkstra 基于实际的路径代价进行扩展,一定能找到最优解。A*则是基于某种预计。假设你让估价函数H预计得太离谱,A* 就不一定能找到最优解了。

    估价值 <= 实际值A*才干找到最优解。


      以下是我实现的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.本文源码project下载:


      http://download.csdn.net/detail/stevenkylelee/7734787





  • 相关阅读:
    Navicat for MySQL远程连接的时候报错mysql 1130的解决方法
    阿里云主机 CentOS6.5 安装Mysql php Apache
    MAC下使用feddler进行抓包
    javascript钩子之Backbone里的实现
    SASS编译
    动态代理模式和AOP探究
    二分查找算法
    MyBatis在非Spring环境下第三方DataSource设置-Druid篇
    写字节流转换String 代码示例
    SpringAOP代理报错问题
  • 原文地址:https://www.cnblogs.com/lcchuguo/p/5118095.html
Copyright © 2020-2023  润新知