Astar Algorithm

参考文献:https://www.gamedev.net/reference/articles/article2003.asp
这篇东西写的贼好。

介绍

A*算法主要用于寻路,比如游戏中的自动寻路系统。比其他算法优越的地方就在于他缩小了搜索空间。像迪杰斯特拉弗洛伊德算法这些都只是考虑了到原点的距离,他没有考虑到终点的代价,而且求出了很多不太必要的信息。如果我们仅仅只是求两点的距离,就没有必要用迪杰斯特拉和弗洛伊德了,因为他们把原点到所有点的距离都求了出来。
还有一些其他的算法比如广度和深度搜索,这些算法的都是遍历可能的空间,而深度优先不适用于最短路径的搜索,因为深度优先求出来的路径和代码编写的搜索方向有关系,也就是说深度优先搜索的路径有随机性。所以深度优先如果要做最短路径就只能遍历所有的路径了。广度优先搜索适用于最短路径,但是搜索空间还是很大的,比如如下的场景:



@是起点和终点,#是墙,如果是广度优先,需要把以起点到终点为半径的圆都搜索一遍。



这样的复杂度还是挺大的。为了能够缩小搜索区域,我们可以考虑把对终点的代价也计算在内。

算法细节

和迪杰斯特拉类似,迪杰斯特拉是寻找离原点最近的点,那么我们只需要加上终点代价即可,于是我们定义代价函数F = G + H,G是原点的距离,用勾股定理可以知道斜方向的距离是14,正方向的距离是10,乘10方便计算。H是到达原点的距离,用哈密顿距离即可。
事实上这样的估计只是一种粗略估计,求出来的就算不是最短路径那也和最短路径差不多了。如果存在场景使得哈密顿距离误差很大的话那么求出来的就未必是最短路径了。
需要注意的是,对于终点的计算,方块都是固定的,但是对于G的计算有些特别,讲道理每一方块到原点距离都是一样的,但是如果一直用这样用勾股定理计算,麻烦不说,并且在搜索过程中就没办法调整路径了。
比如,我们要计算路径,自然要保存父节点


如图,当前我们在绿色节点右下角的蓝色节点处,但是问题来了,蓝色节点左下角,也就是方块F值是88的节点如果按照勾股定理计算G就是20,但是目前由于这个节点是当前蓝色节点发现的,所以他的父节点是当前蓝色节点,但是事实上这并不是离原点最近的指向,他应该指向上才是最近的。我们就没办法更新这些父子关系了。
于是我们规定,G节点不再直接计算,方块上下左右移动增加10,斜方向移动增加14即可,这样就可以保留从父节点再到原点的G值了,在搜索过程中就可以对比G值更新父节点。
如图:

当前在节点2,我们通过节点2搜索得到的F=88的节点,此节点G为28(左上角是F值,左下角是G值,右下角是H值)。但是当我们搜索到节点3的时候,3也发现了F=88的节点,这个时候发现从3号更短,与是把F=88的节点父亲设置为3号。这样就达到了更新的目的。如果没有搜索到3号,那么说明这些节点太远了,没有用。
定义了代价函数后就是如何搜索了。
首先规定搜索规则,上下左右可以搜索,斜方向也可以搜索,但是斜方向的搜索不能被正向的挡住。比如往右上角移动,那么右方向和上方向就不可以有阻碍。
以#为墙为例子:
。# 。 | 。# 。 | 。 。 。
。起 # |。起 。 | 。起 #
上面的情况都不可以进行右上角的移动,以此类推即可。
准备openList和endList,openList用来存放候选节点,也就是最短路径的候选值,endList用来存放已经搜索完成的节点。
初始情况,我们把起点丢进openList里面,每一次从openList里面取出F值最小的节点,然后把这个节点上下左右斜方向都进行扩展,把扩展出来的候选值计算好F放进openList里面,这个时候从openList拿出的节点已经完成搜索,我们只需要加入endList即可。不断重复这个循环,从openList中取值。
在从openList取出值并扩展的获得候选点的时候有3个情况。如果扩展的候选点在endList里面了,那就不用管了,如果不在endList里面在openList里面,说明他已经有一个父亲和一个从父亲计算来的G了,这个时候你就要判断从当前节点到这个候选点是不是更近,也就是G是不是更小的,如果是更小的就替换父亲,替换G。如果不在openList也不在endList,那就计算FGH添加进openList即可。
流程非常简单。
接下来就是实现代码。

代码实现

class node {
private:
    int x, y;
    node *parent;
    double G;
    double H;
    double F;

节点node信息,getset方法不贴了。

    int nRow;
    int nColumn;
    pair<int, int> beginning;
    pair<int, int> ending;
    vector<pair<int, int>> walls;
    vector<string> maps;
    vector<bool> visited_open;
    vector<bool> visited_end;
    //F G node
    vector<node *> openList;
    vector<node *> endList;

map中存储数据结构。visited_open和visited_end是为了防止搜索重复遍历open和end列表。

    void generateMap() {
        vector<string> results;
        results.resize(nRow);
        visited_open.resize(nRow * nColumn);
        visited_end.resize(nRow * nColumn);
        for (int i = 0; i < nRow; i++) {
            for (int j = 0; j < nColumn; j++) {
                results[i][j] = '.';
            }
        }
        for (int i = 0; i < walls.size(); i++) {
            results[walls[i].first][walls[i].second] = '#';
        }
        results[beginning.first][beginning.second] = '@';
        results[ending.first][ending.second] = '@';
        maps = results;
    }

生成地图。
主算法:

        node *begin = new node(beginning.first, beginning.second, nullptr);
        begin->setF(0);
        begin->setG(0);
        begin->setH(0);
        //起始点丢进openlist里面,更新最小点
        openList.push_back(begin);
        visited_open[begin->getX() * nColumn + begin->getY()] = true;

可以把起点丢进open列表,begin->getX() * nColumn + begin->getY()是这个点的编号,把矩阵拉长变成一维编号留下的值。

        while (!openList.empty()) {
            int index = getLowestF();
            node *current = openList[index];

            //cout << current->getX() << " " << current->getY() << endl;

            openList.erase(openList.begin() + index);

            endList.push_back(current);
            visited_end[current->getX() * nColumn + current->getY()] = true;
            if (current->getX() == ending.first && current->getY() == ending.second) {
                return;
            }

            addPoint(current);
        }
    }

首先遍历获取最小的F值,从open取出并加入end中,别忘了维护visited,其次把扩展的候选点加入open。

代码效果


初始地图。



箭头即是最短路径。
我们来看看搜索空间:



*号是加入open的节点,也就是参与搜索的节点,可以看到搜索空间比广度优先几乎少了一半。

算法优化

很明显,这个算法优化的空间还是比较大的,之前在openList就考虑过能不能用优先队列,因为在迪杰斯特拉算法中有优先队列是有很大的效率提升,但是问题就在于,在候选点存在openList列表的时候,我们需要进行更新父节点,这个时候要搜索openList了,C++的优先队列没有提供遍历功能,总不能全部倒出来再塞回去吧。所以如果使用优先队列,就需要考虑如何在优先队列中获取特点元素并完成动态更新的过程,因为你找到这个节点如果更新了之后,优先队列需要再动态的排序一次的。而具有让priorityqueue能动态排序的我目前就知道pair。不知道其他的语言优先队列能不能遍历,C++可以自己实现一个二叉堆即可。
其次,代价还是对于算法影响是非常大的,当H是0的时候,就完全退化成了迪杰斯特拉算法,因为他之和原点距离有关了。当H比实际代价要小上许多时,我们的搜索复杂度就会增加,因为这个时候主要取决于G,于是就会在原点周围不断扩展,搜索的点会非常多。
我们上面的例子中代价都是乘10,H的代价也是乘10的,一个格子就是10,我们现在缩减十倍,对于G来说,上下左右+10,斜方向加14,而对于H来说一格只加1,我们看看搜索空间的变化。



可以看到基本就退化成了广度优先。
当H原大于实际代价的时候,这就有一点随机性了,得看你障碍物的位置和你代码编写遍历方向的顺序有关系了。因为这个时候主要取决于H的变化。这就有点像BFS了。
所以按照这个方向优化,我们可以考虑让H代价更准确一点,修改代价函数F = G + W * H。W可以影响H的权重,这里的W可能要根据不同场景进行设置,可以设置所有节点都一样,也可以设置没个节点都不同。或许可以随着搜索的增加不断的增大H,使得搜索空间成一个哑铃的形状。当然这只是个想法,是不是还得实验一下。

代码:https://github.com/GreenArrow2017/PracticeAlgorithm/tree/master/Astar

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