《程式設計之美》讀書筆記02: 1.3 一摞烙餅的排序
問題:
星期五的晚上,一幫同事在希格瑪大廈附近的「硬碟酒吧」多喝了幾杯。程式設計師多喝了幾杯之後談什麼呢?自然是演算法問題。有個同事說:「我以前在餐館打工,顧客經常點非常多的烙餅。店裡的餅大小不一,我習慣在到達顧客飯桌前,把一摞餅按照大小次序擺好——小的在上面,大的在下面。由於我乙隻手托著盤子,只好用另乙隻手,一次抓住最上面的幾塊餅,把它們上下顛倒個個兒,反覆幾次之後,這摞烙餅就排好序了。我後來想,這實際上是個有趣的排序問題:假設有n塊大小不一的烙餅,那最少要翻幾次,才能達到最後大小有序的結果呢?」
你能否寫出乙個程式,對於n塊大小不一的烙餅,輸出最優化的翻餅過程呢?
n個烙餅經過翻轉後的所有狀態可組成一棵樹。尋找翻轉最少次數,相當於在樹中搜尋層次最低的某個節點。
2*n-3
(從網上可搜尋到對該上限值最新研究結果:上限值為18/11*n),當然,最好還是直接計算出採用這種方案的翻轉次數做為初始值。
減少遍歷次數:
1
減小「最少翻轉次數上限值」的初始值,採用前面提到的翻轉方案,取其翻轉次數為初始值。
對書中的例子,初始值可以取10。 2
避免出現已處理過的狀態一定會減少遍歷嗎?答案是否定的,
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。
如果翻轉某個位置的烙餅能使所有烙餅就位(即翻轉次數剛好為m),則翻轉其它位置的烙餅,能得到的最少翻轉次數必然大等m,因而這些位置都可以不搜尋。
如果在某個位置的翻轉後,「下限值」為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
661.3_pancake_2 允許
33 74
1.3_pancake_1
不允許195
3981.3_pancake_1 允許
116240
(這個例子比較特殊,**1.3_pancake_2.cpp(與1.3_pancake_1.cpp的最主要區別在於,增加了對翻轉優先順序的判斷,),在不允許翻轉回上次狀態且取min_swap的初始值為2*10-2=18時,呼叫搜尋函式29次,翻轉函式56次)。
搜尋順序對結果影響很大,如果將1.3_pancake_2.cpp第152行:
for (int pos=1, last_swap=cake_swap[step++]; pos
這一行改為:
for (int pos=size-1, last_swap=cake_swap[step++]; pos>=1; --pos){
僅僅調整了搜尋順序,呼叫搜尋函式次數由29次降到11次(對應的翻轉方法:9,6,9,6,9,6),求第1個烙餅數到第10個烙餅數,所用的總時間也由原來的38秒降到21秒。)
最終**:
補充:
在網上下了《程式設計之美》「第6刷」的源**,結果在編譯時存在以下問題:
1 assert
應該是assert
2 m_arrswap
未被定義,應該改為m_swaparray
3 init
函式兩個for迴圈,後乙個沒定義變數i,應該將i 改為int i
另外,每執行一次run函式,就會呼叫init函式,就會申請新的記憶體,但卻沒有釋放原來的記憶體,會造成記憶體洩漏。if(step + nestimate > m_nmaxswap) 這句還會造成後面對m_reversecakearrayswap陣列的越界訪問,使程式不能正常執行。
書上程式的低效主要是由於進行剪枝判斷時,沒有考慮好邊界條件,可進行如下修改:
1if(step + nestimate > m_nmaxswap)>
改為》=。
2判斷下界時,如果最大的烙餅不在最後乙個位置,則要多翻轉一次,因而在lowerbound函式return ret; 前插入一行:
if (pcakearray[ncakecnt-1] != ncakecnt-1) ret++; 。3n
個烙餅,翻轉最大的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。
程式設計之美 一摞烙餅排序
問題 星期五的晚上,一幫同事在希格瑪大廈附近的 硬碟酒吧 多喝了幾杯。程式設計師多喝了幾杯之後談什麼呢?自然是演算法問題。有個同事說 我以前在餐館打工,顧客經常點非常多的烙餅。店裡的餅大小不一,我習慣在到達顧客飯桌前,把一摞餅按照大小次序擺好 小的在上面,大的在下面。由於我乙隻手托著盤子,只好用另乙...
程式設計之美 一摞烙餅的排序
問題描述 星期五的晚上,一幫同事在希格瑪大廈附近的 硬碟酒吧 多喝了幾杯。程式設計師多喝了幾杯之後談什麼呢?自然是演算法問題。有個同事說 我以前在餐館打工,顧客經常點非常多的烙餅。店裡的餅大小不一,我習慣在到達顧客飯桌前,把一摞餅按照大小次序擺好 小的在上面,大的在下面。由於我乙隻手托著盤子,只好用...
程式設計之美 一摞烙餅的排序
先想了乙個最簡單的方法 首先對n個烙餅進行處理,找到最大的那個烙餅,將其之上的進行翻轉,然後對前n個烙餅堆進行翻轉 第二次操作對上面n 1個烙餅進行操作,還是找到n 1個中最大的,將其之上的進行翻轉,然後對前n 1個烙餅堆進行翻轉。這樣總共進行n 1次,每次翻轉兩次orz view code 1 i...