廣度優先搜索詳解
1. 也稱寬度優先搜索,顧名思義,就是將一棵樹一層一層往下搜。
算法首先搜索和s距離為k的所有頂點,然后再去搜索和S距離為k+l的其他頂點。BFS是一種完備策略,即只要問題有解,它就一定可以找到解。並且,廣度優先搜索找到的解,還一定是路徑最短的解。但是它盲目性較大,尤其是當目標節點距初始節點較遠時,將產生許多無用的節點,因此其搜索效率較低。需要保存所有擴展出的狀態,占用的空間大。
一般需求最優解的時候用廣搜。
搜索過程是:從初始節點S0開始逐層向下擴展,在第n層節點還沒有全部搜索完之前,不進入第n+1層節點的搜索。
假設有兩個表:Open表存放待處理節點,Closed表存放處理完節點
Open表中的節點總是按進入的先后排序,先進入Open表的節點排在前面,后進入Open表的節點排在后面。
廣度優先搜索算法如下:(用 QUEUE)
(1) 把初始節點S0放入Open表中;
(2) 如果Open表為空,則問題無解,失敗退出;
(3) 把Open表的第一個節點取出放入Closed表,並記該節點為n;
(4) 考察節點n是否為目標節點。若是,則得到問題的解,成功退出;
(5) 若節點n不可擴展,則轉第(2)步;
(6) 擴展節點n,將其子節點放入Open表的尾部,並為每一個子節點設置指向父節點的指針,然后轉第(2)步。
代碼框架:
BFS()
{
初始化隊列
while(隊列不為空且未找到目標節點)
{
取隊首節點擴展,並將擴展出的節點放入隊尾;
必要時要記住每個節點的父節點;
}
}
2. 判重
新擴展出的節點如果和以前擴展出的節點相同,則則個新節點就不必再考慮。設定判重的方法的時候要注意時間和空間的代價。
方案一:每個節點對應於一個九進制數,則4個字節就能表示一個節點。 ( 228< 99=387,420,489 <229)
判重需要一個標志位序列,每個狀態對應於標志位序列中的1位,標志位為0表示該狀態尚未擴展,為1則說明已經擴展過了
標志位序列可以用字符數組存放。數組的每個元素存放8個狀態的標志位。位序列最多需要99位,因此存放位序列的數組需要99/8 + 1個字節 48,427,562字節
如果某個狀態對應於一個9進制數a,則其標志位就是標志位序列中的第a位(其所屬的數組元素下標是a/8)
方案二:為節點編號
把每個節點都看一個排列,以此排列在全部排列中的位置作為其編號
排列總數:9!=362880
只需要一個整數(4字節)即可存下一個節點
判重用的標志數組只需要362,880字節即可。
此方案比方案1省空間
此方案需要編寫給定排列求序號和給定序號求排列的函數,這些函數的執行速度慢於字符串形式的9進制數到其整型值的互相轉換函數。
時間與空間的權衡
對於狀態數較小的問題,可以用最直接的方式編碼以空間換時間,
對於狀態數太大的問題,需要利用好的編碼方法以時間換空間。
3. 改進
雙向廣度優先搜索:從兩個方向以廣度優先的順序同時擴展。
DBFS算法從兩個方向以廣度優先的順序同時擴展,一個是從起始節點開始擴展,另一個是從目的節點擴展,直到一個擴展隊列中出現另外一個隊列中已經擴展的節點,也就相當於兩個擴展方向出現了交點,那么可以認為我們找到了一條路徑。DBFS算法相對於BFS算法來說,由於采用了從兩個跟開始擴展的方式,搜索樹的深度得到了明顯的減少,所以在算法的時間復雜度和空間復雜度上都有較大的優勢!
DBFS的框架
一、主控函數:
void solve()
{
1. 將起始節點放入隊列q1,將目的節點放入隊列q2;
2. 當 兩個隊列都未空時,作如下循環:
1) 如果隊列q1里的未處理節點比q2中的少(即tail[0]-head[0] < tail[1]-head[1]),則擴展(expand())隊列q1;
2) 否則擴展(expand())隊列q2 (即tail[0]-head[0] >= tail[1]-head[1]時);
3. 如果隊列q1未空,循環擴展(expand())q1直到為空;
4. 如果隊列q2未空,循環擴展(expand())q2直到為空;
}
二、擴展函數
int expand(i) //其中i為隊列的編號(表示q0或者q1)
{
取隊列qi的頭結點H;
對頭節點H的每一個相鄰節點adj,作如下循環:
1 如果adj已經在隊列qi之前的某個位置出現,則
拋棄節點adj;
2 如果adj在隊列qi中不存在[函數 isduplicate(i)]
1)將adj放入隊列qi;
2) 如果adj 在隊列(q(1-i)),也就是另外一個
隊列中出現[函數 isintersect()];
輸出 找到路徑 ;
}
三、判斷當前擴展出的節點是否在另外一個隊列出現,也就是判斷相交的函數:
int isintersect(i,j) //i為隊列編號,j為當前節點在隊列中的指針
{
遍歷隊列,判斷是否存在
}
//【線性遍歷的時間復雜度為O(N),如果用HashTable優化,時間復雜度可以降到O(1)】
A*:啟發式搜索搜索
A*算法基本上與BFS算法相同,但是在擴展出一結點后,要根據估價函數對待擴展的結點排序,從而保證每次擴展的結點都是估價函數最小的結點。
估價函數: f'(n) = g'(n) + h'(n) 這里f'(n)是估價函數,g'(n)是起點到終點的最短路徑值(也稱為最小耗費或最小代價),h'(n)是n到目標的最短路經的啟發值。
由於這個f‘(n)其實是無法預先知道的,所以實際上使用“不在位”數和當前層數之和。
A*算法的基本步驟
建立一個隊列,計算初始結點的估價函數f,並將初始結點入隊,設置隊列頭和尾指針。
取出隊列頭(隊列頭指針所指)的結點,如果該結點是目標結點,則輸出路徑,程序結束。否則對結點進行擴展。
檢查擴展的新結點是否與隊列中的結點重復
若與不能再擴展的結點重復(位於隊列頭指針之前),則將它拋棄;
若新結點與待擴展的結點重復(位於隊列頭指針之后),則比較兩個結點的估價函數中g的大小,保留較小g值的結點。跳至第五步。
如果擴展出的新結點與隊列中的結點不重復,則按照它的估價函數f大小將它插入隊列中的頭結點后待擴展結點的適當位置,使它們按從小到大的順序排列,最后更新隊列尾指針
如果隊列頭的結點還可以擴展,直接返回第二步。否則將隊列頭指針指向下一結點,再返回第二步。
例子解析:
sicily1050
要求:給出5個數和一個目標數,求對5個數用加減乘除四種方法任意計算,能得到的最接近目標數的數。
方法:使用廣度搜索,在廣搜開始前,先比較當前這5個數中離目標數最接近的數,是否為目前得到的最近距離,是就將最近距離更新,並記錄當前這個得到最近距離的數。然后開始廣搜:從第一個數開始,計算第一個數和之后每一個數進行加減乘除后得到的值替換為第一個數,然后將第二個數標為已訪問,再重復這個廣搜過程。
最后得到的那個得到最近距離的數,就是要求的答案。
廣搜代碼:
這里的判重使用的是一個數組,來標記對應的5個數是否已經被訪問。當回退時,記得將標記還原為未訪問。
sicily 1444
每次改變一個數字,且每次改變后的數依然是素數,找出一個四位素數最少經過幾次變換可以得到另一個四位素數。
1. 建素數表
isprime[0] = isprime[1] = false;
for(i = 2; i <= 100; i++)
for(j = i; i*j <= 10000; j++)
if(isprime[i])
isprime[i*j] = false;
2. bfs過程