A 尋路演算法之解決路徑多拐點問題

2021-08-22 16:24:53 字數 4387 閱讀 8874

最近公司正在開發的遊戲涉及到了尋路演算法,然後我從網上找了乙份a*演算法**,整理了一下寫了乙個a*演算法基礎實現。然而,在真正實用時a*尋路時,卻發現了幾個問題:

基礎實現版的a*尋路演算法在大地圖的搜尋上,耗時較長;

使用最小堆實現的openlist來優化a*演算法後,發現最後得到的路徑往往是s型的;

然後策劃看到效果後,提出了兩點要求:1)尋路的路徑中,拐點必須最少;2)存在多條路徑時,必須優先走最快碰到拐點的路徑。

稍微解釋一下上面的兩個需求:假如出發點和目的點是"日"字型時,可經過上中下三條路徑到達目的地。上下兩條路徑的拐點是1個,而中間路徑的拐點是2個,淘汰中間路徑。另外,上路走一步碰到拐點,而下路走兩步碰到拐點,那麼優先選擇上路。

}}上面的**實現了a*演算法的邏輯,但是在200*200的地圖上測試時,從(0,0)點走到(199,199)尋路一次通常要幾秒到十幾秒。

那麼,我們先分析一下**的效能為什麼那麼低。主要有兩點:

1)  findminfnodeinopenlist() 方法需要遍歷 open list ,查詢 f值最小的節點,該方法的時間複雜度是o(n)。雖然該方法時間複雜度是線性的,但是每次檢查乙個節點時,都會執行一次該方法。

2) 新增新節點或者更新下乙個節點時,都會呼叫一次exists()方法判斷節點是否在openlist或者closelist中。該方法同樣是線性的,但是新增新節點或者更新下乙個節點時,總會呼叫一次或多次,因此時間複雜度是m*o(n)。

針對上面兩個痛點進行優化:首先,在乙個佇列中獲取獲取最大/最小的元素,我們通常首先想到的就是最大/小堆,因此,利用優先佇列priorityqueue實現openlist,那麼每次從openlist中查詢 最小f值的節點時,時間複雜度將降為o(lg n)。其次,分析上下文發現,closelist僅在新增節點到openlist時做去重判斷,而沒有其他作用,那麼可以通過陣列標記或者map儲存遍歷資訊,使時間複雜度達到o(1)。以下是優化後的**:

public class astaroptimization 

});private setopenset = new hashset<>();

private setcloseset = new hashset<>();

public astaroptimization(gamemap map)

public listfindpath()

public listfindpath(node startnode, node endnode)

} else

}if (contains(openset, endnode.x, endnode.y))

} node node = findopenlist(newopenlist, endnode);

return getpathlist(node); }

public arraylistfindneighbornodes(node currentnode)

private void addnode(arraylistarraylist, int x, int y)

} private int calcnodecost(node node1, node node2)

private node findopenlist(priorityqueuenodes, node point)

} return null; }

public listgetpathlist(node parent)

return list; }

public int abs(final int x)

private void addkey(setset, int x, int y)

private void removekey(setset, int x, int y)

private boolean contains(setset, int x, int y)

private string getkey(int x, int y)

}

優化後的**在200*200的地圖上測試,在障礙物比例為0.3時(200 * 200 * 0.3),從(0,0)點走到(199,199)尋路一次基本在30ms~50ms。

由於基於最小堆實現的優先佇列是不穩定,在多個具有相同f值的節點中選擇最小節點的順序是無法保證的。因此,以上優化後的**走出來的路徑通常都是s型的,也就是存在拐點過多的問題。

是否使用基礎版本的a*演算法是否可以消除多拐點的問題呢?答案是否定的,基礎版本的a*演算法是按照一定的方向順序新增節點,在相同f值時,獲取節點也是按照相同方向順序獲取節點,這在空曠的地形中是不會出現s型路徑。但是,當路徑上有障礙物時,按照順序新增節點然後按照順序取節點的方法,就會出現走s型路徑的問題。

例如,如圖所示情形,當從a點出發移動到b, a*演算法按照上下左右的方向順序新增節點,a從綠色路徑移動到b;相反,如果從b移動到a點,a*演算法會向下走到a點,但是沿途有障礙物,於是就形成了s型路徑。

那麼,基於最小堆實現的openlist,如何杜絕路徑多拐點呢?路徑是演算法執行過程中動態生成,生成或有路徑去選擇一條是不可能。最好的辦法是,根據路徑的特點調整路徑上拐點的f值。

a*演算法是利用啟發函式:f(n) = g(n) + h(n),來確定每個點的f值,採用貪心策略選擇f值最小的點作為下乙個待更新節點,直到找到終點。其中g(n)表示從起始點走到該點的代價,h(n)估算從該點走到終點的代價。通常g(n)採用走到該點所用的步數來表示,而h(n)使用該點到終點的距離來表示。

從啟發函式來看,a*演算法對於路徑的特點根本沒有做任何要求,只要是最小f值的節點都可以加入路徑當中。因此,我們可以在啟發函式中加入對節點的路徑特徵的評判,使演算法選擇的最終結果符合我們的預期。基於此目的,修改啟發函式:

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

其中e(n)表示加入該節點後,對路徑的評分進行的微調。話不多說,先上**:

public listfindpath(node startnode, node endnode) 

} else

}if (contains(openset, endnode.x, endnode.y))

} node node = findopenlist(newopenlist, endnode);

return getpathlist(node); }

private int calcnodeextracost(node currnode, node nextnode, node endnode)

// 拐向終點的點

if (nextnode.x == endnode.x || nextnode.y == endnode.y)

// 普通拐點

return 2;

}

**的終點是calcnodeextracost()方法,方法中判斷如果nextnode和之前的節點是保持直線的,那麼e值為0,否則如果是乙個拐點的話,e值將大於0,並且這個e值會和g值存在一起,作為新的g值。

a*路徑多拐點的問題暫時寫到這裡了,之後再補上後面的內容。

尋路演算法之A

最近接觸到moba multiplayer online battle arena 這個新名詞,發現npc尋路有點意思。於是回顧了一下經典的a 演算法。open列表 記錄可用來被尋找的點 closed列表 記錄排除在路線之外的點 對於路線中的乙個點 3.1 g值 從開始到此點的移動量 3.2 h值 ...

python迷宮尋路 迷宮尋路問題 A 演算法

迷宮尋路問題 a 演算法 迷宮尋路問題是人工智慧中的有趣問題,如何表示狀態空間和搜尋路徑是尋路問題的重點,本文的主要內容是a 搜尋演算法的理解和應用,首先對基本知識和演算法思想進行了解,再通過其對迷宮問題求解應用,編寫 python 程式進行深入學習。1.搜尋區域 我們假設某個人要從 start 點...

js演算法之尋路

演算法流程說明 說明 起始節點記作s,目標節點記作e,對於任意節點p,從s到當前節點p的總移動消耗記作gp,節點p到目標e的曼哈頓距離記作hp,從節點p到相鄰節點n的移動消耗記作dpn,用於優先順序排序的值f n 記作fp 選擇起始節點s和目標節點e,將 s,0 節點,節點f n 值 放入openl...