一摞烙饼的排序
一,問題:?????
??????? 星期五的晚上,一幫同事在希格瑪大廈附近的“硬盤酒吧”多喝了幾杯。程序員多喝了幾杯之后談什么呢?自然是算法問題。有個同事說:“我以前在餐館打工,顧客經常點非常多的烙餅。店里的餅大小不一,我習慣在到達顧客飯桌前,把一摞餅按照大小次序擺好——小的在上面,大的在下面。由于我一只手托著盤子,只好用另一只手,一次抓住最上面的幾塊餅,把它們上下顛倒個個兒,反復幾次之后,這摞烙餅就排好序了。我后來想,這實際上是個有趣的排序問題:假設有n塊大小不一的烙餅,那最少要翻幾次,才能達到最后大小有序的結果呢?”
??????? 你能否寫出一個程序,對于n塊大小不一的烙餅,輸出最優化的翻餅過程呢?
二、分析
書中給的分析是這樣的:
1.首先找到了一種可解的方案,但不一定是最優解
這個方案是:如果我們最底層的餅已經排序完畢,那么我們只處理上面的n-1個餅就行,同樣的之后我們再把n-1個餅的排序轉化為n-2,n-3直到最后的兩個餅排好順序。
具體是什么意思呢?第一輪:我們先找到這n個餅中的最大的餅,然后從該處進行翻餅,把這個最大的餅翻到最上面,然后再把所有的餅進行一次翻滾,這樣? ??最大的餅就翻到了最下面。
? ?第二輪:我們上一輪已經把最大的餅翻到最下面了,接著我們用同樣的方法對待上面的n-1個餅,我們在這n-1個餅中找到最大的餅,然后? ??用同樣的方法把其翻滾到最底層的上面那一層(倒數第二層)。
? ?。。。。。。
? ?一直到最后一輪只剩下兩個。
上面的那個方案注意,每一輪都翻滾了兩次,一共需要n-1輪(最后一輪剩下兩個,最后小的不需要再進行翻滾),那么這個方案最多需要2(n-1)次翻滾。
? ? ? ? ?注意:書上也是說2(n-1)次,但是在它后來用的時候直接用的2n,不知道為什么,這樣增加了搜索的次數。
? ? ? 2.找到這個方案之后呢?這個方案只是一個可行的方案,但是肯定不是一個最優的方案, ?考慮這樣一種情況,假如這堆烙餅中有好幾個不同的部分相對有序,憑直覺來猜想,我們可以先把小的一些的烙餅進行翻轉,讓其有序,這樣會比每次都翻轉最大的烙餅要更快。那么既然剛才那個方案不是最優的,書中就給出了一個最原始的方法去找最優解的方法,即窮舉,它把所有可能的翻轉方案都給找出來,然后去找看哪一個翻轉的方案是最優的,但是書中對窮舉進行了優化使其搜索的次數大幅度減少。
怎么減少搜索次數呢?書中給出,其實我們在進行搜索時,如果發現這個方案的需要翻滾的次數比我們剛才找到的那個可行解還多時,那它肯定不再是最優解了,就沒必要再進行下去了。那么它是怎么判斷該方案需要翻滾的次數呢?它給出了一個判定當前烙餅狀態最少還需要多少次翻滾才能排好的方法,去計算相鄰的兩個烙餅的尺寸是否相鄰,把不相鄰的次數給求和,得出的就是該烙餅狀態下最少還需要多少次翻滾,然后再把這個次數+從初始狀態到達該狀態已經翻滾的次數,相加的結果和剛才我們已經找到的解決方案進行比較,如果大于這個方案,那就代表我們找到的這個方案就肯定不是最優的了,這個過程也就是所謂的剪枝的過程。
三、代碼
為了避免敲代碼浪費時間,代碼是從網上copy的,和原書上的代碼基本差不多,只是稍微的改動,稍后我會解釋進行了哪些改動。
// sort.cpp : 定義控制臺應用程序的入口點。 //#include "stdafx.h" #include <iostream> #include <cassert> #include <cstdio> class laobing { private:int *m_CakeArray; // 烙餅信息數組 int m_nCakeCnt; // 烙餅個數 int m_nMaxSwap; // 最多交換次數。根據前面的推斷,這里最多為 // m_nCakeCnt * 2 int *m_SwapArray; // 交換結果數組 int *m_ReverseCakeArray; // 當前翻轉烙餅信息數組 int *m_ReverseCakeArraySwap; // 當前翻轉烙餅交換結果數組 int m_nSearch; // 當前搜索次數信息 public:laobing(){m_nCakeCnt = 0;m_nMaxSwap = 0;}~laobing(){if (m_CakeArray != NULL)delete m_CakeArray;if (m_SwapArray != NULL)delete m_SwapArray;if (m_ReverseCakeArray != NULL)delete m_ReverseCakeArray;if (m_ReverseCakeArraySwap != NULL)delete m_ReverseCakeArraySwap;}// // 計算烙餅翻轉信息 // @param // pCakeArray 存儲烙餅索引數組 // nCakeCnt 烙餅個數 // void Run(int* pCakeArray, int nCakeCnt){Init(pCakeArray, nCakeCnt);m_nSearch = 0;Search(0);}void mOutput(int* CakeArray, int nCakeCnt, int *m_SwapArray, int m_nMaxSwap){int t;for (int i = 0; i < m_nMaxSwap; i++)//swap times {for (int j1 = 0, j2 = m_SwapArray[i]; j1 < j2; j1++, j2--) //reverse array {t = CakeArray[j1];CakeArray[j1] = CakeArray[j2];CakeArray[j2] = t;}for (int k = 0; k < nCakeCnt; ++k)printf("%d ", CakeArray[k]);printf("\n");}}void Output()// 輸出烙餅具體翻轉的次數 {for (int i = 0; i < m_nMaxSwap; i++){printf("%d ", m_SwapArray[i]);}printf("\n|Search Times| : %d\n", m_nSearch);printf("Total Swap times = %d\n", m_nMaxSwap);mOutput(m_CakeArray, m_nCakeCnt, m_SwapArray, m_nMaxSwap);//輸出交換過程 }private:// // 初始化數組信息 // @param // pCakeArray 存儲烙餅索引數組 // nCakeCnt 烙餅個數 // void Init(int* pCakeArray, int nCakeCnt){assert(pCakeArray != NULL);assert(nCakeCnt > 0);m_nCakeCnt = nCakeCnt;// 初始化烙餅數組 m_CakeArray = new int[m_nCakeCnt];assert(m_CakeArray != NULL);for (int i = 0; i < m_nCakeCnt; i++){m_CakeArray[i] = pCakeArray[i];}// 設置最多交換次數信息 m_nMaxSwap = UpBound(m_nCakeCnt);// 初始化交換結果數組 m_SwapArray = new int[m_nMaxSwap + 1];assert(m_SwapArray != NULL);// 初始化中間交換結果信息 m_ReverseCakeArray = new int[m_nCakeCnt];for (int i = 0; i < m_nCakeCnt; i++){m_ReverseCakeArray[i] = m_CakeArray[i];}m_ReverseCakeArraySwap = new int[m_nMaxSwap];}int UpBound(int nCakeCnt)// 尋找當前翻轉的上界 {return (nCakeCnt-1) * 2;}int LowerBound(int* pCakeArray, int nCakeCnt) // 尋找當前翻轉的下界 {int t, ret = 0;// 根據當前數組的排序信息情況來判斷最少需要交換多少次 for (int i = 1; i < nCakeCnt; i++){// 判斷位置相鄰的兩個烙餅,是否為尺寸排序上相鄰的 t = pCakeArray[i] - pCakeArray[i - 1];if ((t == 1) || (t == -1)){}else{ret++;}}if (pCakeArray[nCakeCnt - 1] != nCakeCnt - 1)ret++;return ret;}// 排序的主函數 void Search(int step){int i, nEstimate;m_nSearch++;// 估算這次搜索所需要的最小交換次數 nEstimate = LowerBound(m_ReverseCakeArray, m_nCakeCnt);if (step + nEstimate > m_nMaxSwap)return;// 如果已經排好序,即翻轉完成,輸出結果 if (IsSorted(m_ReverseCakeArray, m_nCakeCnt)){if (step < m_nMaxSwap){m_nMaxSwap = step;for (i = 0; i < m_nMaxSwap; i++)m_SwapArray[i] = m_ReverseCakeArraySwap[i];}return;}// 遞歸進行翻轉 for (i = 1; i < m_nCakeCnt; i++){Revert(0, i);//反轉 m_ReverseCakeArraySwap[step] = i; //第一步 反轉的哪一個 Search(step + 1);Revert(0, i);}}// // true : 已經排好序 // false : 未排序 // bool IsSorted(int* pCakeArray, int nCakeCnt){for (int i = 1; i < nCakeCnt; i++){if (pCakeArray[i - 1] > pCakeArray[i]){return false;}}return true;}// // 翻轉烙餅信息 // void Revert(int nBegin, int nEnd){assert(nEnd > nBegin);int i, j, t;long long number;// 翻轉烙餅信息 for (i = nBegin, j = nEnd; i < j; i++, j--){t = m_ReverseCakeArray[i];m_ReverseCakeArray[i] = m_ReverseCakeArray[j];m_ReverseCakeArray[j] = t;}} };int main() {laobing ll;//這里ll 不可以加括號 laobing *l = new laobing();int aa[10] = { 3, 2, 1, 6, 5, 4, 9, 8, 7, 0 };l->Run(aa, 10);l->Output();ll.Run(aa, 10);return 0; }
上面那段代碼是從http://blog.csdn.net/tianshuai1111/article/details/7659673這篇博客復制過來的,但是在這篇博客中作者說:
書上程序的低效主要是由于進行剪枝判斷時,沒有考慮好邊界條件,可進行如下修改:
1 )?if(step + nEstimate > m_nMaxSwap)? >改為 >=。
但是我仔細一想,這樣的做法會使得程序有一個bug,原因如下:我們考慮這樣一種情況,如果恰巧我們最初找到的那個解決方案是最優的話(我知道這種可能性很小,但是我的猜測是應該有的),我們如果在剪枝的時候直接把相等于這個方案的解給pass掉,最終可能會導致找不到最優解,原因很簡單,如果我們剛才把已經找到的最優解都給pass了,那么其余的肯定都會比這個最優解翻滾的次數多,這就導致程序一直運行到最后也沒有找到一個比最初我們的方案翻滾次數少的解決方案。
這個剪枝的方案之所以在該作者運行的時候又找到了最優解,又減少了搜索次數,我的猜測是恰巧它用的這個烙餅的例子最優解是小于2(n-1)的,但是如果不把這個條件加上又無疑增加了很多的搜索,我的想法是如果我們加上這個>=的條件之后,如果到最后沒有返回最優解,那么就代表我們最初的那個方案就是最優的,那么我們寫一個判斷,如果沒有返回,然后我們讓烙餅按照最初的解決方案進行翻滾,最后把這個翻滾的過程給返回。
注意以上只是我的猜想,因為這個:“恰巧我們最初找到的那個解決方案是最優的”我至今沒有想出來一個例子,而且我也不能證明是否不存在。
但是該博主提到的:
3 ) n個烙餅,翻轉最大的n-2烙餅最多需要2*(n-2)次,剩下的2個最多1次,因而上限值為2*n-3,因此,m_nMaxSwap初始值可以取2*n-3+1=2*n-2,這樣每步與m_nMaxSwap的判斷就可以取大等于號。
這個我很贊同,也很佩服博主的思考問題的嚴謹程度。
最后貼出運行結果:
總結
- 上一篇: 共生无线电系统
- 下一篇: 奥特曼系列ol进不去服务器,奥特曼系列O