1. 程式人生 > >A*演算法(C++實現)

A*演算法(C++實現)

簡易地圖

如圖所示簡易地圖, 其中綠色方塊的是起點 (用 A 表示), 中間藍色的是障礙物, 紅色的方塊 (用 B 表示) 是目的地. 為了可以用一個二維陣列來表示地圖, 我們將地圖劃分成一個個的小方塊.

二維陣列在遊戲中的應用是很多的, 比如貪吃蛇和俄羅斯方塊基本原理就是移動方塊而已. 而大型遊戲的地圖, 則是將各種"地貌"鋪在這樣的小方塊上.

尋路步驟

1. 從起點A開始, 把它作為待處理的方格存入一個"開啟列表", 開啟列表就是一個等待檢查方格的列表.

2. 尋找起點A周圍可以到達的方格, 將它們放入"開啟列表", 並設定它們的"父方格"為A.

3. 從"開啟列表"中刪除起點 A, 並將起點 A 加入"關閉列表", "關閉列表"中存放的都是不需要再次檢查的方格

 圖中淺綠色描邊的方塊表示已經加入 "開啟列表" 等待檢查. 淡藍色描邊的起點 A 表示已經放入 "關閉列表" , 它不需要再執行檢查.

 從 "開啟列表" 中找出相對最靠譜的方塊, 什麼是最靠譜? 它們通過公式 F=G+H 來計算.

 F = G + H

表示從起點 A 移動到網格上指定方格的移動耗費 (可沿斜方向移動).

表示從指定的方格移動到終點 B 的預計耗費 (H 有很多計算方法, 這裡我們設定只可以上下左右移動).

我們假設橫向移動一個格子的耗費為10, 為了便於計算, 沿斜方向移動一個格子耗費是14. 為了更直觀的展示如何運算 FGH, 圖中方塊的左上角數字表示 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 不是一個明智的選擇, 因為它需要更遠的路, 這時我們什麼也不做.

 如圖, 我們選中了 C 因為它的 F 值最小, 我們把它從 "開啟列表" 中刪除, 並把它加入 "關閉列表". 它右邊上下三個都是牆, 所以不考慮它們. 它左邊是起始方塊, 已經加入到 "關閉列表" 了, 也不考慮. 所以它周圍的候選方塊就只剩下 4 個. 讓我們來看看 C 下面的那個格子, 它目前的 G 是14, 如果通過 C 到達它的話, G將會是 10 + 10, 這比 14 要大, 因此我們什麼也不做.

然後我們繼續從 "開啟列表" 中找出 F 值最小的, 但我們發現 C 上面的和下面的同時為 54, 這時怎麼辦呢? 這時隨便取哪一個都行, 比如我們選擇了 C 下面的那個方塊 D.

 D 右邊已經右上方的都是牆, 所以不考慮, 但為什麼右下角的沒有被加進 "開啟列表" 呢? 因為如果 C 下面的那塊也不可以走, 想要到達 C 右下角的方塊就需要從 "方塊的角" 走了, 在程式中設定是否允許這樣走. (圖中的示例不允許這樣走)

就這樣, 我們從 "開啟列表" 找出 F 值最小的, 將它從 "開啟列表" 中移掉, 新增到 "關閉列表". 再繼續找出它周圍可以到達的方塊, 如此迴圈下去...

那麼什麼時候停止呢? —— 當我們發現 "開始列表" 裡出現了目標終點方塊的時候, 說明路徑已經被找到.

如何找回路徑

如上圖所示, 除了起始方塊, 每一個曾經或者現在還在 "開啟列表" 裡的方塊, 它都有一個 "父方塊", 通過 "父方塊" 可以索引到最初的 "起始方塊", 這就是路徑.

將整個過程抽象

把起始格新增到 "開啟列表" 
do 
{ 
        尋找開啟列表中F值最低的格子, 我們稱它為當前格. 
        把它切換到關閉列表. 
        對當前格相鄰的8格中的每一個 
        if (它不可通過 || 已經在 "關閉列表" 中) 
        { 
                什麼也不做. 
        } 
        if (它不在開啟列表中) 
        { 
                把它新增進 "開啟列表", 把當前格作為這一格的父節點, 計算這一格的 FGH 
        }
        if (它已經在開啟列表中) 
        { 
                if (用 G 值為參考檢查新的路徑是否更好, 更低的G值意味著更好的路徑) 
                { 
                        把這一格的父節點改成當前格, 並且重新計算這一格的 GF 值. 
                } 
        }
} while( 目標格已經在 "開啟列表", 這時候路徑被找到) 
如果開啟列表已經空了, 說明路徑不存在.

最後從目標格開始, 沿著每一格的父節點移動直到回到起始格, 這就是路徑.

C++實現程式碼:

版本1:

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 isIgnoreCorner);

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)) == 1 ? kCost1 : kCost2;
	int parentG = point->parent == NULL ? 0 : point->parent->G; //如果是初始節點,則其父節點是空
	return parentG + extraG;
}

int Astar::calcH(Point *point, Point *end)
{
	//用簡單的歐幾里得距離計算H,這個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,找到當前周圍八個格中可以通過的格子
		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(target, &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
		|| maze[target->x][target->y] == 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"
using namespace std;

int main()
{
	//初始化地圖,用二維矩陣代表地圖,1表示障礙物,0表示可通
	vector<vector<int>> maze = {
		{ 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(maze);

	//設定起始和結束點
	Point start(1, 1);
	Point end(6, 10);
	//A*演算法找尋路徑
	list<Point *> path = astar.GetPath(start, end, false);
	//列印
	for (auto &p : path)
		cout << '(' << p->x << ',' << p->y << ')' << endl;

	system("pause");
	return 0;
}

版本2:

Astar.h

#ifndef ASTAR_H
#define ASTAR_H
#include <iostream>
#include <queue>
#include <vector>
#include <stack>
#include<algorithm>
using namespace std;

typedef struct Node
{
	int x, y;
	int g; //起始點到當前點實際代價
	int h;//當前節點到目標節點最佳路徑的估計代價
	int f;//估計值
	Node* father;
	Node(int x, int y)
	{
		this->x = x;
		this->y = y;
		this->g = 0;
		this->h = 0;
		this->f = 0;
		this->father = NULL;
	}
	Node(int x, int y, Node* father)
	{
		this->x = x;
		this->y = y;
		this->g = 0;
		this->h = 0;
		this->f = 0;
		this->father = father;
	}
}Node;
class Astar{
public:
	Astar();
	~Astar();
	void search(Node* startPos, Node* endPos);

	void checkPoit(int x, int y, Node* father, int g);
	void NextStep(Node* currentPoint);
	int isContains(vector<Node*>* Nodelist, int x, int y);
	void countGHF(Node* sNode, Node* eNode, int g);
	static bool compare(Node* n1, Node* n2);
	bool unWalk(int x, int y);
	void printPath(Node* current);
	void printMap();
	vector<Node*> openList;
	vector<Node*> closeList;
	Node *startPos;
	Node *endPos;
	static const int WeightW = 10;// 正方向消耗
	static const int WeightWH = 14;//打斜方向的消耗
	static const int row = 6;
	static const int col = 8;
};
#endif

Astar.cpp

#include "Astar.h"
int map[101][101] =
{
	{ 0, 0, 0, 1, 0, 1, 0, 0, 0 },
	{ 0, 0, 0, 1, 0, 1, 0, 0, 0 },
	{ 0, 0, 0, 0, 0, 1, 0, 0, 0 },
	{ 0, 0, 0, 1, 0, 1, 0, 1, 0 },
	{ 0, 0, 0, 1, 0, 1, 0, 1, 0 },
	{ 0, 0, 0, 1, 0, 0, 0, 1, 0 },
	{ 0, 0, 0, 1, 0, 0, 0, 1, 0 }
};
Astar::Astar()
{

}
Astar::~Astar()
{

}

void Astar::search(Node* startPos, Node* endPos)
{
	if (startPos->x < 0 || startPos->x > row || startPos->y < 0 || startPos->y >col ||
		endPos->x < 0 || endPos->x > row || endPos->y < 0 || endPos->y > col)
		return;
	Node* current;
	this->startPos = startPos;
	this->endPos = endPos;
	openList.push_back(startPos);
	//主要是這塊,把開始的節點放入openlist後開始查詢旁邊的8個節點,如果座標超長範圍或在closelist就return 如果已經存在openlist就對比當前節點到遍歷到的那個節點的G值和當前節點到原來父節點的G值 如果原來的G值比較大 不用管 否則重新賦值G值 父節點 和f 如果是新節點 加入到openlist 直到opellist為空或找到終點
	while (openList.size() > 0)
	{
		current = openList[0];
		if (current->x == endPos->x && current->y == endPos->y)
		{
			cout << "find the path" << endl;
			printMap();
			printPath(current);
			openList.clear();
			closeList.clear();
			break;
		}
		NextStep(current);
		closeList.push_back(current);
		openList.erase(openList.begin());
		sort(openList.begin(), openList.end(), compare);
	}
}

void Astar::checkPoit(int x, int y, Node* father, int g)
{
	if (x < 0 || x > row || y < 0 || y > col)
		return;
	if (this->unWalk(x, y))
		return;
	if (isContains(&closeList, x, y) != -1)
		return;
	int index;
	if ((index = isContains(&openList, x, y)) != -1)
	{
		Node *point = openList[index];
		if (point->g > father->g + g)
		{
			point->father = father;
			point->g = father->g + g;
			point->f = point->g + point->h;
		}
	}
	else
	{
		Node * point = new Node(x, y, father);
		countGHF(point, endPos, g);
		openList.push_back(point);
	}
}

void Astar::NextStep(Node* current)
{
	checkPoit(current->x - 1, current->y, current, WeightW);//左
	checkPoit(current->x + 1, current->y, current, WeightW);//右
	checkPoit(current->x, current->y + 1, current, WeightW);//上
	checkPoit(current->x, current->y - 1, current, WeightW);//下
	checkPoit(current->x - 1, current->y + 1, current, WeightWH);//左上
	checkPoit(current->x - 1, current->y - 1, current, WeightWH);//左下
	checkPoit(current->x + 1, current->y - 1, current, WeightWH);//右下
	checkPoit(current->x + 1, current->y + 1, current, WeightWH);//右上
}

int Astar::isContains(vector<Node*>* Nodelist, int x, int y)
{
	for (int i = 0; i < Nodelist->size(); i++)
	{
		if (Nodelist->at(i)->x == x && Nodelist->at(i)->y == y)
		{
			return i;
		}
	}
	return -1;
}

void Astar::countGHF(Node* sNode, Node* eNode, int g)
{
	int h = (abs(sNode->x - eNode->x) + abs(sNode->y - eNode->y)) * WeightW;
	int currentg = sNode->father->g + g;
	int f = currentg + h;
	sNode->f = f;
	sNode->h = h;
	sNode->g = currentg;
}

bool Astar::compare(Node* n1, Node* n2)
{
	//printf("%d,%d",n1->f,n2->f);
	return n1->f < n2->f;
}

bool Astar::unWalk(int x, int y)
{
	if (map[x][y] == 1)
		return true;
	return false;
}

void Astar::printPath(Node* current)
{
	if (current->father != NULL)
		printPath(current->father);
	printf("(%d,%d)", current->x, current->y);
}

void Astar::printMap()
{
	for (int i = 0; i <= row; i++){
		for (int j = 0; j <= col; j++){
			printf("%d ", map[i][j]);
		}
		printf("\n");
	}
}

main.cpp

#include "Astar.h"

int main(int argc, char* argv[])
{
	Astar astar;
	Node *startPos = new Node(5, 1);
	Node *endPos = new Node(3, 8);
	astar.search(startPos, endPos);
	getchar();
	return 0;
}

參考: