問題:
星期五的晚上,一幫同事在希格瑪大廈附近的「硬碟酒吧」多喝了幾杯。程式設計師多喝了幾杯之後談什麼呢?自然是演算法問題。有個同事說:「我以前在餐館打工,顧客經常點非常多的烙餅。店裡的餅大小不一,我習慣在到達顧客飯桌前,把一摞餅按照大小次序擺好——小的在上面,大的在下面。由於我乙隻手托著盤子,只好用另乙隻手,一次抓住最上面的幾塊餅,把它們上下顛倒個個兒,反覆幾次之後,這摞烙餅就排好序了。我後來想,這實際上是個有趣的排序問題:假設有n塊大小不一的烙餅,那最少要翻幾次,才能達到最後大小有序的結果呢?」
你能否寫出乙個程式,對於n塊大小不一的烙餅,輸出最優化的翻餅過程呢?
(1)基本步驟
1、最上面的和下面「未選出的最大」的做一次翻轉——————》最大的跑到上面了
2、最大的和下面未初始化的第乙個翻轉——————》最大的跑到了最下面
3、重複以上過程,直到全部有序
(2)如何改進以上的次數?
關於一摞烙餅的排序問題我們可以採用遞迴的方式來完成。其間我們要做的是盡量調整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*演算法的估計代價。所以,在給出乙個合理的代價之前,我們所有的努力都只能是幫忙「加速」,而無法真正在多項式時間內解決問題。
程式設計之美 一摞烙餅排序
問題 星期五的晚上,一幫同事在希格瑪大廈附近的 硬碟酒吧 多喝了幾杯。程式設計師多喝了幾杯之後談什麼呢?自然是演算法問題。有個同事說 我以前在餐館打工,顧客經常點非常多的烙餅。店裡的餅大小不一,我習慣在到達顧客飯桌前,把一摞餅按照大小次序擺好 小的在上面,大的在下面。由於我乙隻手托著盤子,只好用另乙...
程式設計之美 一摞烙餅的排序
問題描述 星期五的晚上,一幫同事在希格瑪大廈附近的 硬碟酒吧 多喝了幾杯。程式設計師多喝了幾杯之後談什麼呢?自然是演算法問題。有個同事說 我以前在餐館打工,顧客經常點非常多的烙餅。店裡的餅大小不一,我習慣在到達顧客飯桌前,把一摞餅按照大小次序擺好 小的在上面,大的在下面。由於我乙隻手托著盤子,只好用...
程式設計之美 一摞烙餅的排序
先想了乙個最簡單的方法 首先對n個烙餅進行處理,找到最大的那個烙餅,將其之上的進行翻轉,然後對前n個烙餅堆進行翻轉 第二次操作對上面n 1個烙餅進行操作,還是找到n 1個中最大的,將其之上的進行翻轉,然後對前n 1個烙餅堆進行翻轉。這樣總共進行n 1次,每次翻轉兩次orz view code 1 i...