《程式設計之美》讀書筆記:1.3 一摞烙餅的排序
問題:
星期五的晚上,一幫同事在希格瑪大廈附近的「硬碟酒吧」多喝了幾杯。程式設計師多喝了幾杯之後談什麼呢?自然是演算法問題。有個同事說:「我以前在餐館打工,顧客經常點非常多的烙餅。店裡的餅大小不一,我習慣在到達顧客飯桌前,把一摞餅按照大小次序擺好——小的在上面,大的在下面。由於我乙隻手托著盤子,只好用另乙隻手,一次抓住最上面的幾塊餅,把它們上下顛倒個個兒,反覆幾次之後,這摞烙餅就排好序了。我後來想,這實際上是個有趣的排序問題:假設有n塊大小不一的烙餅,那最少要翻幾次,才能達到最後大小有序的結果呢?」
你能否寫出乙個程式,對於n塊大小不一的烙餅,輸出最優化的翻餅過程呢?
n個烙餅經過翻轉後的所有狀態可組成一棵樹。尋找翻轉最少次數,相當於在樹中搜尋層次最低的某個節點。
2*n-3
(從網上可搜尋到對該上限值最新研究結果:上限值為18/11*n),當然,最好還是直接計算出採用這種方案的翻轉次數做為初始值。
減少遍歷次數:
1 減小「最少翻轉次數上限值」的初始值,採用前面提到的翻轉方案,取其翻轉次數為初始值。
對書中的例子,初始值可以取10。
),不允許翻轉回上次狀態時需搜尋195次,而允許翻轉回上次狀態時只要搜尋116次。
3 如果最後的幾個烙餅已經就位,只須考慮前面的幾個烙餅。
對狀態(0,1,3,4,2,5,6),編號為5和6的烙餅已經就位,只須考慮前5個烙餅,即狀態(0,1,3,4,2)。如果乙個最優解,從某次翻轉開始移動了乙個已經就位的烙餅,且該烙餅後的所有烙餅都已經就位,那麼,對這個解法,從這次翻轉開始得到的一系列狀態,從中移除這個烙餅,得到新的狀態,可以設計出乙個新的解法對應這系列新的狀態。該解法所用的翻轉次數不會比原來的多。
4 估計每個狀態還需要翻轉的最少次數(即下限值),加上當前的深度,如果大等於上限值,就無需繼續遍歷。這個下限值可以這樣確定:從最後乙個位置開始,往前找到第乙個與最終結果位置不同的烙餅編號(也就是說排除最後幾個已經就位的烙餅),從該位置到第乙個位置,計算相鄰的烙餅的編號不連續的次數,再加上1。每次翻轉最多只能使不連續的次數減少1,但很多人會忽略掉這個情況:最大的烙餅沒有就位時,必然需要一次翻轉使其就位,而這次翻轉卻不改變不連續次數。(可以在最後面增加乙個更大的烙餅,使這次翻轉可以改變不連續數。)
如:對狀態(0,1,3,4,2,5,6)等同於狀態(0,1,3,4,2),由於1、3和4、2不連續,因而下限值為2+1=3。下限值也可以這樣確定:在最後面增加乙個已經已就位的最大的烙餅,然後再計算不連續數。
如:(0,1,3,4,2),可以看作(0,1,3,4,2,5),1和3 、4和2 、2和5這三個不連續,下限值為3。
5多數情況下,翻轉次數的上限值越大,搜尋次數就越多。可以採用貪心演算法,通過調整每次所有可能翻轉的優先順序,盡快找到乙個解,從而減少搜尋次數。比如,優先搜尋使「下限值」減少的翻轉,其次是使「下限值」不變的翻轉,最後才搜尋使「下限值」增加的翻轉。對「下限值」不變的翻轉,還可以根據其下次的翻轉對「下限值」的影響,再重新排序。由於進行了優先排序,翻轉回上一次狀態能減少搜尋次數的可能性得到進一步降低。
6 其它剪枝方法:
假設第m次翻轉時,「上限值」為min_swap。
如果在某個位置的翻轉後,「下限值」為k,並且 k+m>=min_swap,則對所有的使新「下限值」kk大等於k的翻轉,都有 kk+m>=min_swap,因而都可以不搜尋。
另外,由於翻轉時,只有兩個位置的改變才對「下限值」有影響,因而可以記錄每個狀態的「下限值」,翻轉時,通過幾次比較,就可以確定新狀態的「下限值」。(判斷不連續次數時,最好寫成-1<=x && x<=1, 而不是x==1 || x==-1。對於 int x; a<=x && x<=b,編譯器可以將其優化為 unsigned (x-a) <= b-a。)
結果:對書上的例子:
翻轉回上次狀態
搜尋函式被呼叫次數
翻轉函式被呼叫次數
1.3_pancake_2
不允許29
66 1.3_pancake_2允許
3374
1.3_pancake_1
不允許195398
1.3_pancake_1允許
116
240
另外,對1.3_pancake_2.cpp的第148行做個簡單的改動:
for (int pos=1, last_swap=cake_swap[step++]; pos0; ++pos){
只是改變了搜尋順序,但卻極大提公升了搜尋效率。對書上的例子,搜尋次數進一步降到11次(實際上前六次搜尋找到了乙個解,後而的幾次用於判斷這個解是是最優解)。遍歷所有可能的排列求第1個……第10個烙餅數所用的總時間,也由原來的38秒降到21秒。
補充:在網上下了《程式設計之美》「第6刷」的源**,結果在編譯時存在以下問題:
1 assert 應該是 assert
2 m_arrswap 未被定義,應該改為m_swaparray
3 init函式兩個for迴圈,後乙個沒定義變數i,應該將i 改為 int i
另外,每執行一次run函式,就會呼叫init函式,就會申請新的記憶體,但卻沒有釋放原來的記憶體,會造成記憶體洩漏。
書上程式的低效主要是由於進行剪枝判斷時,沒有考慮好邊界條件,可進行如下修改:
1 if(step + nestimate > m_nmaxswap) >
改為 >=。
2 判斷下界時,如果最大的烙餅不在最後乙個位置,則要多翻轉一次,因而在lowerbound函式return ret; 前插入一行:
if (pcakearray[ncakecnt-1] != ncakecnt-1) ret++; 。
3 n個烙餅,翻轉最大的n-2烙餅最多需要2*(n-2)次,剩下的2個最多1次,因而上限值為2*n-3,因此,m_nmaxswap初始值可以取2*n-3+1=2*n-2,這樣每步與m_nmaxswap的判斷就可以取大等於號。
4 採用書上提到的確定「上限值」的方法,直接構建乙個初始解,取其翻轉次數為m_nmaxswap的初始值。
1和2任改一處,都能使搜尋次數從172126降到兩萬多,兩處都改,搜尋次數降到3475。若再改動第3處,搜尋次數降到2989;若採用4的方法(此時初始值為10),搜尋次數可降到1045。
程式設計之美 一摞烙餅排序
問題 星期五的晚上,一幫同事在希格瑪大廈附近的 硬碟酒吧 多喝了幾杯。程式設計師多喝了幾杯之後談什麼呢?自然是演算法問題。有個同事說 我以前在餐館打工,顧客經常點非常多的烙餅。店裡的餅大小不一,我習慣在到達顧客飯桌前,把一摞餅按照大小次序擺好 小的在上面,大的在下面。由於我乙隻手托著盤子,只好用另乙...
程式設計之美1 3 一摞烙餅的排序(Python)
問題 星期五的晚上,一幫同事在希格瑪大廈附近的 硬碟酒吧 多喝了幾杯。程式設計師多喝了幾杯之後談什麼呢?自然是演算法問題。有個同事說 我以前在餐館打工,顧客經常點非常多的烙餅。店裡的餅大小不一,我習慣在到達顧客飯桌前,把一摞餅按照大小次序擺好 小的在上面,大的在下面。由於我乙隻手托著盤子,只好用另乙...
程式設計之美 一摞烙餅的排序
問題描述 星期五的晚上,一幫同事在希格瑪大廈附近的 硬碟酒吧 多喝了幾杯。程式設計師多喝了幾杯之後談什麼呢?自然是演算法問題。有個同事說 我以前在餐館打工,顧客經常點非常多的烙餅。店裡的餅大小不一,我習慣在到達顧客飯桌前,把一摞餅按照大小次序擺好 小的在上面,大的在下面。由於我乙隻手托著盤子,只好用...