zoukankan      html  css  js  c++  java
  • 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





  • 相关阅读:
    给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
    11
    实战 迁移学习 VGG19、ResNet50、InceptionV3 实践 猫狗大战 问题
    tx2系统备份与恢复
    如何在Ubuntu 18.04上安装和卸载TeamViewer
    bzoj 3732 Network (kruskal重构树)
    bzoj2152 聪聪可可 (树形dp)
    牛客 216D 消消乐 (二分图最小点覆盖)
    牛客 197E 01串
    Wannafly挑战赛23
  • 原文地址:https://www.cnblogs.com/lcchuguo/p/5118095.html
Copyright © 2011-2022 走看看