数据结构算法回顾-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
- - - - - - - - - - - - # - - - - - - -
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容