圖中的算法多的要命,大多數都是搜索算法吧,因爲其應用實在太廣了。就比如說機器人眼中的世界其實就是一張graph,graph的大小的有限的(這跟分配給它的memory有關),機器人就根據現有的graph信息邊計算邊決策,然後邊走邊獲取新graph信息,再重新計算再決策再走……這是一個迭代過程,直到到達目的地爲止,汽車導航儀也是類似的。我覺得這過程有點像你用一個英雄去打敵方老窩,在地圖還不是完全可見的情況下,你得親自控制着他,看好路,根據情況選擇對應的戰鬥策略。……哈哈扯遠了,這個其實算Informed Search了,也就是帶有決策的啓發式搜索了(我們將在下一篇中介紹)。我們先來介紹簡單的Uninformed Search。
什麼叫Search?Search就是在所有Search space(狀態空間——往往能用樹或圖來表示出)中找到你想要的那個狀態。
Uninformed Search,顧名思義就是消息不靈通的search,被矇蔽的search,也可以叫做Blind Search(盲目搜索)或者Brute-force Search(也就是傳說中的暴力解法)。
我們先來看兩個搜索的例子。
如圖,我們從A處進入這個空間,我們想要到達J處。當然我們人眼是肯定一眼就看穿路線了,但讓機器應該怎麼做?
計算機勢必得先把這張地圖信息存起來,格式只能是計算機能實現的數據結構。
在這棵樹(也可以叫圖)上,我們把每一個節點視作一個搜索狀態,每一條邊視作一次搜索動作,只有能從初始節點到達目標節點就算搜索成功了。經典的算法莫過於深度優先搜索和廣度優先搜索了(我們將待會兒介紹)。
我們再來看一個例子。
有一種兩人遊戲叫Nim Game,規則是:在一根柱子上放N個大小相同的環,兩個人輪流從上面取環下來,規定每人每次最多隻能取連續的m個環,不能不取,誰取到最後一個環就算輸。
假定現在N=6, m=3,兩人遊戲過程如圖。想想看先取者和後取者誰的勝率較高?進一步再想想看先取者和後取者有沒有最有把握勝的策略。
我們將這個問題中所有的狀態空間用一棵樹來表示出。
所有的節點中的數字表示剩下的環的個數,剩下1則表示接下來取的那個人肯定輸了,圖中的邊則表示取下的環的個數。
可見,總共有13種不同的遊戲過程,先取者贏的概率爲7/13,後取者贏的概率爲6/13。更進一步,如果要制定最有把握勝的策略的話,這件事只要交給計算機去做好了。先把所有狀態空間都存起來,每當對手取完後,就找到取完那個狀態的節點,然後遍歷那個節點的子樹以計算出勝率最大的下一步操作。
這裏已經略微涉及到人機博弈的概念了(這個我還沒有研究過),我想基礎的東西肯定是大同小異的,只是複雜度提高了。大名鼎鼎的“深藍”挑戰國際象棋冠軍,它裏面存有了200萬張不同的棋局,配有200多個獨立的計算器(應該也是ALU吧),不管對手怎麼動,它都能在它的庫存中找到對應的最佳解法。
下面我們開始介紹真正的Uninformed Search方法吧。
1. Depth-First Search(DFS)深度優先
經典算法啊,想必大家都懂的,這裏我只想提一下它的非遞歸實現方法。
還是要使用一個Stack
(1)把根節點入棧
(2)Pop一下,訪問該節點,然後把它的右子女和左子女(不爲NULL)依次Push進去
(3)重複(2),直到找到目標節點或棧爲空(沒有找到)。
爲了避免環的出現而使算法陷入死循環,可以引入一個visited數組。
顯然,DFS算法雖然一定能找到出現在圖中的節點,但效率很低。若要找F,DFS就相當於把整個圖都遍歷了一遍。
2. Depth-Limited Search(DLS)
DLS其實是DFS的變體,它限制住了訪問的深度,在DFS算法的第(2)步中,若該節點當前深度大於限制深度,那就不再把它的子女Push進去了。
爲了實現此算法,又多開闢了一個深度Stack,它與節點Stack同步進行操作,Push節點的時候也把當前的深度Push進去,Pop同理。
顯然,DLS未必能找到目標節點。
3. Iterative Deepening Search(IDS)
IDS其實又是DLS的變體了,它通過depth的增量(初始值和增量大小可視情況而定),迭代進行DLS算法,直到找到目標節點。
顯然當目標節點很深的情況下,IDS算法的效率肯定不如直接DFS的。但當深度很大,而且標節點處於中間位置或較淺位置時,或者深度未知的情況下,就能體現出IDS算法的價值了。
4. Breadth-First Search(BFS)廣度優先
也是經典啊,通過隊列實現,不多說了。只說下,BFS也可以像DFS一樣引入一個visited數組,不是爲了避免死循環,而是可以避免有環的情況下重複訪問相同的節點,以加速搜索。
5. Uniform-Cost Search(UCS)
這個算法適用與帶權圖中,爲了找出最短路徑。
如圖,找出從A到E的最短路徑。
我們需要一個優先隊列(也就是堆)來實現,裏面存放節點和當前到達該節點總共的Cost,按Cost來把堆調整爲最小堆。
(1)初始節點插入空堆
(2)出堆,訪問該節點,把它所有的子女(不爲NULL)依次入堆(Cost=當前節點的Cost + 與子女邊的權)
(3)重複(2),直到出堆的節點即爲目標節點,或堆爲空了(沒有找到)。
注意,因爲是堆,所以每次入堆出堆操作堆內都會進行調整。
這裏我們再來討論一種backtracking技術,因爲問題是通過此算法,我們只能找到最短路徑的長度是多少,而不知道這條路徑經過了哪些節點。其實最簡單的方法就是隻要讓每個節點記住它的parent就可以了。
我們需要的數據結構如下
struct Node
{
int node_id;
int current_total_cost; // current_total_cost = 父節點current_total_cost + 邊上的權
Node* parent;
}
每次取當前節點的子女時,將parent指針指向當前節點就OK了。只要將賦值好的Node插入堆中就行了。
當算法結束時,取出的最後一個節點爲目標節點,然後通過它的parent指針以及parent的parent指針(迭代)一路可以找到初始節點(初始節點的parent當然爲NULL),然後按正向輸入節點id就OK了。
6. Bidirectional Search 雙向搜索
顧名思義,就是從兩端同時搜索。但它有個前提條件,必須知道目標節點在哪(有指針指向它)。
它的基本思想是兩端都採用BFS搜索,直到兩條路徑碰頭。但現實中,若都採用BFS搜索,往往是不會碰頭的,即使碰頭的話,效率也非常低。
現實中的圖往往復雜到這樣
所以一般兩端都採用一種Informed Search叫A* Search(我們將在下篇中介紹)
Summary
Algorithm |
Time Complexity |
Space Complexity |
Derivative |
DFS |
O(bm) |
O(bm) |
|
DLS |
O(bl) |
O(bl) |
DFS |
IDS |
O(bd) |
O(bd) |
DLS |
BFS |
O(bd) |
O(bd) |
|
UCS |
O(bd) |
O(bd) |
BFS |
BIDI(Bidirectional) |
O(bd/2) |
O(bd/2) |
BFS(兩端採用BFS) |
b, branching factor
d, tree depth of the solution
m, tree depth
l, search depth limit
綜上這些算法,除了UCS(通過優先隊列來選擇當前Cost最小的節點優先訪問)帶有一點策略性,(BIDI當兩端採用A* Search時也具有策略性),其他算法都是不具備決策能力的盲目式搜索,當數據量很大時,顯然是低效的。