通過搜索來解決問題
狄更斯的《雙城記》中第一句話是: 這是最好的時代,也是最壞的時代。
這本首次出版於1859年的書距離我們已經150多年了。如果狄更斯來到今天,我們可以堅定地告訴他:“這是最好的時代,也是最壞的時代。”
因為我們正在從信息時代跨入智能時代。曾經,我們要學習如何操作機器,掌握機器的語言,向機器靠攏。而今天,機器在向人類靠攏,試圖理解人類、用我們的語言與我們對話。這就是“智能時代”,這個時代的基礎是數據和算法。
我們還沒有經歷過機器在智能上全面超越人類的時代,我們需要在這樣的環境中學會生存。這將是一個讓我們振奮的時代,也是一個給我們帶來空前挑戰的時代。
這兩年,出盡風頭的人工智能是阿爾法狗。阿爾法狗(AlphaGo)是第一個擊敗人類職業圍棋選手、第一個戰勝圍棋世界冠軍的人工智能程序,由谷歌(Google)旗下DeepMind公司戴密斯·哈薩比斯領銜的團隊開發。具體到下棋的策略,阿爾法狗里面有兩個關鍵的技術:
第一個是把棋盤上當前的狀態變成一個獲勝概率的數學模型,這個模型的關鍵是靠大數據訓練(學習)出來的。
第二個是啟發式搜索算法——蒙特卡洛樹搜索算法(Monte Carlo Tree Search),這個算法能將搜索的空間限制在非常有限的范圍內,保證計算機能夠快速找到好的下法。
由此可見,下圍棋這個看似智能型的問題,從本質上講,是一個大數據和算法的問題。
數據和算法是驅動智能時代的兩架馬車。“大數據”這個時髦的詞聽得我們耳朵都要長繭了。今天我們不說數據,來談談算法,並重點引入上文提到的搜索算法及其應用。
1. 什么是算法?
在很多人眼里,很多人認為算法是這樣的:
或者是這樣的:
恭喜你,你的感覺是對的😹😂
但是你想過沒有,最開始的它其實是這樣的,又萌又直接:<{=....(嘎嘎嘎~)
這個圖詮釋了納蘭性德的詞:“人生若只如初見”,再回首:“哇的一聲哭出來~ ”
誰si誰知道~,憋着"你行你上"內心,可能是這樣的:
這說明什么的呢?我們還是得從基礎入手,而不是一步跳到算法的深海中去,不信來看看😂
按照老傳統,我們對算法給出一個定義,算法在百度百科中的定義如下:
算法(Algorithm)是指解題方案的准確而完整的描述,是一系列解決問題的清晰指令,算法代表着用系統的方法描述解決問題的策略機制。
一個算法應該具有以下五個重要的特征:
有窮性:(Finiteness)算法的有窮性是指算法必須能在執行有限個步驟之后終止;
確切性:(Definiteness)算法的每一步驟必須有確切的定義;
輸入項:(Input)一個算法有0個或多個輸入,以刻畫運算對象的初始情況,所謂0個輸入是指算法本身定出了初始條件;
輸出項:(Output)一個算法有一個或多個輸出,以反映對輸入數據加工后的結果。沒有輸出的算法是毫無意義的;
可行性:(Effectiveness)算法中執行的任何計算步驟都是可以被分解為基本的可執行的操作步,即每個計算步都可以在有限時間內完成(也稱之為有效性)。
是不是看着很迷糊?是的,定義總是那么晦澀難懂,先不用管它。簡單一句話總結:算法就是解決問題的過程。
這個聽起來高大上的詞,實際上在我們生活中隨處可見,每一天我們都會用到各種算法,只是你不知道而已。
比如菜譜就是一個算法。它就是一個飯菜制作的流程,解決我們如何做飯的問題。
按照算法的定義,菜譜具有算法的五個特征。
舉個栗子:
菜譜:拍黃瓜
1、黃瓜洗干凈。
2、放在案板上,用刀拍開,切小塊。
3、蒜切末,花生用刀碾碎。
4、黃瓜加入蒜末、花生粒、生抽、芝麻油、香醋,充分攪拌均勻入味,即可。
有窮性:拍黃瓜,有4個步驟
確切性:每個步驟目標明確。
輸入項:黃瓜、蒜、花生、生抽、芝麻油、香醋
輸出項:拍黃瓜這道菜
可行性:每個步驟目標明確,能夠在有限時間內完成。(拍個黃瓜也就5分鍾)
其實,菜譜也好,說明書也好,人生的取舍與選擇也好,商業戰略決策也好,這些都是一種算法問題。
舉兩個生活中應用簡單算法的例子:
比如打印一個20頁的文件,需要打2份。
打印出來的文件排序是:“112233445566….2020”。現在你開始分成兩份,正常情況下,你會左一張右一張這樣分成兩摞。
實際上有個更簡易的分法:先在左邊分1張,然后右邊分兩張,再左邊分兩張,再右邊分兩張……最后一張分左邊。排序如下:[1,12,23,34,45…1920,20]
通過采用算法的優化,一次兩張的分法,工作量一下減少了一半。
再比如,快速排序算法:
作為一名有前途的圖書管理員,你需要把還回來的一堆書(比如有100本)按順序入架。你該怎么做?
傳統的辦法:
一本一本書按照序號還回到書架。100本書,你需要跑100次。經過算法優化的方法:
先從這堆書里隨便挑出來一本,
把比它號小的扔左邊,比它號大的扔右邊。
分成兩堆后,再重復上面的步驟。
從小到大排序后的書,按照書架順序歸類。
每個書架跑一次,可能只需要跑10次就完成了。
通過采用算法的優化,工作量能夠減少了一大半。
從以上生活中的例子能看出來,自從人類有文明以來,我們就一直在發明、使用和傳播各種各樣的算法,用於衣、食、住、行,等等。
上面的例子中算法雖然有用,但是我們用笨方法也能完成任務。
然而,在人生中,很多時候會面臨選擇的問題。人生沒有回頭路,用笨方法的人往往會“后悔”和“錯過”。尤其是在今天這個智能時代,不掌握點有用的算法,最好的時代也會變成最壞的時代。因為不掌握點算法,你可能終被算法替代,退一步,就算終將被替代,但是不掌握點算法的話,你會連被誰干掉的都不知道。
算法並非猛獸,但是它可以成為猛獸!言歸正傳,了解什么是算法之后,進入我們今天的主題,搜索算法,那什么是搜索算法呢?以及它能干什么呢?帶着這些個問題,我們一探究竟。
2. 什么是搜索?
生活中我們常常遇到關於搜索的問題,比如下圖,現在想要從同花順大樓開車到杭州東站,能到達的路線會有很多,那怎么找出最快的路線呢?
另一個例子,經典的華容道游戲,如何用最少的步驟讓曹操移動到棋盤的最下部?
如何解決類似這樣的方方面面的問題呢?
3. 搜索算法
老規矩,我們先給出搜索算法定義,有一個先驗印象:
搜索算法是利用計算機的高性能來有目的的窮舉一個問題解空間的部分或所有的可能情況,從而求出問題的解的一種方法。現階段一般有枚舉算法、深度優先搜索(DFS)、廣度優先搜索(BFS)、A*算法、回溯算法、蒙特卡洛樹搜索、散列函數等算法。
通過搜索我們可以解決許多在生活中、科研和工程領域有趣的問題,搜索算法可以被分為以下兩種:
- 盲目的搜索算法,或稱無信息圖搜索算法
- 除了問題的定義之外,沒有給出其他信息。雖然可以解決問題,但不一定是最優解,這類算法常用的有:
- 廣度優先搜索
- 深度優先搜索
- 除了問題的定義之外,沒有給出其他信息。雖然可以解決問題,但不一定是最優解,這類算法常用的有:
- 啟發式搜索算法,又稱為有信息的圖搜索算法
- 給出了一些指引,根據提示找到最佳方案,常用的有:
- 貪婪最佳優先算法
- A*尋路算法
- 給出了一些指引,根據提示找到最佳方案,常用的有:
接下來我們以例子的形式講解搜索過程,即上述算法。
3.1 如何做路徑規划?
羅馬尼亞度假問題
羅馬里亞度假問題是一個經典的路徑規划問題,即搜索最佳路徑。
搜索問題通常可以用圖來表示,本例為示例,實際生活中地圖的路徑規划問題要復雜的多。
假設我們目前在Arad,明天要趕往布加勒斯特(Bucharest),現在需要找出一條通往布加勒斯特的最短路徑。
為了方便描述,我們做了如下約定:
初始狀態: 開始的初始狀態,例如我們在羅馬尼亞的初始狀態是 In(Arad)
當前所在為位置。
動作: 描述可能采取的操作。給定一個狀態s,Actions(s)返回在s中可以執行的一組操作,例如在狀態 In(Arad)中,可以采取的操作是到三個城市 {Go(Sibiu)、Go(Timisoara)、Go(Zerind)}
當前位置情況下,可以去的城市路徑。
轉換模型: 對每個動作的描述, 它由一個函數Result(s,a)指定,該模型返回在狀態中執行動作a后所產生的狀態。RESULT(In(Arad),Go(Zerind)) = In(Zerind)
可以簡單理解為已經走過的路徑。
圖(路徑): 初始狀態、動作和轉換模型隱式定義問題的狀態空間
由任意一系列動作從初始狀態可到達的所有狀態集。狀態空間形成有向網絡或圖,其中節點是狀態,節點之間的鏈接是動作。
可以簡單理解為上圖的地圖。
目標測試: 測試給定狀態是否為目標狀態。在這個問題中,我們的目標是 set{In(Bucharest)}。
走過的路徑是否為最短路徑。
路徑成本: 到達目的地的一條路徑的成本可能是以公里為單位的數值。
所走路徑的路程長度。
3.2 搜索過程
明了問題之后,如何解決呢?
在搜索過程中,從初始位置開始實際上是建立了一棵搜索樹,在搜索過程中樹的分支對應於已探測的路徑。
搜索樹中的分支,實際上對應的是圖中的一個分支,如下圖的對應關系。
3.3 通用搜索算法
我們已經知道搜索的過程是建立一棵搜索樹的過程,那么針對搜索問題,我們可以抽象一個通用的搜索算法:
General-search (problem, strategy)
init() //根據問題的初始狀態初始化搜索樹
loop
if 沒有可進一步可探測的候選節點
return failure
根據算法選擇下一個節點來探測
if 這個節點滿足目標條件
return 最終方案
展開節點並將其所有后繼節點添加到搜索樹
end loop
看不懂?!沒關系,客官不要急,后面都會懂的🎃
下圖中,初始點位置為Arad,候選的城市有三個:Zerind,Sibiu,Timisoara。
接下來的一步中,如果選擇了Zerind。
判斷選擇的點是不是目的地,根據任務描述,這顯然不是終點,則繼續探索路徑。
接下來候選點(城市)變為Oradea,Sibiu,Timisoara。
在繼續往下探索的過程中,大家可能會發現一個問題,那我是先選擇Oradea這條路呢,還是選擇Sibiu或者Timisoara這條路?這就涉及到我們的遍歷方法了。
不同搜索方法在探索空間的方式上有所不同,也就是他們如何選擇下一步要擴展的節點方式不同!
下面我們介紹一下主要的搜索遍歷方式
3.4 盲目的搜索算法
3.4.1 深度優先遍歷(Deep First Search)
顧名思義,這種遍歷方法是以深度為優先對圖進行搜索或者遍歷,我們可以先看下DFS的基本步驟:
從當前節點開始,先標記當前節點,再尋找與當前節點相鄰,且未標記過的節點:
(1)當前節點不存在下一個節點,則返回前一個節點進行DFS
(2)當前節點存在下一個節點,則從下一個節點進行DFS
概括得通俗一點就是 “順着起點往下走,直到無路可走就退回去找下一條路徑,直到走完所有的結點。”
我們用圖的方式來演示一下DFS的過程。
一開始,可以看出,若沒有走到最終的葉子節點,這種遍歷方式會從start節點沿着一條路一直深入遍歷下去(start -> 1 -> 2 -> 3)。
若走到葉子節點,便會退回上一節點,遍歷上一節點的其他相鄰節點(2 ->3-> 4)。
這樣一直重復,直到找到最終目標節點。
如你所見到的一樣,這樣的搜索方法像一根貪婪的蚯蚓,喜歡往深的地方鑽,所以就自然而然的叫做深度優先算法了。
我們可以順便寫出DFS的偽代碼:
DFS_find(節點){
if(此結點已經遍歷 || 此節點在圖外 || 節點不滿足要求) return;
if(找到了end節點) 輸出結果 ; return;
標記此節點,表示已經遍歷過了;
while(存在下一個相鄰節點) find(下一個節點);
}
思考: 如果樹的分支無窮深,那怎么辦?
由於一個有解的問題樹可能含有無窮分枝,深度優先搜索如果誤入無窮分枝(即深度無限),則不可能找到目標節點。為了避免這種情況的出現,在實施這一方法時,需要定出一個深度界限,在搜索達到這一深度界限而且尚未找到目標時,即返回重找,所以深度優先搜索算法是不完備的。另外,應用此算法得到的解不一定是最佳解(最短路徑)。
3.4.2 廣度優先遍歷(BFS)
對於深度優先算法,強迫症就很不爽了,並表示:“為什么不干干凈凈,一層一層地從start節點搜索下去呢,就像病毒感染一樣,這樣才像正常的搜索的樣子嘛!”於是便有了BFS算法。廣度優先算法便如其名字,它是以廣度為優先的,一層一層搜索下去的。
BFS總是先訪問完同一層的結點,然后才繼續訪問下一層結點,它最有用的性質是可以遍歷一次就生成中心結點到所遍歷結點的最短路徑,這一點在求無權圖的最短路徑時非常有用。
還是以圖的方式來演示,下圖中start為搜索的初始節點,end為目標節點:
我們先把start節點的相關節點遍歷一次
接下來把第一步遍歷過的節點當成start節點,重復第一步
一直重復一二步,這樣便是一個放射樣式的搜索方法,直到找到end節點
可以看出,這樣放射性的尋找方式,能找到從start到end的最短路徑(因為每次只走一步,且把所有的可能都走了,誰先到end說明這就是最短路)。
從實現的角度上,在廣度優先遍歷的過程中,我們需要用到隊列:
1. 首先擴展最淺的節點
2. 將后繼節點放入隊列的末尾(FIFO)
BFS是從根節點(起始節點)開始,按層進行搜索,也就是按層來擴展節點。所謂按層擴展,就是前一層的節點擴展完畢后才進行下一層節點的擴展,直到得到目標節點為止。
我們可以寫出BFS的偽代碼
BFS_find(start節點){
把start節點push入隊列;
while(隊列不為空) {
把隊列首節點pop出隊列;
對節點進行相關處理或者判斷;
while(此節點有下一個相關節點){
把相關節點push入對列;
}
}
}
小結:
一般情況下,深度優先適合深度大的樹,不適合廣度大的樹,廣度優先則正好相反。
所謂深度大的樹就是指起始節點到目標節點的中間節點多的樹(可以理解成問題有很多中間解,這些解都可以認為是部分正確的,但要得到完全正確的結果——目標節點,就必須先依次求出這些中間解)。
所謂廣度大的樹就是指起始節點到目標節點的可能節點很多的樹(可以理解成問題有很多可能解,這些解要么正確,要么錯誤。要得到完全正確的結果——目標節點,就必須依次判斷這些可能解是否正確)。
多數情況下,深度優先搜索的效率要高於寬度優先搜索。但某些時候,對於這兩種搜索算法的優劣(或效率)還需要針對不同的問題進行具體分析比較。
擴展: BFS和DFS在搜索引擎中的應用
進一步的,我們做一點擴展,看看BFS和DFS在搜索引擎中的應用。
通常,搜索引擎爬蟲會從全網抓取網頁,但是全網有數十萬億的網頁,谷歌和百度之類的搜索引擎是如何下載整個互聯網中的全部網頁呢? 是用BFS還是DFS呢?
現在的互聯網非常龐大,今天Google的索引中有超過1萬億個網頁,即使更新最頻繁的基礎索引也有幾百億個網頁,假如下載一個網頁需要一秒鍾,下載這100億個網頁則需要317年。
我們已經了解了BFS和DFS的原理,雖然從理論上講,這兩個算法(在不考慮時間因素的前提下)都能夠在大致相同的時間里“爬下”整個“靜態”互聯網上的內容,但這只是理論上的可行性,它有兩個假設——不考慮時間因素,互聯網靜態不變,都是現實中做不到的。
搜索引擎的網絡爬蟲問題更應該定義成“如何在有限時間里最多地爬下最重要的網頁”。顯然各個網站最重要的網頁應該是它的首頁。在最極端的情況下,如果爬蟲非常小,只能下載非常有限的網頁,那么應該下載的是所有網站的首頁,如果把爬蟲再擴大些,應該爬下從首頁直接鏈接的網頁(就如同和北京直接相連的城市),因為這些網頁是網站設計者自己認為相當重要的網頁。在這個前提下,顯然BFS明顯優於DFS。
事實上在搜索引擎的爬蟲里,雖然不是簡單地采用BFS,但是先爬哪個網頁,后爬哪個網頁的調度程序,原理上基本上是BFS。 那么是否DFS就不使用了呢?也不是這樣的。實際的網絡爬蟲都是一個由成百上千甚至成千上萬台服務器組成的分布式系統。對於某個網站,一般是由特定的一台或者幾台服務器專門下載。這些服務器下載完一個網站,然后再進入下一個網站,而不是每個網站先輪流下載5%,然后再回過頭來下載第二批。這樣可以避免握手的次數太多。如果是下載完第一個網站再下載第二個,那么這又有點像DFS,雖然下載同一個網站(或者子網站)時,還是需要用BFS的。
網絡爬蟲對網頁遍歷的次序不是簡單的BFS或者DFS,而是有一個相對復雜的下載優先級排序的方法。管理這個優先級排序的子系統一般稱為調度系統(Scheduler),由它來決定當一個網頁下載完成后,接下來下載哪一個。
擴展閱讀:
3.4.3 Dijkstra 算法
寬度優先算法,解決了起始頂點到目標頂點路徑規划問題,但不是最優以及合適的,因為它的邊沒有權值(比如距離),路徑無法進行估算比較最優解。為何權值這么重要,因為真實環境中,2個城市之間的路線並非一直都是直線,需要繞過障礙物才能達到目的地,比如森林,湖水,高山,都需要繞過而行,並非直接穿過。
比如我采用寬度優先算法,遇到如下情況,他會直接穿過障礙物(綠色部分),明顯這個不是我們想要的結果:因為這個規划在實際中是一個錯誤的結果。
那么如何解決帶有權重時的尋找最佳路徑的問題呢?
尋找圖中一個頂點到另一個頂點的最短以及最小帶權路徑是非常重要的提煉過程。為每個頂點之間的邊增加一個權值,用來跟蹤所選路徑的消耗成本(比如上文提到的城市之間的路程),如果位置的新路徑比先前的最佳路徑更好,我們就將它放到新的路線中。
Dijkstra 算法基於BFS算法進行改進,把當前看起來最短的邊加入最短路徑樹中,利用貪心算法計算並最終能夠產生最優結果的算法。具體步驟如下:
貪心算法,正如它的名字,貪心,每次都選擇局部的最優解,並不考慮這個局部最優選擇對全局的影響。
1、每個頂點都包含一個預估值cost(起點到當前頂點的距離),每條邊都有權值v,初始時,只有起始頂點的預估值cost為0,其他頂點的預估值d都為無窮大 ∞。
2、查找cost值最小的頂點A,放入path隊列
3、循環A的直接子頂點,獲取子頂點當前cost值命名為current_cost,並計算新路徑new_cost,new_cost=父節點A的cost v(父節點到當前節點的邊權值),如果new_cost<current_cost,當前頂點的cost=new_cost
4、重復2,3直至沒有頂點可以訪問.
我們看到雖然Dijkstra 算法相對於寬度優先搜索更加智能,基於cost_so_far ,可以規避路線比較長或者無法行走的區域,但依然會存在盲目搜索的傾向,我們在地圖中常見的情況是查找目標和起始點的路徑,具有一定的方向性,而Dijkstra 算法從上述的圖中可以看到,也是基於起點向子節點全方位擴散。
也就是對於Dijkstra算法來說,對全部路徑進行了一遍查找,這其實是花費了非常多的工作量的,這種搜索具有有一定的盲目性,那么有沒有更好的方法能夠解決這種盲目性呢?答案是肯定的,那就是啟發式搜索算法。
3.5 啟發式搜索算法(有信息的圖搜索算法)
在啟發式搜索算法中,在問題本身定義之外給出了一些指引,能夠比無信息的圖搜索算法(如DFS、BFS等)更有效地找到解決方案。
比較經典的啟發式搜索算法有貪婪最佳優先算法和A*尋路算法等,在游戲中應用比較廣泛。
所有尋路算法都需要一種方法以數學的方式估算某個節點是否應該被選擇。大多數游戲都會使用啟發式(heuristic) ,以 h(x) 表示,就是估算從某個位置到目標位置的開銷。理想情況下,啟發式結果越接近真實越好。
——《游戲編程算法與技巧》
3.5.1 貪婪最佳優先搜索算法(Greedy Best-First Search)
如上所述,貪心算法的含義是求解問題時,總是做出在當前來說最好的選擇。通俗點說就是,這是一個“短視”的算法。
搜索的每一步,都會查找相鄰的節點,計算它們距離終點的曼哈頓距離,即最低開銷的啟發式。
曼哈頓距離——兩點在南北方向上的距離加上在東西方向上的距離,即h(i,j)=|xi-xj|+|yi-yj|。 對於一個具有正南正北、正東正西方向規則布局的城鎮街道,從一點到達另一點的距離正是在南北方向上旅行的距離加上在東西方向上旅行的距離,因此,曼哈頓距離又稱為出租車距離。
在Dijkstra算法中,我們已經發現了其最致命的缺陷:搜索存在盲目性。而貪婪最佳優先搜索在障礙物少的時候足夠的快,但最佳優先搜索得到的都是次優的路徑。舉例來說,如果目標節點在起始點的南方,那么貪婪最佳優先搜索算法會將注意力集中在向南的路徑上。
如下圖,算法不斷地尋找當前 h(啟發式)最小的值,但這條路徑很明顯不是最優的。貪心最好優先算法雖然做了較少的計算,但卻並不能找到一條較好的路徑。
從上圖中我們可以明顯看到右邊的算法(貪婪最佳優先搜索)尋找速度要快於左側,雖然它的路徑不是最優和最短的,但障礙物最少的時候,他的速度卻足夠的快。這就是貪心算法的優勢,基於目標去搜索,而不是完全搜索。
那么這種算法有沒有缺點呢? 肯定是有的,那就是得到的路徑不是最短路徑,只能是較優。
如何在搜索盡量少的頂點同時保證最短路徑?我們來看A*算法。
3.5.2 基於BFS的A*尋路算法
從上面算法的演進,我們逐漸找到了最短路徑和搜索頂點最少數量的兩種方案,Dijkstra 算法和 貪婪最佳優先搜索。那么我們有沒有可能汲取兩種算法的優勢,令尋路搜索算法即便快速又高效?
答案是可以的,A*算法正是這么做了,它吸取了Dijkstra 算法中的cost_so_far,為每個邊長設置權值,不停的計算每個頂點到起始頂點的距離,以獲得最短路線,同時也汲取貪婪最佳優先搜索算法中不斷向目標前進優勢,並持續計算每個頂點到目標頂點的距離,以引導搜索隊列不斷想目標逼近,從而搜索更少的頂點,保持尋路的高效。
假設g(n)表示從起點到任意節點n的路徑長度,h(n)表示從節點n到目標節點路徑花費的估計值(啟發值)。在上面的圖中,黃色體現了節點距離目標較遠,而青色體現了節點距離起點較遠。A* 算法在物體移動的同時平衡這兩者的值。定義f(n)=g(n)+h(n),A*算法將每次檢測具有最小f(n)值的節點。然后朝着f(n)最小的點移動。
以下分別是Dijkstra算法,貪婪最佳優先搜索算法,以及A*算法的尋路雷達圖,其中格子有數字標識已經被搜索了,可以對比下三種效率:
好了,到現在為止我們介紹了盲目的圖搜索算法(DFS、BFS、Dijkstra), 啟發式搜索算法(貪婪最佳優先、A*),你們是否已經對搜索算法有所了解了呢?
聲明
本博客所有內容僅供學習,不為商用,如有侵權,請聯系博主謝謝。
參考文獻
[1] 人工智能:一種現代的方法(第3版)
[2] 吳軍說谷歌爬蟲技術
[3] A* Pathfinding for Beginners
[4] A*’s Use of the Heuristic
[5] Solving problems by searching
[6] 夜深人靜寫算法