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

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

Siliphen2014-08-14 14:06:203815 次阅读

如果读者忘记了之前我们的Dijkstra的实现,请顺藤摸瓜翻到第一节文章回顾一下。为什么要这样做呢?因为本节要讲的A*算法其实是Dijkstra的一种改进,只有理解了Dijkstra才能更好地理解A*。


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


1.回顾和修改一下之前的Dijkstra的实现

回顾一下之前Dijkstra的实现。Dijkstra需要从一个表Q中选出一个路径代价最小的顶点。之前我们的实现是,一开始就把所有的顶点都放入这个表Q中。仔细想下就会发现,那些被初始化为路径代价最大值0x0FFFFFFF的顶点是不可能会被选中的,对于这些顶点不需要遍历。从表中取出的路径代价最小的顶点,取出一个就表示从起点找到了到这个顶点的最短路径,这些顶点不需要再放回列表中。


我们可以对Dijkstra做这样一个小小的优化,虽然还是O(N^2),时间复杂度没有改变:


一开始只把起始顶点放入表中。


如果松弛成功,就把边终点指向的顶点放入表中。


这样做的话,Relax就要返回结果了。


实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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*算法,保存启发式函数计算出来的值。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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*伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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 。道理很简单, H = 0 ,那就是 F = G + 0 ,F 直接等于 G 了,选拥有最小启发式函数值F的顶点就变成了选拥有最小路径代价的顶点,可见失去估价函数H的 A* 就和 Dijkstra 是一样的。所以,在选顶点方面,优化Dijkstra的方案也是优化A*的方案。


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


下面是我实现的A*算法。


AStar.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#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类的使用,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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* 完成了。测试运行一下:

1407996186280167.gif

经过测试。我们的A*能找到最短路径。并且执行速度比Dijkstra和Spfa都快。


4.简要总结Djikstra,SPFA,A*算法

  • Dijsktra : 选出一个具有最小路径代价的顶点,松弛其所有的边。

  • SPFA : 用一个队列存放顶点,从队列中取出队头顶点,松弛其所有边,如果松弛成功,边上顶点入队。

  • A* : 是Djikstra的改进版。选出具有启发式函数值最小的顶点,松弛其所有的边。


5.本文源代码工程Test.zip

来源网址:http://blog.csdn.net/stevenkylelee/article/details/38456419

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值