A*算法

一、A*算法

(一)算法思路

A*算法通过下面这个函数来计算每个节点的优先级。

f(n)=g(n)+h(n)

其中:

  • f(n) 是节点n的综合优先级。当我们选择下一个要遍历的节点时,我们总会选取综合优先级最高(值最小)的节点。
  • g(n) 是节点n距离起点的代价。
  • h(n) 是节点n距离终点的预计代价,这也就是A*算法的启发函数。

A* 算法在运算过程中,每次从优先队列中选取 f(n) 值最小(优先级最高)的节点作为下一个待遍历的节点。

另外,A*算法使用两个集合来表示待遍历的节点已经遍历过的节点,这通常称之为open_setclose_set

(二)图片展示

二、启发函数

(一)启发函数影响

启发函数会影响A*算法的行为。

  • 在极端情况下,当启发函数h(n)始终为0,则将由g(n)决定节点的优先级,此时算法就退化成了Dijkstra算法。
  • 如果h(n)始终小于等于节点n到终点的代价,则A* 算法保证一定能够找到最短路径。但是当h(n)的值越小,算法将遍历越多的节点,也就导致算法越慢。
  • 如果h(n)完全等于节点n到终点的代价,则A* 算法将找到最佳路径,并且速度很快。可惜的是,并非所有场景下都能做到这一点。因为在没有达到终点之前,我们很难确切算出距离终点还有多远。
  • 如果h(n)的值比节点n到终点的代价要大,则A*算法不能保证找到最短路径,不过此时会很快。 在另外一个极端情况下,如果h(n)相较于g(n)大很多,则此时只有h(n)产生效果,这也就变成了最佳优先搜索。

(二)启发函数使用

对于网格形式的图,有以下这些启发函数可以使用

  • 如果图形中只允许朝上下左右四个方向移动,则可以使用曼哈顿距离(Manhattan distance)。
  • 如果图形中允许朝八个方向移动,则可以使用对角距离。
  • 如果图形中允许朝任何方向移动,则可以使用欧几里得距离(Euclidean distance)。

(三)关于距离

1.曼哈顿距离

如果图形中只允许朝上下左右四个方向移动,则启发函数可以使用曼哈顿距离,它的计算方法如下图所示:

计算曼哈顿距离的函数如下,这里的LINECOST是指两个相邻节点之间的直线移动代价,通常是一个固定的常数。

function heuristic(node) =
    dx = abs(node.x - goal.x)
    dy = abs(node.y - goal.y)
    return LINECOST * (dx + dy)

2.对角距离

如果图形中允许斜着朝邻近的节点移动,则启发函数可以使用对角距离。它的计算方法如下:

计算对角距离的函数如下,这里的SLASHCOST指的是两个斜着相邻节点之间的移动代价。

function heuristic(node) =
    dx = abs(node.x - goal.x)
    dy = abs(node.y - goal.y)
    return LINECOST * (dx + dy) + (SLASHCOST - 2 * LINECOST) * min(dx, dy)

3.欧几里得距离

如果图形中允许朝任意方向移动,则可以使用欧几里得距离。

欧几里得距离是指两个节点之间的直线距离,因此其计算方法也是我们比较熟悉的:

function heuristic(node) =
    dx = abs(node.x - goal.x)
    dy = abs(node.y - goal.y)
    return COST * sqrt(dx * dx + dy * dy)

三、A*算法最优性问题的提出及证明

If h(n) is always lower than (or equal to) the cost of moving from n to the goal, then A* is guaranteed to find a shortest path.
The lower h(n) is, the more node A* expands, making it slower.

以上证明步骤,应该将5、6调换顺序看比较通顺。因为假设了g(s)是次优的,所以一定有一个更优的状态s'在open表中,于是有6的推断。但是由于选择的是s而不是s',所以又有5的推断,两者相互矛盾,所以g(s)一定是最优的。

四、实现

#include <iostream>
#include <vector>
using namespace std;
//行数 Y轴
#define ROWS  10
//列数 X轴
#define COLS  10

//直线代价
#define LINECOST  10
//斜线代价
#define SLASHCOST  14
//点结构  点类
struct MyPoint {
    int y, x;

    int f, g, h;
};

//枚举类型   就是为了代码可读性
enum dir { p_up, p_down, p_left, p_right, p_lup, p_ldown, p_rup, p_rdown };


//树节点类型
struct TreeNode {
    MyPoint             pos;
    TreeNode* parent;
    vector<TreeNode*> child;

    TreeNode(MyPoint p) {
        pos.y = p.y;
        pos.x = p.x;
        pos.g = p.g;
        parent = NULL;
    }
};

//计算H值并返回
int getManhattan(MyPoint pos, MyPoint end) {
    int x = ((pos.x > end.x) ? (pos.x - end.x) : (end.x - pos.x));
    int y = ((pos.y > end.y) ? (pos.y - end.y) : (end.y - pos.y));
    return (LINECOST * (x + y));
}

int main() {
    //二维数组描述地图
    int totalMap[ROWS][COLS] = {//0表示路  1表示障碍
        { 0, 0, 0, 0, 1,  0, 0, 0, 0, 0 },
        { 0, 0, 1, 1, 1,  0, 0, 0, 0, 0 },
        { 0, 0, 1, 1, 1,  0, 0, 0, 0, 0 },
        { 0, 0, 1, 0, 1,  0, 0, 0, 0, 0 },
        { 0, 0, 1, 0, 1,  0, 0, 0, 0, 0 },

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

    MyPoint begPos = { 0, 0 };
    MyPoint endPos = { 7, 6 };

    //二维数组记录是否走过
    // 0 false 表示没有走过  1 true 走过
    bool pathMap[ROWS][COLS] = { 0 };//所有的点都是0 都没有走过
    //标记起点走过
    pathMap[begPos.y][begPos.x] = true;

    //创建一颗空树
    TreeNode* rootNode = NULL;
    rootNode = new TreeNode(begPos);//起点入树

    //准备一个数组  用来找最小的f
    vector<TreeNode*> buff;

    //当前点
    TreeNode* pCurrent = rootNode;
    TreeNode* pChild = NULL;

    bool isFindEnd = false;

    while (1) {
        //1 把八点都做出来
        for (int i = 0; i < 8; i++) {
            pChild = new TreeNode(pCurrent->pos);
            switch (i)
            {
            case p_up:
                pChild->pos.y--;
                pChild->pos.g += LINECOST;
                break;
            case p_down:
                pChild->pos.y++;
                pChild->pos.g += LINECOST;
                break;
            case p_left:
                pChild->pos.x--;
                pChild->pos.g += LINECOST;
                break;
            case p_right:
                pChild->pos.x++;
                pChild->pos.g += LINECOST;
                break;
            case p_lup:
                pChild->pos.x--;
                pChild->pos.y--;
                pChild->pos.g += SLASHCOST;
                break;
            case p_rup:
                pChild->pos.x++;
                pChild->pos.y--;
                pChild->pos.g += SLASHCOST;
                break;
            case p_ldown:
                pChild->pos.x--;
                pChild->pos.y++;
                pChild->pos.g += SLASHCOST;
                break;
            case p_rdown:
                pChild->pos.x++;
                pChild->pos.y++;
                pChild->pos.g += SLASHCOST;
                break;
            }


            if (pChild->pos.y >= 0 && pChild->pos.y < ROWS &&
                pChild->pos.x >= 0 && pChild->pos.x < COLS &&
                //没有走过
                pathMap[pChild->pos.y][pChild->pos.x] == false &&
                //不是障碍
                totalMap[pChild->pos.y][pChild->pos.x] == 0
                ) {//能走
                //2 算出来h 和f值 放到树里 放到buff里
                pChild->pos.h = getManhattan(pChild->pos, endPos);//算出来h
                //算出来f
                pChild->pos.f = pChild->pos.g + pChild->pos.h;
                //放到buff里
                buff.push_back(pChild);
                //入树
                pCurrent->child.push_back(pChild);
                pChild->parent = pCurrent;
            }
            else {//不能走
                delete pChild;
            }
        }

        vector<TreeNode*>::iterator it;
        vector<TreeNode*>::iterator itMin;
        //3 找出buff中 f最小的点  走  删掉
        itMin = buff.begin();//假设第一个最小
        for (it = buff.begin(); it != buff.end(); it++) {//遍历整个buff数组
            itMin = (((*itMin)->pos.f < (*it)->pos.f) ? itMin : it);
        }
        //
        pCurrent = *itMin;
        //删掉
        buff.erase(itMin);
        //4 如果找到了终点 循环结束
        if (pCurrent->pos.y == endPos.y &&
            pCurrent->pos.x == endPos.x) {
            isFindEnd = true;
            break;
        }
        //5 buff空了 循环结束
        if (buff.empty() == true) {
            break;
        }
    }

    //输出路径 看一看

    if (isFindEnd) {
        cout << "找到终点了:";

        while (pCurrent) {
            cout << "(" << pCurrent->pos.y << "," <<
                pCurrent->pos.x << ")";
            pCurrent = pCurrent->parent;
        }

        cout << endl;
    }
    else {
        cout << "没有找到路径" << endl;
    }
    return 0;
}

五、Dijkstra算法和A*算法

Dijkstra算法和A*都是最短路径问题的常用算法,下面就对这两种算法的特点进行一下比较。

  1. Dijkstra算法计算源点到其他所有点的最短路径长度,A*关注点到点的最短路径(包括具体路径)。
  2. Dijkstra算法建立在较为抽象的图论层面,A*算法可以更轻松地用在诸如游戏地图寻路中。
  3. Dijkstra算法的实质是广度优先搜索,是一种发散式的搜索,所以空间复杂度和时间复杂度都比较高。对路径上的当前点,A*算法不但记录其到源点的代价,还计算当前点到目标点的期望代价,是一种启发式算法,也可以认为是一种深度优先的算法。
  4. 由第一点,当目标点很多时,A*算法会带入大量重复数据和复杂的估价函数,所以如果不要求获得具体路径而只比较路径长度时,Dijkstra算法会成为更好的选择。
posted @ 2023-02-14 15:53  ImreW  阅读(69)  评论(0编辑  收藏  举报