一,說在前面的話
大概在半年前,看見一到信息競賽題:在任意方格陣中設置障礙物,確定起始點后,求這兩點之間路徑。當時覺得蠻有意思的,但是沒有時間去做,今天花了兩個小時來實現它。據說有一個更高級的尋路算法叫做a*, 那我就把我的算法叫做W*。
這個算法主要用於解迷宮和實現戰棋游戲(SLG)的尋路。
首先講一講我的算法的思路:
我們先確定起始點,然后從起點出發,按一定順序判斷這個位置上下左右是否有可走的位置,如果發現有可走的位置,則遞歸進入該位置的判斷。在遞歸的同時記錄所走的路線。當發現某個位置無路可走,則刪除路線的最后一個位置並返回上級位置進行判斷。如此反復嘗試最終找到路線。
說了這么多,就來講解一下代碼吧。
二,講解部分
包含頭文件(全部都是stl中的):
#include <map> #include <vector> #include <iostream>
為幾個冗長的類型重命名,用來使后來的代碼更明了。
typedef unsigned int uint; typedef std::vector<int> CRow; //相當於把CLabyrinth定義成一個整型的二維數組 typedef std::vector<CRow> CLabyrinth;
定義一個類類型表示二維數組中的位置:
class CPoint { public: int col; //列 int row; //行 public: //構造函數,接受行和列的初始化 CPoint(int c = 0, int r = 0) : col(c) , row(r) { return; } //賦值操作 CPoint& operator=(const CPoint& pt) { col = pt.col; row = pt.row; return *this; } //比較操作 bool operator==(const CPoint& pt) { return col == pt.col && row == pt.row; } //判斷該位置是否合法 bool allRight() { return col >= 0 && row >= 0; } }; typedef std::vector<CPoint> CRoute;
然后到了核心類類型CLabyrinthAI
{ protected: //裝有迷宮數據的二維數組 CLabyrinth m_xLabyrinth; //起點位置 CPoint m_ptBeginning; //終點位置 CPoint m_ptEnding; //記錄路線的數組 CRoute m_vRoute; public: //枚舉表示起點、終點的值 enum{Beginning = -1, Ending = -2}; //枚舉表示障礙物與可走區的值 enum{CanntGo = 0, CanGo = 1}; //枚舉是否找到終點 enum{FoundEnding = 0, NotFoundEnding = 1}; protected: //判斷某個位置是否已在路線數組中,用於別走重復的路 bool isRepeat(const CPoint& pt) { bool bRes = false; CRoute::iterator it = m_vRoute.begin(); for(; it != m_vRoute.end(); it++){ CPoint pt0 = *it; if(pt0 == pt){ bRes = true; break; } } return bRes; } //將某一位置加入路線數組 void advance(const CPoint& ptTo) { m_vRoute.push_back(ptTo); } //將路線數組最后一個位置彈出 void back() { m_vRoute.pop_back(); } //判斷某一位置是否是起點 bool isBeginning(const CPoint& pt) { return m_ptBeginning == pt; } //判斷某一位置是否是終點 bool isEnding(const CPoint& pt) { return m_ptEnding == pt; } /*-----------------核心算法------------------------*/ //判斷某一位置是否可以向上移動 CPoint canUp(const CPoint& ptCurrent) //接受當前位置 { CPoint ptRes = CPoint(-1, -1); int col = ptCurrent.col; int row = ptCurrent.row; if(row > 0){ CPoint ptNext = CPoint(col, row - 1); //上移后位置 //檢查上移后位置是否已經走過,以免尋路過程中繞圈子進入死循環 if(!isRepeat(ptNext)){ //獲得迷宮二維數組中上移后位置的屬性(起點、終點、可走、障礙) int nAttr = m_xLabyrinth[ptNext.row][ptNext.col]; //如果上移后位置為可走或到達終點,則設定返回值為上移后的位置 if(nAttr == CanGo || nAttr == Ending){ ptRes = ptNext; } } } return ptRes; //如果上移后位置不可走則返回非法的位置 } //以下判斷某一位置可否移動的原理大致與上相同,就不多說了 //判斷某一位置是否可以向下移動 CPoint canDown(const CPoint& ptCurrent) { CPoint ptRes = CPoint(-1, -1); int col = ptCurrent.col; int row = ptCurrent.row; if(row < m_xLabyrinth.size() - 1){ CPoint ptNext = CPoint(col, row + 1); if(!isRepeat(ptNext)){ int nAttr = m_xLabyrinth[ptNext.row][ptNext.col]; if(nAttr == CanGo || nAttr == Ending){ ptRes = ptNext; } } } return ptRes; } //判斷某一位置是否可以向左移動 CPoint canLeft(const CPoint& ptCurrent) { CPoint ptRes = CPoint(-1, -1); int col = ptCurrent.col; int row = ptCurrent.row; if(col > 0){ CPoint ptNext = CPoint(col - 1, row); if(!isRepeat(ptNext)){ int nAttr = m_xLabyrinth[ptNext.row][ptNext.col]; if(nAttr == CanGo || nAttr == Ending){ ptRes = ptNext; } } } return ptRes; } //判斷某一位置是否可以向右移動 CPoint canRight(const CPoint& ptCurrent) { CPoint ptRes = CPoint(-1, -1); int col = ptCurrent.col; int row = ptCurrent.row; if(col < m_xLabyrinth[0].size() - 1){ CPoint ptNext = CPoint(col + 1, row); if(!isRepeat(ptNext)){ int nAttr = m_xLabyrinth[ptNext.row][ptNext.col]; if(nAttr == CanGo || nAttr == Ending){ ptRes = ptNext; } } } return ptRes; } /* *判斷某一位置是否可以向四周移動,如果判斷到某一位置可以移動,則遞歸進入該位置判斷。 *如果該位置沒有任何位置可移動,則返會上級位置並且調用back函數。如果走到終點, *則立刻返回枚舉值FoundEnding,上級位置檢查到返回值為FoundEnding,也直接返回。 */ int findRoute(const CPoint& ptCurrent) { int nRes = NotFoundEnding; //默認返回值為沒有找到終點 CPoint ptNext = CPoint(-1, -1); advance(ptCurrent); //將當前位置加入路線數組 //判斷當前位置是否是終點,如果是終點則不進行下面的判斷,將返回值設置為找到終點 if(isEnding(ptCurrent)){ nRes = FoundEnding; }else{ //按上左下右的順序判斷有無可走路徑 //嘗試向上 ptNext = canUp(ptCurrent); //獲取向上走后的位置 //判斷向上走后的位置是否是合法位置,若不合法,則表明上走到了迷宮的邊緣,或者上面沒有可走路徑 if(ptNext.allRight()){ //上述判斷成功,則將向上移動后的位置傳入給自己,進行遞歸。當該函數退出,查看返回值是否為找到終點。若找到終點則立刻返回FoundEnding if(findRoute(ptNext) == FoundEnding){ nRes = FoundEnding; return nRes; } } //下列嘗試四周位置是否可走的代碼與上述大體相同,就不多說了 //嘗試向左 ptNext = canLeft(ptCurrent); if(ptNext.allRight()){ if(findRoute(ptNext) == FoundEnding){ nRes = FoundEnding; return nRes; } } //嘗試向下 ptNext = canDown(ptCurrent); if(ptNext.allRight()){ if(findRoute(ptNext) == FoundEnding){ nRes = FoundEnding; return nRes; } } //嘗試向右 ptNext = canRight(ptCurrent); if(ptNext.allRight()){ if(findRoute(ptNext) == FoundEnding){ nRes = FoundEnding; return nRes; } } } //檢測是否到達終點,若沒有到達終點,則立刻從路線表中刪除該位置 if(nRes != FoundEnding){ back(); } return nRes; } /*-----------------核心算法------------------------*/ public: //構造函數 CLabyrinthAI() { return; } //帶有初始化迷宮數組構造函數 CLabyrinthAI(const CLabyrinth& vLabyrinth) { m_xLabyrinth = vLabyrinth; getBeginning(); getEnding(); } //初始化迷宮數組 void setLabyrinth(const CLabyrinth& vLabyrinth) { m_xLabyrinth = vLabyrinth; } //查找起點 void getBeginning() { uint nRow = 0; for(; nRow < m_xLabyrinth.size(); nRow++){ CRow xRow = m_xLabyrinth[nRow]; uint nCol = 0; for(; nCol < xRow.size(); nCol++){ int n = xRow[nCol]; if(n == Beginning){ m_ptBeginning = CPoint(nCol, nRow); break; } } } } //查找終點 void getEnding() { uint nRow = 0; for(; nRow < m_xLabyrinth.size(); nRow++){ CRow xRow = m_xLabyrinth[nRow]; uint nCol = 0; for(; nCol < xRow.size(); nCol++){ int n = xRow[nCol]; if(n == Ending){ m_ptEnding = CPoint(nCol, nRow); break; } } } } //調用核心算法函數,輸出獲得的路線 void AI() { findRoute(m_ptBeginning); if(!m_vRoute.empty()){ CRoute::iterator it = m_vRoute.begin(); for(; it != m_vRoute.end(); it++){ CPoint pt = *it; std::cout << "(" << pt.row << ", " << pt.col << ")"; if(it != m_vRoute.end() - 1){ std::cout << "->"; }else{ std::cout << std::endl; } } }else{ //如果沒有找到路線到達終點 std::cout << "Sorry cannot file any ways to get ending." << std::endl; } } };
代碼都加上了注釋,大家可以慢慢看。
如果上述過程把你攪暈了,那就用圖來為你解答吧。
然后來到main函數
//用VC 6.0貌似不需要給main傳參數,那我就偷一下懶 int main() { //定義迷宮數組,定義成C風格的二維數組方便查看 int vLabyrinthArray[][4] = { {1,0,-1,1} , {1,0,0,1} , {0,0,1,1} , {0,1,1,0} , {0,1,1,1} , {-2,1,0,0} }; //以下代碼為將C風格的二維數組導入成C++風格的二維數組 int nRowNum = sizeof(vLabyrinthArray) / sizeof(vLabyrinthArray[0]); int nColNum = sizeof(vLabyrinthArray[0]) / sizeof(int); CLabyrinth vLabyrinth; for(int row = 0; row < nRowNum; row++){ CRow xRow; for(int col = 0; col < nColNum; col++){ int n = vLabyrinthArray[row][col]; xRow.push_back(n); } vLabyrinth.push_back(xRow); } //實例化CLabyrinthAI CLabyrinthAI xAI(vLabyrinth); //打出路線 xAI.AI(); //使程序暫停,方便查看數據 system("Pause"); return 0; }
以上代碼同樣加了注釋,相信了解C++的同學都能看懂。
運行截圖:
(Dos的,有點丑……)
三,Javascript版
順便我也把C++版的移植到了JavaScript上,代碼如下:
function CLabyrinthAI(){ var s = this; s.m_xLabyrinth = new Array(new Array()); s.m_ptBeginning = {}; s.m_ptEnding = {}; s.m_vRoute = new Array(); s.Beginning = -1; s.Ending = -2; s.CannotGo = 0; s.CanGo = 1; s.FoundEnding = 0; s.NotFoundEnding = 1; } CLabyrinthAI.prototype.initAI = function(){ var s = this; s.getBeginning(); s.getEnding(); } CLabyrinthAI.prototype.isRepeat = function(pt){ var s = this; var bRes = false; for(var n = 0; n < s.m_vRoute.length; n++){ var pt0 = s.m_vRoute[n]; if(pt0.col == pt.col && pt0.row == pt.row){ bRes = true; break; } } return bRes; }; CLabyrinthAI.prototype.advance = function(ptTo){ this.m_vRoute.push(ptTo); }; CLabyrinthAI.prototype.back = function(){ this.m_vRoute.splice(this.m_vRoute.length-1,1); }; CLabyrinthAI.prototype.isBeginning = function(pt){ if(this.m_ptBeginning.col == pt.col && this.m_ptBeginning.row == pt.row){ return true; }else{ return false; } }; CLabyrinthAI.prototype.isEnding = function(pt){ if(this.m_ptEnding.col == pt.col && this.m_ptEnding.row == pt.row){ return true; }else{ return false; } }; CLabyrinthAI.prototype.canUp = function(ptCurrent){ var s = this; var ptRes = {col:-1,row:-1}; var col = ptCurrent.col; var row = ptCurrent.row; if(row > 0){ var ptNext = {col:col,row:row - 1}; if(!s.isRepeat(ptNext)){ var nAttr = s.m_xLabyrinth[ptNext.row][ptNext.col]; if(nAttr == s.CanGo || nAttr == s.Ending){ ptRes = ptNext; } } } return ptRes; }; CLabyrinthAI.prototype.canDown = function(ptCurrent){ var s = this; var ptRes = {col:-1,row:-1}; var col = ptCurrent.col; var row = ptCurrent.row; if(row < s.m_xLabyrinth.length - 1){ var ptNext = {col:col,row:row + 1}; if(!s.isRepeat(ptNext)){ var nAttr = s.m_xLabyrinth[ptNext.row][ptNext.col]; if(nAttr == s.CanGo || nAttr == s.Ending){ ptRes = ptNext; } } } return ptRes; }; CLabyrinthAI.prototype.canLeft = function(ptCurrent){ var s = this; var ptRes = {col:-1,row:-1}; var col = ptCurrent.col; var row = ptCurrent.row; if(col > 0){ var ptNext = {col:col-1,row:row}; if(!s.isRepeat(ptNext)){ var nAttr = s.m_xLabyrinth[ptNext.row][ptNext.col]; if(nAttr == s.CanGo || nAttr == s.Ending){ ptRes = ptNext; } } } return ptRes; }; CLabyrinthAI.prototype.canRight = function(ptCurrent){ var s = this; var ptRes = {col:-1,row:-1}; var col = ptCurrent.col; var row = ptCurrent.row; if(col < s.m_xLabyrinth[0].length - 1){ var ptNext = {col:col+1,row:row}; if(!s.isRepeat(ptNext)){ var nAttr = s.m_xLabyrinth[ptNext.row][ptNext.col]; if(nAttr == s.CanGo || nAttr == s.Ending){ ptRes = ptNext; } } } return ptRes; }; CLabyrinthAI.prototype.allRight = function(p){ if(p.col >= 0 && p.row >= 0){ return true; }else{ return false; } }; CLabyrinthAI.prototype.findRoute = function(ptCurrent){ var s = this; var nRes = s.NotFoundEnding; var ptNext = {col:-1,row:-1}; s.advance(ptCurrent); if(s.isEnding(ptCurrent)){ nRes = s.FoundEnding; }else{ ptNext = s.canUp(ptCurrent); if(s.allRight(ptNext)){ if(s.findRoute(ptNext) == s.FoundEnding){ nRes = s.FoundEnding; return nRes; } } ptNext = s.canLeft(ptCurrent); if(s.allRight(ptNext)){ if(s.findRoute(ptNext) == s.FoundEnding){ nRes = s.FoundEnding; return nRes; } } ptNext = s.canDown(ptCurrent); if(s.allRight(ptNext)){ if(s.findRoute(ptNext) == s.FoundEnding){ nRes = s.FoundEnding; return nRes; } } ptNext = s.canRight(ptCurrent); if(s.allRight(ptNext)){ if(s.findRoute(ptNext) == s.FoundEnding){ nRes = s.FoundEnding; return nRes; } } } if(nRes != s.FoundEnding){ s.back(); } return nRes; }; CLabyrinthAI.prototype.getBeginning = function(){ var s = this; for(var nRow = 0; nRow < s.m_xLabyrinth.length; nRow++){ var xRow = s.m_xLabyrinth[nRow]; for(var nCol = 0; nCol < xRow.length; nCol++){ var n = xRow[nCol]; if(n == s.Beginning){ s.m_ptBeginning = {col:nCol,row:nRow}; break; } } } }; CLabyrinthAI.prototype.getEnding = function(){ var s = this; for(var nRow = 0; nRow < s.m_xLabyrinth.length; nRow++){ var xRow = s.m_xLabyrinth[nRow]; for(var nCol = 0; nCol < xRow.length; nCol++){ var n = xRow[nCol]; if(n == s.Ending){ s.m_ptEnding = {col:nCol,row:nRow}; break; } } } }; CLabyrinthAI.prototype.AI = function(data){ var s = this; s.m_xLabyrinth = data; s.initAI(); s.findRoute(s.m_ptBeginning); return s.m_vRoute; };
設計原理和C++版差不多,只是沒有CPoint類而已。
雖然這套算法是研究出來了,但是還不能判斷是否為最近路線,因此有待更新。不過以現在的算法,開發一個SLG應該不是問題了。
※感謝我的哥哥與我一起討論其中的原理。
源代碼下載: