路径规划中A*算法的基本原理和C++代码实现

1.前言

    在机器人运动过程中的最优路径的选择的算法有Dijkstra和A*算法,其中A*算法是Dijkstra算法的一种改进策略.适用于已知起点和终点,求起点到终点的最短距离.对于不知道终点缺需要寻找最短路径的问题仍然使用Dijkstra算法具有普适性和天然优势.

2.A*算法迭代案例


图1.简易地图.

如图所示为一张简易地图,其中绿色方块是起点(A表示),中间蓝色的是障碍物,红色的方块(B表示)是目的地.为了方便,用一个二维数组表示地图,我们将地图划分成一个个的小方块.

二维数组在游戏中的应用是很多的,比如贪吃蛇和俄罗斯方块基本原理就是移动方块.而大型游戏的地图,则将各种“地貌”铺在这样的小方块上.

寻路步骤

1. 从起点A开始,把它作为待处理的方格存入一个“开启列表”,开启列表就是一个等待检查方格的列表.

2.寻找起点A周围可以到达的方格,将它们放入“开启列表”,并设置它们的“父方格”为A.

3.从“开启列表”中删除起点A,并将起点A放入“关闭列表”,关闭列表中存放的都是不需要再次检查的顶点.


图2.八邻域行走方向.

    图中浅绿色描边的方块表示已经加入“开启列表”等待检查.淡蓝色描边的起点A表示已经放入“关闭列表”,它不需要再执行检查.从“开启列表”中找出相对最靠谱的方块,什么是最靠谱?它们通过公式\boldsymbol{F}=\boldsymbol{G}+\boldsymbol{H}来计算.

\boldsymbol{G}表示从起点A移动到网格上指定方格的移动耗费(可沿斜方向移动).

\boldsymbol{H}表示从指定的方格移动到终点B的预计耗费(\boldsymbol{H}有很多计算方法,这里设定为“曼哈顿距离”).


图3.左上角为F之,左下角为G值,右下角为H值.

    从“开启列表”中选择F值最低的方格C(绿色起始块A右边的方块),然后对它进行如下处理:

4.把它从“开启列表”中删除,并加入“关闭列表”中.

5.检查它所有相邻并且可以到达(障碍物和“关闭列表”的方格都不考虑)的方格.如果这些方格还不在“开启列表”中的话,将它们加入“开启列表”,计算这些方格的G,H和F值各是多少,并设置它们的“父节点”为C.

6.如果某个相邻方格D已经在“开启列表”中,检查如果用新的路径(就是经过C的路径)到达它的话,G值是否会更低一些,如果新的G值更低,那就把它的“父方格”改为目前选中的方格C,然后重新计算它的F和G值(H值不需要重新计算,因为对于每个方块,H值是不改变的).如果新的G值比较高,就说明经过C再到D不是一个明智的选择,因为它需要更远的路,这时我们什么也不做.


图4.路径上的第二顶点C的八邻域顶点情况.

如图4,我们选中了C因为它的F值最小,我们把它从“开启列表”中删除,并把它加入“关闭列表”.它右边上下三个都是墙,所以不考虑它们.它左边是起始方块,已经加入到“关闭列表”了,也不考虑.所以它周围的候选方块就只剩4个.然后我们看看C下面的那个格子,它目前的G是14,如果通过C到达它的话,G将会是10+10,这比14要大,所以我们什么也不做.

然后我们继续从“开启列表”中找出F值最小的,但是我们发现C上面和下面的同时为54,这时怎么办?这时随便取哪一个都行,比如选C下面的方块D.


图5.路径上第三个顶点的八邻域情况.

D右边以及右上角都是墙,所以不考虑,但是为什么右下角没有被加入“开启列表”?因为如果它下面的那块也不可以走,想要到达C右下角就需要从“方块的角”走了,在程序中设置是否允许这样走.(图中的示例不允许这样走).


图6.顶点不断从“开启列表”移除,并添加到“关闭列表”中循环执行.

就这样,我们从“开启列表”找出F值最小的,将它从“开启列表”中移除,添加到“关闭列表”.再继续找出它周围可以到达的方块,如此循环下去...

那么什么售后停止呢?——当我们发现“开始列表”中出现了目标终点方块的时候,说明路径已经找到了.

如何找回路径

图7.起点到终点的最短路径.

如上图所示,除了起始方块,每一个曾经或者现在还在“开启列表”中的方块,它都有一个“父方块”,通过“父方块”可以索引到最初的“起始方块”,这是路径.

3.A*算法伪代码


图8.A*算法伪代码.

4.A*算法C++代码实现

Astar.h文件:

#pragma once

/*

// A*算法对象类

*/

#include<vector>

#include<list>

const int kCost1 = 10; //直移一格消耗

const int kCost2 = 14; //斜移一格消耗

struct Point {

int x, y; //点坐标,这里为了方便按照C++的数组来计算,x代表横排,y代表竖列

int F, G, H; //F = G + H

Point* parent; //parent的坐标,这里没有用指针,从而简化代码

Point(int _x, int _y) : x(_x), y(_y), F(0), G(0), H(0), parent(NULL) {

}

};

class Astar {

public:

void InitAstar(std::vector<std::vector<int>>& _maze);

std::list<Point*> GetPath(Point& startPoint, Point& endPoint, bool isIgnoreConer);

private:

Point *findPath(Point& startPoint, Point& endPoint, bool isIgnoreCorner);

std::vector<Point*>getSurroundPoints(const Point* point, bool isIgnoreCorner) const;

bool isCanreach(const Point* point, const Point* target, bool isIgnoreCorner) const; //判断某点是否可以用于下一步判断

Point *isInList(const std::list<Point*>& list, const Point *point)const;//判断开启/关闭列表中是否包含某点

Point* getLeastFpoint(); //从开启列表中返回F值最小的节点

//计算FGH值

int calcG(Point *temp_start, Point *point);

int calcH(Point* point, Point* end);

int calcF(Point* point);

private:

std::vector<std::vector<int>> maze;

std::list<Point *> openList; //开启列表

std::list<Point*> closeList; //关闭列表

};


Astar.cpp文件:

#include<math.h>

#include "Astar.h"

void Astar::InitAstar(std::vector<std::vector<int>>& _maze) {

maze = _maze;

}

int Astar::calcG(Point* temp_start, Point* point) {

int extraG = (abs(point->x - temp_start->x) + abs(point->y - temp_start->y));

int parentG = point->parent == NULL ? 0 : point->parent->G; //如果是初始节点,则其父节点为空

return parentG + extraG;

}

int Astar::calcH(Point* point, Point* end) {

//用简单的欧几里得距离计算,这个H的计算是关键,还有很多算法

return sqrt((double)(end->x - point->x) * (double)(end->x - point->x) + (double)(end->y - point->y) * (double)(end->y - point->y) * kCost1);

}

int Astar::calcF(Point* point) {

return point->G + point->H;

}

Point* Astar::getLeastFpoint() {

if (!openList.empty()) {

auto resPoint = openList.front();

for (auto& point : openList)

if (point->F < resPoint->F)

resPoint = point;

return resPoint;

}

return NULL;

}

Point* Astar::findPath(Point &startPoint, Point &endPoint, bool isIgnoreCorner) {

openList.push_back(new Point(startPoint.x, startPoint.y)); //置入起点,拷贝开辟一个节点,内外隔离

while (!openList.empty()) {

auto curPoint = getLeastFpoint(); //找到F值最小的点

openList.remove(curPoint);        //从开启列表中删除

closeList.push_back(curPoint);    //放到关闭列表

// 1. 找到当前周围8个格中可以通过的格子

auto surroundPoints = getSurroundPoints(curPoint, isIgnoreCorner);

for (auto& target : surroundPoints) {

// 2. 对某一个格子,如果它不在开启列表中,加入到开启列表,设置当前格为其父节点,计算F G H

if (!isInList(openList, target)) {

target->parent = curPoint;

target->G = calcG(curPoint, target);

target->H = calcH(curPoint, &endPoint);

target->F = calcF(target);

openList.push_back(target);

}

// 3. 对某一格子,它在开启列表中,计算G值,如果比原来的大,就什么都不做,否则设置它的父节点为当前点,并更新G和F

else {

int tempG = calcG(curPoint, target);

if (tempG < target->G) {

target->parent = curPoint;

target->G = tempG;

target->F = calcF(target);

}

}

Point* resPoint = isInList(openList, &endPoint);

if (resPoint)

return resPoint; //返回列表中的节点指针,不要用原来传入的endpoint指针,因为发生了深拷贝

}

}

return NULL;

}

std::list<Point*> Astar::GetPath(Point& startPoint, Point& endPoint, bool isIgnoreCorner) {

Point* result = findPath(startPoint, endPoint, isIgnoreCorner);

std::list<Point*> path;

// 返回路径,如果没找到路径,返回空链表

while (result) {

path.push_front(result);

result = result->parent;

}

// 清空临时开闭列表,防止重复执行GetPath导致结果异常

openList.clear();

closeList.clear();

return path;

}

Point* Astar::isInList(const std::list<Point*>& list, const Point* point) const {

// 判断某个节点是否在列表中,这里不能比较指针,因为每次加入列表是新开辟的节点,只能比较坐标

for (auto p : list)

if (p->x == point->x && p->y == point->y)

return p;

return NULL;

}

bool Astar::isCanreach(const Point* point, const Point* target, bool isIgnoreCorner) const {

if (target->x < 0 || target->x > maze.size() - 1

|| target->y < 0 || target->y > maze[0].size() - 1

|| target->x == point->x && target->y == point->y

|| isInList(closeList, target))//如果点与当前节点重合、超出地图、是障碍物、或者在关闭列表中,返回false

return false;

else {

if (abs(point->x - target->x) + abs(point->y - target->y) == 1)//非斜角可以

return true;

else {

//斜对角要判断是否绊住

if (maze[point->x][target->y] == 0 && maze[target->x][point->y] == 0)

return true;

else

return isIgnoreCorner;

}

}

}

std::vector<Point*> Astar::getSurroundPoints(const Point* point, bool isIgnoreCorner) const {

std::vector<Point*> surroundPoints;

for (int x = point->x - 1; x <= point->x + 1; x++)

for (int y = point->y - 1; y <= point->y + 1; y++)

if (isCanreach(point, new Point(x, y), isIgnoreCorner))

surroundPoints.push_back(new Point(x, y));

return surroundPoints;

}


main.cpp文件:

#include <iostream>

#include "Astar.h"

bool InPath(const int& row, const int& col, const std::list<Point*>& path) {

for (const auto& p : path) {

if (row == p->x && col == p->y) {

return true;

}

}

return false;

}

int main() {

// 初始化地图,用二维数组代表地图,1表示障碍物,0表示可通

std::vector<std::vector<int>> map = { {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},

  {1, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1},

  {1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1},

  {1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1},

  {1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1},

  {1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1},

  {1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1},

  {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1} };

Astar astar;

astar.InitAstar(map);

//设置起点和结束点

Point start(1, 1);

Point end(6, 10);

//A*算法找寻路径

std::list<Point*> path = astar.GetPath(start, end, false);

//打印结束

for (auto& p : path) {

std::cout << "(" << p->x << "," << p->y << ")";

}

std::cout << "\n";

for (int row = 0; row < map.size(); row++) {

for (int col = 0; col < map[0].size(); col++) {

if (InPath(row, col, path)) {

if (map[row][col] != 0) {

std::cout << "e ";

}

else {

std::cout << "*";

}

}

else {

std::cout << map[row][col] << " ";

}

}

std::cout << "\n";

}

return 0;

}

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

推荐阅读更多精彩内容