演算法之美9 搜尋

2021-09-17 01:11:45 字數 4386 閱讀 5699

dfs、bfs、a*啟發式

bfs

廣度優先搜尋,通俗的理解就是,地毯式層層推進,從起始頂點開始,依次往外遍歷。廣度優先搜尋需要借助佇列來實現,遍歷得到的路徑就是,起始頂點到終止頂點的最短路徑。

時間複雜度是 o(e),空間複雜度是 o(v)。

例子:水滴波紋

def

bfs(graph, start, end)

: queue =

[start]

) visited =

set(

)# 樹可省,圖不可

visited.add(start)

while queue:

node = queue.pop(

) visited.add(node)

process(node)

nodes = generate_related_nodes(node)

queue.push(nodes)

# other processing work

dfs

深度優先搜尋用的是回溯思想,非常適合用遞迴實現。換種說法,深度優先搜尋是借助來實現的。

時間複雜度是 o(e),空間複雜度是 o(v)。

例子就是「走迷宮」

def

dfs(node)

:# 遞迴寫法

visited =

set(

) visited.add(node)

# process current node here

for next_node in node.children():

ifnot

not next_node in visited:

dfs(next_node)

def

dfs_stack

(self, tree)

:if tree.root is

none

:return

visited =

set(

) stack =

[tree.node]

while stack:

node = stack.pop(

) visited.add(node)

process(node)

nodes = generate_related_nodes(node)

stack.push(nodes)

# other processing work

如何找出社交網路中的三度好友關係?

這個問題就非常適合用圖的廣度優先搜尋演算法來解決因為廣度優先搜尋是層層往外推進的。首先,遍歷與起始頂點最近的一層頂點,也就是使用者的一度好友,然後再遍歷與使用者距離的邊數為 2 的頂點,也就是二度好友關係,以及與使用者距離的邊數為 3 的頂點,也就是三度好友關係。

我們只需要稍加改造一下廣度優先搜尋**,用乙個陣列來記錄每個頂點與起始頂點的距離,非常容易就可以找出三度好友關係。

我們通過廣度優先搜尋演算法解決了開篇的問題,你可以思考一下,能否用深度優先搜尋來解決呢?

深度優先用於尋找3度好友,可以設定搜尋的深度,到3就向上回溯。

學習資料結構最難的不是理解和掌握原理,而是能靈活地將各種場景和問題抽象成對應的資料結構和演算法。今天的內容中提到,迷宮可以抽象成圖,走迷宮可以抽象成搜尋演算法,你能具體講講,如何將迷宮抽象成乙個圖嗎?或者換個說法,如何在計算機中儲存乙個迷宮?

類似於尤拉七橋問題,需要將迷宮抽象成圖,每個分叉路口作為頂點,頂點之間連成邊,構成一張無向圖,可以儲存在鄰接矩陣或鄰接表中。

dijkstra 演算法 & a* 演算法

實際上,像出行路線規劃、遊戲尋路,這些真實軟體開發中的問題,一般情況下,我們都不需要非得求最優解(也就是最短路徑)。在權衡路線規劃質量和執行效率的情況下,我們只需要尋求乙個次優解就足夠了。那如何快速找出一條接近於最短路線的次優路線呢?這個快速的路徑規劃演算法,就是我們今天要學習的a* 演算法。實際上,a* 演算法是對 dijkstra 演算法的優化和改造。

當我們遍歷到某個頂點的時候,從起點走到這個頂點的路徑長度是確定的,我們記作g(i)(i 表示頂點編號)。但是,從這個頂點到終點的路徑長度,我們是未知的。雖然確切的值無法提前知道,但是我們可以用其他估計值來代替。

這裡我們可以通過這個頂點跟終點之間的直線距離,也就是歐幾里得距離,來近似地估計這個頂點跟終點的路徑長度(注意:路徑長度跟直線距離是兩個概念)。我們把這個距離記作h(i)(i 表示這個頂點的編號),專業的叫法是啟發函式(heuristic function)。因為歐幾里得距離的計算公式,會涉及比較耗時的開根號計算,所以,我們一般通過另外乙個更加簡單的距離計算公式,那就是曼哈頓距離(manhattan distance)。曼哈頓距離是兩點之間橫縱座標的距離之和。計算的過程只涉及加減法、符號位反轉,所以比歐幾里得距離更加高效。

原來只是單純地通過頂點與起點之間的路徑長度 g(i),來判斷誰先出佇列,現在有了頂點到終點的路徑長度估計值,我們通過兩者之和 f(i)=g(i)+h(i),來判斷哪個頂點該最先出佇列。綜合兩部分,我們就能有效避免剛剛講的「跑偏」。這裡 f(i) 的專業叫法是估價函式(evaluation function)。

a* 演算法就是對 dijkstra 演算法的簡單改造。a* 演算法的**實現的主要邏輯是下面這段**。它跟 dijkstra 演算法的**實現,主要有 3 點區別:

儘管 a* 演算法可以更加快速的找到從起點到終點的路線,但是它並不能像 dijkstra 演算法那樣,找到最短路線。這是為什麼呢?

要找出起點 s 到終點 t 的最短路徑,最簡單的方法是,通過回溯窮舉所有從 s 到達 t 的不同路徑,然後對比找出最短的那個。不過很顯然,回溯演算法的執行效率非常低,是指數級的。

a* 演算法之所以不能像 dijkstra 演算法那樣,找到最短路徑,主要原因是兩者的 while 迴圈結束條件不一樣。剛剛我們講過,dijkstra 演算法是在終點出佇列的時候才結束,a 演算法是一旦遍歷到終點就結束*。對於 dijkstra 演算法來說,當終點出佇列的時候,終點的 dist 值是優先順序佇列中所有頂點的最小值,即便再執行下去,終點的 dist 值也不會再被更新了。對於 a* 演算法來說,一旦遍歷到終點,我們就結束 while 迴圈,這個時候,終點的 dist 值未必是最小值。

如何借助 a* 演算法解決今天的遊戲尋路問題?

要利用 a* 演算法解決這個問題,我們只需要把地圖,抽象成圖就可以了。不過,遊戲中的地圖跟我們平常用的地圖是不一樣的。因為遊戲中的地圖並不像我們現實生活中那樣,存在規劃非常清晰的道路,更多的是寬闊的荒野、草坪等。所以,我們沒法把岔路口抽象成頂點,把道路抽象成邊。

實際上,我們可以換一種抽象的思路,把整個地圖分割成乙個乙個的小方塊。在某乙個方塊上的人物,只能往上下左右四個方向的方塊上移動。我們可以把每個方塊看作乙個頂點。兩個方塊相鄰,我們就在它們之間,連兩條有向邊,並且邊的權值都是 1。所以,這個問題就轉化成了,在乙個有向有權圖中,找某個頂點到另乙個頂點的路徑問題。將地圖抽象成邊權值為 1 的有向圖之後,我們就可以套用 a* 演算法,來實現遊戲中人物的自動尋路功能了。

總結引申

我們今天講的 a* 演算法屬於一種啟發式搜尋演算法(heuristically search algorithm)。實際上,啟發式搜尋演算法並不僅僅只有 a* 演算法,還有很多其他演算法,比如 ida* 演算法、蟻群演算法、遺傳演算法、模擬退火演算法等。如果感興趣,你可以自行研究下。

思考

我們之前講的「迷宮問題」是否可以借助 a* 演算法來更快速地找到乙個走出去的路線呢?如果可以,請具體講講該怎麼來做;如果不可以,請說說原因。

資料結構與演算法之美(筆記9)雜湊演算法

我們前面講到雜湊表,雜湊函式,這裡又是雜湊演算法,實際上,雜湊函式就是雜湊演算法的乙個特例。只不過在雜湊表中,我們通常希望雜湊函式簡單,才不會影響查詢等的效能。雜湊演算法的定義和原理很簡單,就是把任意二進位制串值對映為固定長度的二進位制值串,這個對映的規則就是雜湊演算法,而通過原始的資料對映之後得到...

《資料結構與演算法之美》9講學習筆記

一 什麼是佇列?1.先進者先出,這就是典型的 佇列 結構。2.支援兩個操作 入隊enqueue 放乙個資料到隊尾 出隊dequeue 從隊頭取乙個元素。3.所以,和棧一樣,佇列也是一種操作受限的線性表。二 如何實現佇列?1.佇列api public inte ce queue2.陣列實現 順序佇列 ...

演算法之美 3 2 2 MP演算法

這塊硬骨頭,放在這裡半年的時間了,一直沒有動,今天週末看看,書上把過程寫的比較詳細,自己基本也看懂了,但是對 本身的編寫還是比較生疏,要經常複習,估計才能看透,後面有看了kmp 這兩者之間的關係也是頭大。1 2 file mp 演算法.cpp3 4 author ranjiewen 5 date 2...