圖搜索簡介
圖搜索總能產生一棵搜索樹,高效+最優構建搜索樹為算法核心。
圖搜索算法一般框架如下所示:
盲目搜索方法
所有的圖搜索算法都具有一種容器(container)和一種方法(algorithm)。
- “容器”在一定意義上就是開集,確定了算法的數據結構基礎,以起始節點\(S\)初始化,定義了結點進出的規則,深搜就是棧(stack),廣搜就是隊列(queue)。
- “方法”確定了結點彈出的順序,深搜(Depth First Search)中是“彈出最深的節點”,廣搜(Breadth First Search)中是“彈出最淺的節點”(在樹中表現為由根向葉層序推進)。
需要注意的是,DFS不能保證在一定的時空復雜度限制下尋找到最短路徑。因此,圖搜索的基礎是BFS。
啟發式方法
一般地,BFS只適用於任意兩點距離為1的圖搜索找最短路徑,而且屬於“撒網式”的沒有明確目標方向的盲目嘗試。在BFS的基礎上,重新定義結點出棧的順序“具有最優屬性的結點先彈出容器”,並升級容器為“優先隊列”,就形成了具有啟發式的路徑搜索算法。
在正邊權有向圖中,每個節點的距離代價評估可用估價函數\({f(n)}\)來建模。
其中\(g(n)\)是在狀態空間中從初始節點到節點\(n\)的實際代價,\(h(n)\)是從節點\(n\)到目標節點最佳路徑的啟發式估計代價,即“啟發式(heuristic)距離”,“猜測”當前節點距離目標節點還有多遠。
- Greedy:\(f(n)=h(n)\)
策略:不斷訪問距終點啟發距離最小的鄰點(默認當前點到所有鄰點距離相同,不同則加上當前點到鄰點距離代價)- 無障礙情況下比BFS高效;在最短路徑上出現障礙則極大可能找不到最優解。
- Dijkstra:\(f(n)=g(n)\)
-
策略:不斷訪問距原點累計距離最小的鄰點,鄰點若未擴展過直接加入優先隊列,若已擴展過(即在優先隊列中)則進行松弛 -
最優性保證:已擴展點存儲的一定是距離起始點的最短距離 -
搜索過程均勻擴展(與邊權相關),若任意兩點距離為1退化為BFS
-
偽代碼如下:
-
Dijkstra與Greedy算法的對比如下:
-
- A-star:\({f(n)=g(n)+h(n)}\)
-
A*算法與Dijstra等一致代價搜索算法的主要區別在於啟發項\({h(n)}\)的存在將優先隊列的排序依據由\(g(n)\)變成\(f(n)\)。
A-star編程注意更新時要同步更新優先隊列中每個節點的\(g(n)\)。
-
估價距離\({h(n)}\)不大於節點\(n\)到目標節點的距離時,搜索的點數多、范圍大、效率低,保證得到最優解;若估價距離大於實際距離, 搜索的點數少、范圍小、效率高,但不能保證得到最優解。估價值與實際值越接近,估價函數質量越高。
-
\(h\le h^*\)時保證算法完備性,舉例如下:
-
偽代碼如下:
-
A-star算法流程
A*算法是靜態路網中求解最短路最有效的方法之一,主要搜索過程偽代碼示意如下:
//step 1
創建兩個表,OPEN表保存所有已生成而未考察的節點,CLOSED表中記錄已訪問過的節點。
//step 2
遍歷當前節點的各個節點,將n節點放入CLOSE中,取n節點的子節點X,算X的估價值
//step 3
While(OPEN!=NULL)
{
從OPEN表中取估價值f最小的節點n;
if(n節點==目標節點) break;
else
{
if(X in OPEN)
比較兩個X的估價值f //注意是同一個節點的兩個不同路徑的估價值
if( X的估價值小於OPEN表的估價值 )
更新OPEN表中的估價值; //取最小路徑的估價值
if(X in CLOSE)
比較兩個X的估價值 //注意是同一個節點的兩個不同路徑的估價值
if( X的估價值小於CLOSE表的估價值 )
更新CLOSE表中的估價值; 把X節點放入OPEN //取最小路徑的估價值
if(X not in both)
求X的估價值;並將X插入OPEN表中; //還沒有排序
}
將n節點插入CLOSE表中;按照估價值將OPEN表中的節點排序;
//(實際上是比較OPEN表內節點f的大小,從最小路徑的節點向下進行。)
}
A*算法框圖展示如下:
A-star算法實現
[編譯環境]
Windows 系統|PyCharm 編譯器|python 3.8.11
定義地圖類
由長度、寬度、起點坐標、終點坐標、障礙坐標列表、地圖模式(4鄰接模式/8鄰接模式)唯一確定一個地圖類。
[4鄰接模式]:
agent所有可能的移動范圍包括上、下、左、右四個方向,一步行進一個單位長度
[8鄰接模式]:
agent所有可能的移動范圍包括上、下、左、右、左上、左下、右上、右下八個方向,一步行進一個單位長度
class Map:
def __init__(self, width, height, start, end, obstacles, mode):
assert mode == 4 or mode == 8
self.OBSTACLE = -1
self.START = 1
self.END = 2
self.start = start
self.end = end
self.height = height
self.width = width
self.mode = mode
# --------------------------------------------------
self.mp = np.zeros((height, width))
# set begin and end
self.mp[start] = self.START
self.mp[end] = self.END
# set obstacles
for x, y in obstacles:
self.mp[x, y] = self.OBSTACLE
A*算法類
繼承地圖類的信息,類內成員變量和函數具體闡釋如下:
class Solver(Map):
def __init__(self, width, height, start, end, obstacles, mode):
super(Solver, self).__init__(width, height, start, end, obstacles, mode)
self.mindistance = inf
self.path = []
def within(self, x, y): # border detection
return 0 <= x < self.height and 0 <= y < self.width
def neighbors(self, node): # get neighbors
if self.mode == 4:
direction = [(-1, 0), (0, -1), (0, 1), (1, 0)]
if self.mode == 8:
direction = [(-1, 0), (0, -1), (0, 1), (1, 0),
(-1, -1), (1, -1), (-1, 1), (1, 1)]
return [(node[0] + x, node[1] + y) for (x, y) in direction if
self.within(node[0] + x, node[1] + y) and self.mp[node[0] + x, node[1] + y] != self.OBSTACLE]
def movecost(self, cur, near): # move cost,移動距離由mode決定
if self.mode == 8:
ord = np.inf
if self.mode == 4:
ord = 1
return np.linalg.norm(np.array(cur) - np.array(near), ord=ord)
def heuristic(self, near, end): # heuristic distance,啟發式距離可人為設定,默認曼哈頓距離
# 當mode = 4, ord = 1 / 2 / inf
# 當mode = 8, ord = inf
if self.mode == 8:
ord = np.inf
if self.mode == 4:
ord = np.random.choice([1, 2, np.inf])
return np.linalg.norm(np.array(end) - np.array(near), ord=ord)
def A_star(self): # search
# init priority-queue
q = PriorityQueue()
q.put(self.start, int(0))
# init path recorder
comeFrom = {self.start: None}
# init current cost recorder
costSoFar = {self.start: 0}
# searching
while q.qsize():
cur = q.get()
if cur == self.end:
break
for near in self.neighbors(cur):
newCost = costSoFar[cur] + self.movecost(cur, near)
if near not in costSoFar or newCost < costSoFar[near]: # 沒有搜過的點相當於距離無窮大
costSoFar[near] = newCost
comeFrom[near] = cur
q.put(near, costSoFar[near] + self.heuristic(near, self.end))
# terminate,find path recursively
terminal = self.end
path = [self.end]
while comeFrom.get(terminal, None) is not None:
path.append(comeFrom[terminal])
terminal = comeFrom[terminal]
path.reverse()
self.mindistance = costSoFar.get(self.end, inf)
self.path = path
def outputresult(self):
mindistance = self.mindistance if self.mindistance != inf else '∞'
print(f'從{self.start}到{self.end}最短距離:{mindistance}')
print('最短路徑如下:')
if len(self.path) == 1 and self.path[0] == end:
print('empty path')
else:
for i, node in enumerate(self.path):
print(node, end='')
if i != len(self.path) - 1:
print('->', end='')
else:
print()
數據導入
def loadTestData(n=1):
if n == 1:
# 起始點
start = (2, 2)
end = (6, 12)
# 創建障礙
obstacle_y = [i for i in range(5, 10)]
obstacle_x = [2] * len(obstacle_y)
tmp = [i for i in range(3, 6)]
obstacle_x.extend(tmp)
obstacle_y.extend([9] * len(tmp))
tmp = [i for i in range(5, 10)]
obstacle_y.extend(tmp)
obstacle_x.extend([6] * len(tmp))
obstacles = zip(obstacle_x, obstacle_y)
if n == 2:
start = (0, 0)
end = (3, 3)
obstacles = [(3, 2), (3, 4), (2, 3), (4, 3)]
return start, end, obstacles
主函數
if __name__ == '__main__':
# 初始化地圖基本屬性
WIDTH = 15
HEIGHT = 10
mode = 4
start, end, obstacles = loadTestData(n=1)
print(f'起點:{start} 終點:{end}')
print('障礙:', *obstacles)
print('------------------------------------------------------')
# A*最短路徑求解
A_star_solver = Solver(WIDTH, HEIGHT, start, end, obstacles, mode)
A_star_solver.A_star()
A_star_solver.outputresult()
控制台測試
- 正常情況測試(存在最短路徑)
- 異常情況測試(4-鄰接下無最短路徑)
A-star算法可視化呈現
引入PythonPyQt5第三方庫,主要通過自行實現GameBoard類搭建窗口程序,完成A*算法在地圖尋路上的應用(具體代碼詳見附件)。
窗口的主要區域為地圖可視化顯示,地圖右側分別展示窗口的使用說明、地圖的顏色說明、操作功能鍵以及信息輸出。通過加載預測地圖或者根據使用說明設置地圖后即可點擊“開始搜索”進行尋路結果演示,算法尋找到的最優路徑以及路徑的最短距離在尋路演示之后會呈現在信息輸出區域。
[使用說明]
右鍵 : 首次單擊格子選定起始點,第二次單擊格子選定終點
左鍵 : 選定格子為牆壁,單擊牆壁則刪除牆壁
[顏色說明]
黃色 : 代表起點
綠色 : 代表終點
黑色 : 代表牆壁
灰色 : 代表可行區域
紅色 : 閃爍,代表最短路徑上的每個節點
視頻演示中我們分別使智能體以4鄰接和8鄰接方式進行尋路,所使用的地圖如上所示。結果比較如下:
# 4鄰接
從(0, 0)到(12, 15)最短距離:41
最短路徑如下:
(0, 0)->(1, 0)->(2, 0)->(3, 0)->(4, 0)->(5, 0)->(6, 0)->(6, 1)->(6, 2)->(5, 2)->(4, 2)->(3, 2)->(2, 2)->(1, 2)->(0, 2)->(0, 3)->(0, 4)->(0, 5)->(0, 6)->(0, 7)->(0, 8)->(0, 9)->(1, 9)->(2, 9)->(3, 9)->(4, 9)->(5, 9)->(6, 9)->(6, 10)->(6, 11)->(5, 11)->(5, 12)->(5, 13)->(5, 14)->(5, 15)->(6, 15)->(7, 15)->(8, 15)->(9, 15)->(10, 15)->(11, 15)->(12, 15)
# 8鄰接
從(0, 0)到(12, 15)最短距離:21
最短路徑:
(0, 0)->(1, 0)->(2, 0)->(3, 0)->(4, 0)->(5, 0)->(6, 0)->(7, 1)->(8, 2)->(9, 3)->(10, 4)->(11, 5)->(10, 6)->(11, 7)->(12, 8)->(12, 9)->(12, 10)->(12, 11)->(12, 12)->(12, 13)->(12, 14)->(12, 15)
應用A*算法在自己設計的游戲界面上運行順利,我們繼續探索,將算法應用在真實游戲中,實現功能:通過鼠標點擊目標位置使游戲人物以最短路徑到達指定位置。結果呈現見視頻演示。
評價
- A*算法的核心代碼部分主要基於優先隊列的數據結構實現(底層結構為二叉堆),既凸顯啟發式算法的特征,在代碼效率方面相比其他數據結構又有一定的提升;同時考慮到無最短路徑的特殊情況,算法魯棒性強。
- A*算法的核心代碼以及可視化代碼通過類進行封裝並形成一個完整模塊,便於改變地圖模式,也便於代碼的維護與調試。
擴展
地圖路標形式
將地圖的拓撲特征抽取出,使用路標形式存儲地圖可以有效提高算法尋路的效率。
A-star算法工程應用
從更加宏觀和一般的角度看待含有啟發式信息的尋路算法:
Weighted A-star:\(f(n)=g(n)+\epsilon h(n),\epsilon > 1\)
- 用次優解換取更少的搜索時間,高估的啟發距離使其更偏向貪心算法,可證明次優解質量滿足:\(cost\le \epsilon·cost^*\)
- 還可以使\(\epsilon\)隨搜索越來越接近1,在最優性和時間成本之間權衡
-
最合適的啟發式函數
由於h越接近h*越好,而在無障礙的柵格地圖中最短路徑一定沿以起點終點確定的矩形的對角線,因此可定義Diagonal Heuristic:
# D為水平/豎直移動代價;D2為斜線移動代價 def heuristic(node,goal): dx = abs(node.x - goal.x) dy = abs(node.y - goal.y) return D * (dx + dy) + (D2 - 2 * D) * min(dx, dy) -
打破路徑的對稱性以減少搜索次數
對於f相等的路徑,A*中是無差別探索,結果趨向於找到多條最優路徑。但實際上只需要一條,因此在搜索的時候可以設置“傾向”,僅找一條最短路徑,思路可以選擇如下幾種:- 在f相同時選擇h大/小的路線
- 構建一張僅與坐標關聯的隨機數表,\(h=h+\epsilon\)
- 趨向選擇更接近對角線的路線
def h_(start,node,goal): dx1 = abs(node.x - goal.x) dy1 = abs(node.y - goal.y) dx2 = abs(start.x - goal.x) dy2 = abs(start.y - goal.y) cross = abs(dy2*dx1-dx2*dy1) return h(node,goal) + cross * 0.001 # h為原啟發式函數,cross愈大相當於當前點離對角線上的點越遠對原 h給與更高的懲罰,cross的系數必須小不要逾越h_<=h*的最優條件 - 稍微“打破”完備性條件
D-star算法淺談
A算法是靜態路網中有效的尋路算法,而D**算法是不斷變化的動態環境下采用的有效尋路算法,其主要算法流程如下:
//step 1
先用Dijstra算法從目標節點G向起始節點搜索。儲存路網中目標點到各個節點的最短路和該位置到目標點的實際值h,k。(k為所有變化h之中最小的值,當前為k=h。每個節點包含上一節點到目標點的最短路信息1(2),2(5),5(4),4(7)。則1到4的最短路為1-2-5-4)
原OPEN和CLOSE中節點信息保存。
//step 2
/機器人沿最短路開始移動,在移動的下一節點沒有變化時,無需計算,利用上一步Dijstra計算出的最短路信息從出發點向后追述即可,當在Y點探測到下一節點X狀態發生改變(如堵塞)。機器人首先調整自己在當前位置Y到目標點G的實際值h(Y),h(Y)=X到Y的新權值c(X,Y)+X的原實際值h(X).X為下一節點(到目標點方向Y->X->G),Y是當前點。k值取h值變化前后的最小。
//step 3
用A*或其它算法計算,這里假設用A*算法,遍歷Y的子節點,點放入CLOSE,調整Y的子節點a的h值,h(a)=h(Y)+Y到子節點a的權重C(Y,a),比較a點是否存在於OPEN和CLOSE中,
//偽碼示意
while()
{
從OPEN表中取k值最小的節點Y;
遍歷Y的子節點a,計算a的h值 h(a)=h(Y)+Y到子節點a的權重C(Y,a)
{
if(a in OPEN)
比較兩個a的h值
if( a的h值小於OPEN表a的h值 )
{
更新OPEN表中a的h值;k值取最小的h值
有未受影響的最短路經存在
break;
}
if(a in CLOSE)
比較兩個a的h值 //注意是同一個節點的兩個不同路徑的估價值
if( a的h值小於CLOSE表的h值 )
{
更新CLOSE表中a的h值; k值取最小的h值;將a節點放入OPEN表
有未受影響的最短路經存在
break;
}
if(a not in both)
將a插入OPEN表中; //還沒有排序
}
放Y到CLOSE表;
OPEN表比較k值大小進行排序;
}
機器人利用第一步Dijstra計算出的最短路信息從a點到目標點的最短路經進行。
總結
本文從圖搜索到A*算法從理論到實踐分析比較了A-star算法的優勢並給出其代碼實現,並且進一步探討了路標形式表示優化算法效率的方法以及了解了應用於動態環境下的D-star算法,為更復雜問題的尋路搜索提供了思路。
