前 面已經寫了一些關於烙餅問題的簡單分析,但因為那天太累有些意猶未盡,今天再充實一些內容那這個問題研究透。我想,通過這篇文章,我們就可以把這一類問題 搞懂。再遇到優化問題,如果我們想不到別的辦法,就可以採用搜尋樹演算法來解決,至少我們不至於拿不出解決方案。前面我們已經知道,關於一摞烙餅的排序問題 我們可以採用遞迴的方式來完成。其間我們要做的是盡量調整
upperbound
和lowerbound
,已減少運算次數。對於這種方法,在演算法課中我們應該稱之為:
tree searching strategy
。即整個解空間為一棵搜尋樹,我們按照一定的策略遍歷解空間,並尋找最優解。一旦找到比當前最優解更好的解,就用它替換當前最優解,並用它來進行「剪枝」操作來加速求解過程。
書中給出的解法就是採用深度優先的方式來遍歷這棵搜尋樹,例如要排序
[4,2,1,3]
,最大反轉次數不應該超過
(4-1)*2=6
次,所以搜尋樹的深度也不應大於
6,搜尋樹如下圖所示:
這裡只列到第三層,其中被畫斜線的方塊由於和上層的某一節點的狀態重複而無需再擴充套件下去(即便擴充套件也不可能比有相同狀態的上層節點的代價少)。我們可以看到在右子樹中的乙個分支,只需要用
3次反轉即可完成,我們的目標是如何更為快速有效的找到這一分支。直觀上我們可以看到:基本的搜尋方法要先從左子樹開始,所以要找到本例最佳的方案的代價是很高的(利用書中的演算法需要查詢
292次)。
既然要遍歷搜尋樹,就有廣度優先和深度優先之分,可以分別用棧和佇列來實現(當然也可以用遞迴的方法)。那麼如何能更有效地解決問題呢?我們主要考慮一下幾種方法:
(1)爬山法
該方法是在深度優先的搜尋過程中使用貪心方法確定搜尋方向,它實際上是一種深度優先搜尋策略。爬山法採用啟發式側讀來排序節點的擴充套件順序,其關鍵點就在於測度函式
f(n)
的定義。我們來看一下如何為上例定製代價函式
f(n)
,以快速找到右子樹中最好的那個分支(很像貪心演算法,呵呵)。
我們看到在
[1,2,4,3]
中,[1,2,3]
已經相對有序,而
[4]位與他們之間,要想另整體有序,需要
4次反轉;而
[3,1,2,4]
中,由於
[4]已經就位,剩下的數變成了長度為
3的子佇列,而子佇列中
[1,2]
有序,令其全體有序只需要
2次反轉。
所以我們的代價函式應該如下定義:
1 從當前狀態的最後乙個餅開始搜尋,如果該餅在其應該在的位置(中間斷開不算),則跳過;
2 自後向前的搜尋過程中,如果碰到兩個數不相鄰的情況,就+1
這樣我們就可以在本例中迅速找到最優分枝。因為在樹的第一層
f(2,4,1,3)=3
,f(1,2,4,3)=2
,f(3,1,2,4)=1
,所以我們選擇
[3,1,2,4]
那一枝,而在
[3,1,2,4]
f(1,3,2,4)=2
,f(2,1,3,4)=1
,f(4,2,1,3)=2
,所以我們又找到了最佳的路徑。
上面方法看似不錯,但是數字比較多的時候呢?我們來看書中給出的
10個數的例子:
[3,2,1,6,5,4,9,8,7,0]
,程式給出的最佳翻轉序列為(從0
開始算起)
那麼,對於搜尋樹的第一層,按照上面的演算法我計算的結果如下:
f(2,3,1,6,5,4,9,8,7,0)=4
f(1,2,3,6,5,4,9,8,7,0)=3
f(6,1,2,3,5,4,9,8,7,0)=4
f(5,6,1,2,3,4,9,8,7,0)=3 f(
4,5,6,1,2,3,9,8,7,0)=3
f(9,4,5,6,1,2,3,8,7,0)=4
f(8,9,4,5,6,1,2,3,7,0)=4
f(7,8,9,4,5,6,1,2,3,0)=3
f(0,7,8,9,4,5,6,1,2,3)=3
我們看到有
4個分支的結果和最佳結果相同,也就是說,我們目前的代價函式還不夠「一擊致命」,但是這已經比書中的結果要好一些,起碼我們能更快地找到最佳方案,這使得我們在此後的剪枝過程更加高效。
爬山法的偽**如下:
1 構造由根組成的單元素棧s
2 if top(s)
是目標節點
then
停止;3 pop(s);
4 s的子節點按照啟發式測度,由小到大的順序壓入s
5 if
棧空then
失敗else 返回2
如果有時間我會把爬山法解決的烙餅問題貼在後面。
(2)best-first
搜尋策略
最佳優先搜尋策略結合了深度優先和廣度優先二者的優點,它採取的策略是根據評價函式,在目前產生的所有節點中選擇具有最小代價值的節點進行擴充套件。該策略具有全域性優化的觀念,而爬山法則只具有區域性優化的能力。具體用小根堆來實現搜尋樹就可以了,這裡不再贅述。
(3)a*
演算法如果我們把下棋比喻成解決問題,則爬山法和
best-first
演算法就是兩個只能「看」未來一步棋的玩家。而
a*演算法則至少能夠「看」到未來的兩步棋。
我們知道,搜尋樹的每乙個節點的代價
f*(n)=g(n)+h*(n)
。其中,
g(n)
為從根節點到節點
n的代價,這個值我們是可求的;
h*(n)
則是從n
節點到目標節點的代價,這個值我們是無法實際算出的,只能進行估計。我們可以用下一層節點代價的最小者來替代
h*(n)
,這也就是「看」了兩步棋。可以證明,如果
a*演算法找到了乙個解,那它一定是優化解。
a*演算法的描述如下:
1.使用
bestfirst
搜尋樹2.
按照上面所述對下層點
n進行計算獲得
f*(n)
的估計值
f(n)
,並取其最小者進行擴充套件。
3.若找到目標節點,則演算法停止,返回優化解
總結:歸根到底,烙餅問題之所以難於在多項式時間內解決的關鍵就在於我們無法為搜尋樹中的每一條邊設定乙個合理的權值。在這裡,每條邊的權值都是
1, 因為從上乙個狀態節點到下乙個狀態節點之需要一次翻轉。所以我們不能簡單地把每個節點的代價定義為翻轉次數,而應該根據其距離最終解的接近程度來給出乙個 數值,而這恰恰就是該問題的難點。但是無論上面哪一種方法,都需要我們確定搜尋樹各個邊的代價是多少,然後才能進行要麼廣度優先、要麼深度優先、要麼
a*演算法的估計代價。所以,在給出乙個合理的代價之前,我們所有的努力都只能是幫忙「加速」,而無法真正在多項式時間內解決問題。
程式設計之美 烙餅問題
把一摞烙餅按大的在下,小的在上拍好,乙隻手一次只能抓住上面的幾張餅,把它們上下顛倒個個。反覆幾次後把餅排好。問把餅排好需要的最小的次數。問題 是看看把餅排好需要的最小次數。找最優解的問題,可以想到用窮舉法。用遞迴的方式去遍歷所有的翻轉方式。然後找到最小的值。可以先把最上面的兩張翻一下,把次數加一,看...
《程式設計之美》讀書筆記
程式設計之美 讀書筆記 一 中國象棋將帥問題 程式設計之美 讀書筆記 二 求二進位制數中1的個數 擴充套件問題 程式設計之美 讀書筆記 三 一摞烙餅的排序問題 程式設計之美 讀書筆記 四 買書折扣問題的貪心解法 程式設計之美 讀書筆記 五 飲料 問題 程式設計之美 讀書筆記 六 連連看遊戲設計 程式...
程式設計之美之烙餅問題
程式設計之美之一摞烙餅.cpp 定義控制台應用程式的入口點。不同的程式會得到不同的結果嗎?有個程式能得到不同的結果,但後來證明他的結果是錯的 該演算法有點在於在於找最少的交換次數,不是在於用最少的時間進行排序,此演算法的排序時間不是最少的。因為演算法的查詢時間太長了。include stdafx.h...