数据结构算法回顾-A*算法

维基百科的说法:

A搜索算法,俗称A星算法*。这是一种在图形平面上,有多个节点的路径,求出最低通过成本的算法。常用于游戏中的NPC的移动计算,或在线游戏的BOT的移动计算上。

A* 算法最常用作求游戏地图两点间的最短距离,rpg游戏用得最多,往往是客户端处理人物自动寻路,服务器处理怪物的自动寻路。当然A*是一个比较通用的算法,并不局限于游戏地图里面。

图1:

  • 启发式搜索
    从起点一步一步接近终点,每一步都有一个距离的评估结果,每一次选择估值最高的开始下一步,避免了穷举搜索;

  • f(n) = g(n) + h(n)
    f(n)就是对每个结点的评估函数,g(n)是起点到结点n的实际代价,h(n)是结点n到终点的评估值,g(n)需要g(1),g(2)...g(n-1)累积计算;

  • 结点
    把地图划分为多个结点组成,目的是方便计算,一个地图分得结点越多,计算结果自然就更准确,图1就是把地图划分为一个个正方形结点,一个正方形格子就是一个计算单位,表示一条路线就是多个格子组合起来;

  • 堆排序
    将计算的结果用二叉堆排序;

定义数据结构

  • 先定义地图:
    0 为可以通过,3 为障碍物,选中作为路线的结点赋值为 1
 int map[20][20] =
    {
        {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,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,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,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,0,0,0,0,0,0,0,0,0,0,0,0},
        {0,0,3,3,3,3,3,3,0,0,0,0,0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,3,3,0,0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,3,0,0,3,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,3,0,0,3,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,3,0,0,3,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,3,0,0,3,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0},
    };
  • 定义结点:
typedef struct star_node_t {
    int x;
    int y;
    int g;
    int h;
    int f;
} Star;
  • 定义一个二叉堆:
typedef struct heap_t {
    Star *arr;//这是一个数组指针
    int length;//当前堆长度
    bool (*func)(Star,Star);//比较函数
} Heap;
如何实现?

A*算法的搜索过程需要两张表,一个open表(用最小堆实现),用于存放发现的待比较的结点,一个close表(在本文里就是将map[x][y] = 1),存放已经判断过得结点

这里有个更加详细的描述

流程:

  1. 把起始格(如果用图1表示就是绿格)添加到open列表。
  2. 重复如下的工作:
    a) 寻找open列表中f值最低的格子。我们称它为当前格。
    b) 把它切换到close列表。
    c) 对相邻的8格中的每一个可通过且不在close表中不在open表中的结点添加进去。把当前格作为添加结点的父节点。记录这一格的f,g,和h值,如果它已经在open列表中,用g值为参考检查新一轮算出的g值是否更好,更低的g值意味着更好的路径(因为g值是累加值,所以不同的时候算出的g值可能会不一样)。如果是这样,就把这个结点的父节点改成当前格,并且重新计算这个结点的g和f值。如果你保持你的open列表按f值排序,改变之后你可能需要重新对开启列表排序。
    d) 停止,当你排序得到的最优结点加入了close列表,这时候路径被找到,保存路径。从终点开始,沿着每个结点的父节点移动直到回到起点,这就是你的路径。

为什么要用最小二叉堆?

每一轮最后都要找出open列表里的f最小值结点,也就是说需要一个高效的排序算法,但是又不需要把所有的值都排序,只要每一次就找到一个最小值即可,所以选择 插入删除后保持根结点最小二叉堆(没有用堆排序,只是利用二叉堆的性质)

二叉堆的实现:

二叉堆一般用数组来表示,如果存储数组的下标基于0,那么下标为 i 的节点的子节点是2i + 1与2i + 2;其父节点的下标是⌊(i − 1) ∕ 2⌋,
当父节点的键值总是大于或等于任何一个子节点的键值时为最大堆。 当父节点的键值总是小于或等于任何一个子节点的键值时为最小堆

  1. 现在构造一个最小堆:
int leftChild(int i) {
    return (i+1) * 2 -1;
}
int rightChild(int i) {
    return (i+1) *2;
}
int parent(int i) {
    return floor((i-1)/2);
}
bool min_heap_compare(Star node1,Star node2) {
    return (node1.f < node2.f);
} //比较函数
 Heap *open_heap;//声明一个最小堆
 Star star[300];//最小堆是数组储存的
 createHeap(open_heap,star,0,min_heap_compare);//构造函数
  1. 构造函数:
void createHeap(Heap *hp,Star *p,int length,bool (*function)(Star,Star)){
    hp -> arr = p;
    hp -> length = length;
    hp -> func = function;
}
  1. 插入函数:
void insert(Heap *hp,Star star) {
    int n = hp ->length;
    hp ->arr[n] = star;
    hp ->length = n + 1;
    filterUp(hp,n);//重下往上替换,见下文
}
  1. 更新函数:
void update(Heap *hp,Star star) {
    for (int i=0;i< hp->length;i++){
        if (star.x == hp->arr[i].x && star.y == hp->arr[i].y) {
            hp->arr[i].g = star.g;
            hp->arr[i].h = star.h;
            int f = hp->arr[i].f;
            hp->arr[i].f = star.f;
            if (f < star.f){
                filterDown(hp,i);//从上往下替换,见下文
            }else{
                filterUp(hp,i);
            }
        }
    }
}
  1. 删除函数:
Star pop(Heap *hp) {
    Star star = hp ->arr[0];
    swap(hp,0,hp ->length -1);
    hp ->length = hp ->length -1;
    filterDown(hp,0);
    return star;
}
  1. 交换函数:
void swap(Heap *hp,int index1,int index2) {
    if (index1 != index2)
    {
        Star tmp;
        tmp = hp->arr[index1];
        hp ->arr[index1] = hp->arr[index2];
        hp ->arr[index2] = tmp;
    }
}
  1. filterUp函数:
    我们知道插入一个结点就是将结点放入堆的最后,再和父节点比较,如果更下就交换,递归比较,一直到不再比父节点小或者比较到了根结点,这样就保持根节点最小,其中用到了fileterUp函数
void filterUp(Heap *hp,int node) {
    for(int i = parent(node);
            i >= 0 && hp -> func(hp ->arr[node],hp ->arr[i]);
            node = i,i= parent(node))
        swap(hp,node,i);
}
  1. filterDown函数:
    这个函数是从根节点往子结点比较,pop函数删除根节点,一般的作法是用最后一个值替换根节点,然后调用filterDown函数找到更小的子结点替换
void filterDown(Heap *hp, int node) {
    int l = leftChild(node);
    int r = rightChild(node);
    int largest = 0;
    int heapMaxI = hp -> length - 1;
    if (l <= heapMaxI && hp ->func(hp ->arr[l],hp ->arr[node]))
        largest = l;
    else
        largest = node;

    if (r <= heapMaxI && hp ->func(hp ->arr[r],hp ->arr[largest]))
        largest = r;

    if (largest != node) {
        swap(hp,largest,node);
        filterDown(hp,largest);
    }else{
        return;
    }
}

寻路实现:

  1. 先定义开始结束结点
 Star begin_star ={0,0,0,0,0};
 Star end_star = {19,19,0,0,0};
  1. 一个open表的索引
    每一次从堆里读取太慢,open索引表存结点g值
 int in_open[20][20];
  1. 开始寻路:
    首先就要将开始点放入open堆里
void find_path(Star begin,Star end,int map[20][20],Heap *open_heap,int in_open[20][20]) {
    insert(open_heap,begin);
    find_next_point(begin,end,map,open_heap,in_open);
}
  1. 计算结点g,f,h值
Star get_star(int i,int j,Star now,Star end) {
    int addg;
    if (i != now.x && j != now.y) {
        addg  = 14;
    }else{
        addg  = 10;
    }
    Star star;
    star.x = i;
    star.y = j;
    star.g = now.g + addg;
    star.h =  abs(i-end.x)*10 + abs(j -end.y)*10;
    star.f = star.g + star.h;
    return star;
}
  1. 寻找下一个结点
    只要不是终点就会递归执行,找到选择结点的周围8个结点,判断是否加入open堆里,之后又在堆里找到最小f值的结点加入close表,在这里是map[x][y]=1
void find_around_star(Star now,Star end,int map[20][20], Heap *open_heap,int in_open[20][20]) {
    int x = now.x;
    int y = now.y;
    for (int i = x-1;i<=x+1;i++){
        for (int j = y-1;j<=y+1;j++) {
            if ( i >= 0 && j>=0 && !(i==x && j==y) && i <=19 && j<=19 && map[i][j] == 0){
                Star star = get_star(i,j,now,end);
                if (in_open[i][j] != 0) {//如果该结点已经在open表里就判断g值,如果更小就更新
                    if (in_open[i][j] > star.g){
                        in_open[i][j]=star.g;
                        update(open_heap,star);
                    }
                }else{
                    in_open[i][j]= star.g;
                    insert(open_heap,star);
                }
            }
        }
    }
}
void find_next_point(Star now,Star end,int map[20][20],Heap *open_heap,int in_open[20][20]) {
    if (now.x != end.x || now.y!=end.y){
        map[now.x][now.y]=1;
        find_around_star(now,end,map,open_heap,in_open);
        Star minStar = pop(open_heap);
        find_next_point(minStar,end,map,open_heap,in_open);
    }
}

最后如果将map二维数组打印出来应该是这样:
o 代表路线即map[x][y]==1的结点, # 代表障碍即map[x][y]==3的结点,其余的为map[x][y]==0的结点

o - - - - - - - - - - - - - - - - - - -
- o - - - - - - - - - - - - - - - - - -
- - o - - - - - - - - - - - - - - - - -
- - - o - - - - - - - - - - - - - - - -
- - - - o - - - - - - - - - - - - - - -
- - - - - o - - - - - - - - - - - - - -
- - - - - - o o - - - - - - - - - - - -
- - # # # # # # o o o - - - - - - - - -
- - - - - - - - o # # o - - - - - - - -
- - - - - - - - - # # - o - - - - - - -
- - - - - - - - - # - - - o - - - - - -
- - - - - - - - - # - - - - o - - - - -
- - - - - - - - - # - - # - - o - - - -
- - - - - - - - - # - - # - - - o - - -
- - - - - - - - - # - - # - - - - o - -
- - - - - - - - - # - - # - - - - - o -
- - - - - - - - - - - - # - - - - - - o
- - - - - - - - - - - - # - - - - - - o
- - - - - - - - - - - - # - - - - - - o
- - - - - - - - - - - - # - - - - - - -

你可能感兴趣的:(数据结构算法回顾-A*算法)