1. 程式人生 > >Astar A*演算法 最短路徑演算法

Astar A*演算法 最短路徑演算法

通常情況下,迷宮尋路演算法可以使用深度優先或者廣度優先演算法,但是由於效率的原因,不會直接使用這些演算法,在路徑搜尋演算法中最常見的就是A*尋路演算法。使用A*演算法的魅力之處在於它不僅能找到地圖中從A到B的一條路徑,還能保證找到的是一條最短路徑,它是一種常見的啟發式搜尋演算法,類似於Dijkstra演算法一樣的最短路徑查詢演算法,很多遊戲應用中的路徑搜尋基本都是採用這種演算法或者是A*演算法的變種。

下面我們來了解一下A*演算法相關的理論知識:

如圖,我們需要在迷宮中找到A點到B點的一條最短的可以通過的路徑,A和B直接被一面牆堵住了。在上一篇部落格中我們說到了,地圖是有二維陣列組成的,牆表示不能通過的地方,用1表示,A*演算法所要做的就是從A找到一條最短的通向B的路徑。當然,不能從牆上飛過去,也不能瞬移到B。只能每次移動一個格子,一步一步地移動到B目標位置。問題在於,每次移動一格的時候,有上下左右四個方向,這裡我們限制物體斜向移動,如何選擇下一個移動方向呢?按照我們的想法,不就是找一條離目標最近的路嗎?那我們可以在這四個方向中,找一個最接近目標點的位置,當然,還要考慮障礙因素,基於這個思想,A*演算法採用了以下的搜尋步驟來實現:

  1.首先把起始位置點加入到一個稱為“open List”的列表,在尋路的過程中,目前,我們可以認為open List這個列表會存放許多待測試的點,這些點是通往目標點的關鍵,以後會逐漸往裡面新增更多的測試點,同時,為了效率考慮,通常這個列表是個已經排序的列表。

  2.如果open List列表不為空,則重複以下工作:

  (1)找出open List中通往目標點代價最小的點作為當前點;

  (2)把當前點放入一個稱為close List的列表;

  (3)對當前點周圍的4個點每個進行處理(這裡是限制了斜向的移動),如果該點是可以通過並且該點不在close List列表中,則處理如下;

  (4)如果該點正好是目標點,則把當前點作為該點的父節點,並退出迴圈,設定已經找到路徑標記;

  (5)如果該點也不在open List中,則計算該節點到目標節點的代價,把當前點作為該點的父節點,並把該節點新增到open List中;

  (6)如果該點已經在open List中了,則比較該點和當前點通往目標點的代價,如果當前點的代價更小,則把當前點作為該點的父節點,同時,重新計算該點通往目標點的代價,並把open List重新排序;

  3.完成以上迴圈後,如果已經找到路徑,則從目標點開始,依次查詢每個節點的父節點,直到找到開始點,這樣就形成了一條路徑。 

 

  以上,就是A*演算法的全部步驟,按照這個步驟,就可以得到一條正確的路徑。這裡有一個關鍵的地方,就是如何計算每個點通往目標點的代價,之所以稱為A*演算法為啟發式搜尋,就是因為通過評估這個代價值來搜尋最近的路徑,對於任意一個點的代價值,在A*演算法中通常使用下列的公式計算:

代價F=G+H

  在這裡,F表示通往目標點的代價,G表示從起始點移動到該點的距離,H則表示從該點到目標點的距離,比如圖中,可以看到小狗的附近格子的代價值,其中左上角的數字代表F值,左下角的數字代表G值,右下角的數字代表H值。拿小狗上方的格子來舉例,G=1,表示從小狗的位置到該點的距離為1個格子,H=6,表示從小狗到骨頭的距離是6個格子,則F=G+H=7。在此處,距離的演算法是採用曼哈頓距離,它計算從當前格子到目的格子之間水平和垂直的方格的數量總和,例如在平面上,座標(x1,y1)的點和座標(x2,y2)的點的曼哈頓距離為:

|x1-x2|+|y1-y2|

當然,距離的演算法也可以採用其他的方法,實際在遊戲中,這個移動的代價除了要考慮距離因素外,還要考慮當前格子的遊戲屬性。比如有的格子表示水路、草地、陸地,這些有可能影響人物移動的速度,實際計算的時候還要把這些考慮在內。

  另一個需要注意的就是,在計算這個距離的時候是毋須考慮障礙因素的,因為在以上A*演算法步驟中會剔除掉障礙。

  這樣,按照前面所說的A*演算法的步驟,第一次迴圈open List的時候,把A點作為當前點,同時把A周圍的四個點放入到open List中。第二次迴圈的時候把A右邊的點作為當前點,該點的父節點就是A,這是處理當前點的時候,只需要把當前點的上下兩個點放入open List中,因為左邊的A已經在close List中,而右邊的是牆,所以直接被忽略。

虛擬碼:

 

具體例子:

 

程式碼:

astar.h

#ifndef ASTAR_H
#define ASTAR_H

#include <vector>
#include <functional>
#include <set>

namespace AStar
{
    struct Vec2i
    {
        int x, y;
        bool operator == (const Vec2i& coordinates_);
    };

    using uint = unsigned int;
    using HeuristicFunction = std::function<uint(Vec2i, Vec2i)>;
    using CoordinateList = std::vector<Vec2i>;

    struct Node
    {
        uint G, H;
        Vec2i coordinates;
        Node *parent;

        Node(Vec2i coord_, Node *parent = nullptr);
        uint getScore();
    };

    using NodeSet = std::set<Node*>;

    class Generator
    {
        // default private
        bool detectCollision(Vec2i coordinates_);
        Node* findNodeOnList(NodeSet &nodes_, Vec2i coordinates_);
        void releaseNodes(NodeSet &nodes_);

    public:
        Generator();
        void setWorldSize(Vec2i worldSize_);
        void setDiagonalMovement(bool enable_);
        void setHeuristic(HeuristicFunction heuristic_);
        CoordinateList findPath(Vec2i source_, Vec2i target_);
        void addCollision(Vec2i coordinates_);
        void removeCollision(Vec2i coordinates_);
        void clearCollision();

    private:
        HeuristicFunction heuristic;
        CoordinateList direction, walls;
        Vec2i worldSize;
        uint directions;
    };

    class Heuristic
    {
    private:
        static Vec2i getDelta(Vec2i source_, Vec2i target_);

    public:
        static uint manhattan(Vec2i source_, Vec2i target_);
        static uint euclidean(Vec2i source_, Vec2i target_);
        static uint octagonal(Vec2i source_, Vec2i target_);
    };
}

#endif // ASTAR_H

 

astar.cpp

#include "astar.h"
#include <algorithm>

using namespace std::placeholders;

bool AStar::Vec2i::operator == (const Vec2i &coordinates_)
{
    return (x == coordinates_.x && y == coordinates_.y);
}

AStar::Vec2i operator + (const AStar::Vec2i& left_, const AStar::Vec2i& right_)
{
    return { left_.x + right_.x, left_.y + right_.y };
}

AStar::Node::Node(Vec2i coordinates_, Node *parent_)
{
    parent = parent_;
    coordinates = coordinates_;
    G = H = 0;
}

AStar::uint AStar::Node::getScore()
{
    return G + H;
}

AStar::Generator::Generator()
{
    setDiagonalMovement(false);
    setHeuristic(&Heuristic::manhattan);
    direction = {
        { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 },
        { -1, -1 }, { 1, 1 }, { -1, 1 }, { 1, -1 }
    };
}

void AStar::Generator::setWorldSize(Vec2i worldSize_)
{
    worldSize = worldSize_;
}

void AStar::Generator::setDiagonalMovement(bool enable_)
{
    directions = (enable_ ? 8 : 4);
}

void AStar::Generator::setHeuristic(HeuristicFunction heuristic_)
{
    heuristic = std::bind(heuristic_, _1, _2);
}

void AStar::Generator::addCollision(Vec2i coordinates_)
{
    walls.push_back(coordinates_);
}

void AStar::Generator::removeCollision(Vec2i coordinates_)
{
    auto it = std::find(walls.begin(), walls.end(), coordinates_);
    if (it != walls.end()) {
        walls.erase(it);
    }
}

void AStar::Generator::clearCollision()
{
    walls.clear();
}

/* key function */
AStar::CoordinateList AStar::Generator::findPath(Vec2i source_, Vec2i target_)
{
    Node *current = nullptr;
    NodeSet openSet, closedSet;
    openSet.insert(new Node(source_));

    while (!openSet.empty())
    {
        current = *openSet.begin();
        for (auto node : openSet) {
            if (node->getScore() <= current->getScore()) {
                current = node;
            }
        }
        if (current->coordinates == target_) {
            break;
        }

        closedSet.insert(current);
        openSet.erase(std::find(openSet.begin(), openSet.end(), current));

        for (uint i = 0; i < directions; ++i) {
            Vec2i newCoordinates(current->coordinates + direction[i]);
            if (detectCollision(newCoordinates) ||
                findNodeOnList(closedSet, newCoordinates)) {
                continue;
            }

            uint totalCost = current->G + ((i < 4) ? 10 : 14);

            Node *successor = findNodeOnList(openSet, newCoordinates);
            if (successor == nullptr) {
                successor = new Node(newCoordinates, current);
                successor->G = totalCost;
                successor->H = heuristic(successor->coordinates, target_);
                openSet.insert(successor);
            }
            else if (totalCost < successor->G) {
                successor->parent = current;
                successor->G = totalCost;
            }
        }
    }
    CoordinateList path;
    while (current != nullptr) {
        path.push_back(current->coordinates);
        current = current->parent;
    }

    releaseNodes(openSet);
    releaseNodes(closedSet);

    return path;
}

AStar::Node* AStar::Generator::findNodeOnList(NodeSet& nodes_, Vec2i coordinates_)
{
    for (auto node : nodes_) {
        if (node->coordinates == coordinates_) {
            return node;
        }
    }
    return nullptr;
}

void AStar::Generator::releaseNodes(NodeSet& nodes_)
{
    for (auto it = nodes_.begin(); it != nodes_.end();) {
        delete *it;
        it = nodes_.erase(it);
    }
}

bool AStar::Generator::detectCollision(Vec2i coordinates_)
{
    if (coordinates_.x < 0 || coordinates_.x >= worldSize.x ||
        coordinates_.y < 0 || coordinates_.y >= worldSize.y ||
        std::find(walls.begin(), walls.end(), coordinates_) != walls.end()) {
        return true;
    }
    return false;
}

AStar::Vec2i AStar::Heuristic::getDelta(Vec2i source_, Vec2i target_)
{
    return{ abs(source_.x - target_.x),  abs(source_.y - target_.y) };
}

AStar::uint AStar::Heuristic::manhattan(Vec2i source_, Vec2i target_)
{
    auto delta = std::move(getDelta(source_, target_));
    return static_cast<uint>(10 * (delta.x + delta.y));
}

AStar::uint AStar::Heuristic::euclidean(Vec2i source_, Vec2i target_)
{
    auto delta = std::move(getDelta(source_, target_));
    return static_cast<uint>(10 * sqrt(pow(delta.x, 2) + pow(delta.y, 2)));
}

AStar::uint AStar::Heuristic::octagonal(Vec2i source_, Vec2i target_)
{
    auto delta = std::move(getDelta(source_, target_));
    return 10 * (delta.x + delta.y) + (-6) * std::min(delta.x, delta.y);
}

 

main.cpp

#include <iostream>
#include "astar.h"
#include <vector>
using namespace std;
int main()
{
    AStar::Generator generator;
    generator.setWorldSize({11, 6});
    generator.setHeuristic(AStar::Heuristic::euclidean);
    generator.setDiagonalMovement(true);

    std::cout << "Generate path ... \n";
    vector<vector<int>> arr = {{3, 1},
                               {3, 2},
                               {4, 2},
                               {5, 2},
                               {6, 2},
                               {7, 2}};
    AStar::Vec2i colli;
    for (size_t i = 0; i < arr.size(); ++i) {
        colli.x = arr[i][0];
        colli.y = arr[i][1];
        generator.addCollision(colli);
    }

    auto path = generator.findPath({7, 4}, {4, 1});

    for(int i = path.size() - 1; i >= 0; --i) {

        std::cout << path[i].x << " " << path[i].y << "\n";
    }
}

執行結果:

實際結果:(程式碼求解與實際結果一致)

 

注:

  • A*演算法是一種啟發式演算法,它的啟發作用(在擴充套件路徑時讓路徑點與start和target之間的距離之和最小)使得其效能比BFS和DFS來尋找最短路徑更優
  • A*演算法由於保持了啟發作用,相當於有一股外力在讓start和target之間的路徑趨向於最短化,因此,在這種啟發下逐步搜尋處的路徑,一定是最短路徑(用曼哈頓距離作為啟發距離則很容易理解,因為在後面從inList中選擇點時,是選擇的F值最小的點,若是曼哈頓距離,則在任意路徑中,F都是一個非減函式,因此每次選擇最小的F值比為最優值,即最短路徑)

 

參考資料:

https://www.youtube.com/watch?v=-L-WgKMFuhE

https://github.com/daancode/a-star

http://www.cnblogs.com/msxh/p/5674417.html