问题:
星期五的晚上,一帮同事在希格玛大厦附近的“硬盘酒吧”多喝了几杯。程序员多喝了几杯之后谈什么呢?自然是算法问题。有个同事说:“我以前在餐馆打工,顾客经常点非常多的烙饼。店里的饼大小不一,我习惯在到达顾客饭桌前,把一摞饼按照大小次序摆好——小的在上面,大的在下面。由于我一只手托着盘子,只好用另一只手,一次抓住最上面的几块饼,把它们上下颠倒个个儿,反复几次之后,这摞烙饼就排好序了。我后来想,这实际上是个有趣的排序问题:假设有n块大小不一的烙饼,那最少要翻几次,才能达到最后大小有序的结果呢?”
你能否写出一个程序,对于n块大小不一的烙饼,输出最优化的翻饼过程呢?
基本上,这种问题只有通过生成树搜索、动态规划、贪心算法来解决。显然,不容易找出一种贪心算法,使得通过让子过程最优使得最终输出最优。动态规划的问题是规划的子过程太多了,遍历子过程的比求解还复杂。那么只能用生成树搜索的办法。书上给出的DFS+剪枝的方法。我觉得WFS应该更快,但是更耗内存。
1、先把书上的解输入eclipse
用书上的例子测试 {3,2,1,6,5,4,9,8,7,0}
连作者都觉得应该好好优化了。
按照作者的思路,让上限(最少翻转次数上限值)或者下限(估算翻转的需要最少次数)越接近最终解能让剪枝更快接近最终解。这是显而易见的。
因此我们可以采取的策略有:
1、if(step + nEstimate > m_nMaxSwap) > 改为 >= 这是因为如果在这个解法中,估算的翻转次数已经大于等于了已知的最少翻转次数,哪怕是等于,也没有必要做下去了。
2、书上的初始上限,用的是2n-1这个固定值。而书上也介绍了一种最简单的翻转烙饼归位的算法,那就是每次将未归位的最大的烙饼先翻到最上面,再翻到指定的位置。我们用这种方法很快【算法复杂度O(n)】就可以得出一个已知的翻转方法,这种方法的翻转次数肯定小于等于2n-1。那么就用这种方法来确定初始上限好啦,一般情况下,上限可以被减小很多。
3、在2中,我们减小了上限,接着可以考虑有没有可能在搜索的过程中让nEstimate尽可能的变大。考虑这样一种情况,所有的烙饼都按照大小排位的,但是不是由小到大【0123456789】,是反过来由大到小【9876543210】,在这种情况下也是需要翻转一次才行的。因而在LowerBound函数return ret; 前插入一行:
if (pCakeArray[nCakeCnt-1] != nCakeCnt-1) ret++;
这里我们默认了烙饼是0123456789这样的数顺序排列的,如果不是,可以在之前检查替换。
然后我将原算法改进。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
//============================================================================// Name : Beauty of Code 1.3.CPP// Author : Original Author// Version : 1.0// Copyright : // Description : Pancake Sorting//============================================================================#includeusing namespace std;class CPrefixSorting{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: //构造函数 CPrefixSorting() { m_nCakeCnt=0; m_nMaxSwap=0; } ~CPrefixSorting() { 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; } //寻找当前翻转的上界 int UpperBound(int nCakeCnt) { return nCakeCnt*2-2; } //根据当前数组信息,判断翻转的最小下界, int LowerBound(int * pCakeArray,int nCakeCnt) { int t,ret=0; for(int i=1;i pCakeArray[i]) return false; } return true; } void Reverse(int nBegin, int nEnd) { assert(nEnd>nBegin); int i,j,t; //nBegin到nEND的数颠倒 for(i=nBegin,j=nEnd;i =m_nMaxSwap) return; if(IsSorted(m_ReverseCakeArray,m_nCakeCnt)) { if(step 1&& !IsSorted(cake,nCakeCnt) ) { for (int i=0;i cake[biggest]) biggest=i; } if(biggest+1==n_Swap) { //cout<<".1."< 0); m_nCakeCnt =nCakeCnt; //初始化烙饼数组 m_CakeArray = new int[m_nCakeCnt]; assert(m_CakeArray != NULL); for(int i=0; i
goThe First Solve: 104 8 6 8 4 9Search Times: 1045Total Swap times: 6
嗯,算法效率提高了挺多的。
4、(1)如果排序排到 3,2,1,6,5,4,0,7,8,9的时候,显然最后面三个饼已经就绪了,就没有必要搜索了。那么可以改进search()函数,将已经就绪m的烙饼剔除,排序前面n-m个烙饼。
(2)还有,如果这次翻转的是第k个烙饼,那么下次就没有必要再翻转k个烙饼,因为这样只会回到上次的情况,完全没有改善。
(3)如果在step=i次翻转后,递归的某个子函数找到了解,那么就没有必要再在这个step的状态里递归搜索其他的解了。因为即使找到了,也不可能比这个解更优。
嗯,我们在问题本身的优化已经很很多了。现在对DFS算法进行优化。
搜索算法一般可以用 爬山法、best-first法、A*算法来优化。简单的说就是向前预测一步或者更多。
5、如果我们可以迅速找到一个比较优的解法,那么搜索的次数就可以大大被减少。这显然有点难。
我们可以退而求其次,通过预测下一次翻转可能的下限来优化搜索的路径,迅速找到最优解。
在原程序中,我们可以通过 step=i 状态 的nEstimate估算,来确定 step=i 状态的解的下限。
对于step=i 状态向下进行step = i+1 状态的递归搜索时,我们原来的做法是从第2个烙饼 到n-1个依次去试,显然这不是一个最聪明的方法。
我们考虑估算step =i 时nEstimate =LowBoundry({3,2,1,6,5,4,9,8,7,0})
而step = i+1 时 nEstimate=LowBoundry({3,2,1,6,5,4,9,8,7,0},翻转第k个) 非要通过翻转烙饼以后才能估算出来么?显然不用。
那么我们就可以计算出 k=1到n-2时的下一步的nEstimate,按照从小到大的顺序排序,估算小的先搜索,如果step+nEstimate大于目前的最优解则不用搜索了。
这样就又一次大幅度优化了算法。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
#includeusing namespace std;class CPrefixSorting{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;//当前搜索次数信息 int reversecount; int lastswap; //int* EstimateArray;//判断搜索情况public: //构造函数 CPrefixSorting() { m_nCakeCnt=0; m_nMaxSwap=0; reversecount=0; } ~CPrefixSorting() { 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; //if(EstimateArray !=NULL) // delete EstimateArray; } //寻找当前翻转的上界,由最简单的解法取代 int UpperBound(int nCakeCnt) { return nCakeCnt*2-2; } //根据当前数组信息,判断翻转的最小下界, int LowerBound(int * pCakeArray,int nCakeCnt) { int t,ret=0; for(int i=1;i pCakeArray[i]) return false; } return true; } void Reverse(int nBegin, int nEnd) { reversecount++; assert(nEnd>nBegin); int i,j,t; //nBegin到nEND的数颠倒 for(i=nBegin,j=nEnd;i =m_nMaxSwap) return; if(IsSorted(m_ReverseCakeArray,m_nCakeCnt)) { if(step =m_nMaxSwap) return false; CakeCnt=returnorder(m_ReverseCakeArray,CakeCnt); if(CakeCnt==1)//== { if(step =EstimateArrayValue[k]) { temp=EstimateArrayValue[k]; EstimateArrayValue[k]=EstimateArrayValue[i]; EstimateArrayValue[i]=temp; temp=EstimateArrayID[k]; EstimateArrayID[k]=EstimateArrayID[i]; EstimateArrayID[i]=temp; } } } if(step!=-1)//debug { cout< <<"~~ E:"< < = m_nMaxSwap) break; if(lastswap==EstimateArrayID[i]) continue; lastswap=EstimateArrayID[i]; Reverse(0, EstimateArrayID[i]); m_ReverseCakeArraySwap[step]=EstimateArrayID[i]; bool temp=Search_V1(step+1,CakeCnt); Reverse(0, EstimateArrayID[i]); if (temp==true) break; } delete EstimateArrayValue; delete EstimateArrayID; } int returnorder(const int CakeArray[],int count)//去除末尾已就位的烙饼,修正烙饼数组大小。 { while(count>1 && count-1 == CakeArray[count-1]) count--; return count; } int returnEstimate(const int CakeArray[],int i,int count)//估算在i位置翻转的LowerBound { int t,ret=0; for(int k=1;k<=i;k++)//计算翻转前段的估算 { t=CakeArray[k]-CakeArray[k-1]; if((t==1)||(t==-1))//如果左相邻,则不增加 {} else {ret++;} } for(int k=i+2;k 1) { for (int i=0;i cake[biggest]) biggest=i; } if(biggest+1==n_Swap) { //cout<<".1."< 0); m_nCakeCnt =nCakeCnt; //初始化烙饼数组 m_CakeArray = new int[m_nCakeCnt]; assert(m_CakeArray != NULL); for(int i=0; i
goThe First Solve: 105 8 5 8 5 9Search Times: 144Swap times: 6reversecount 286
当我觉得我算法已经很优化的时候,我看了http://www.cppblog.com/flyinghearts/archive/2010/06/23/118595.html的文章,查找次数11次。
网上大神还是多。
对于pancake sorting问题为什么是微软的面试题,我觉得很大程度上还是因为比尔盖茨伪辍学者真学神在大学时的某次课程设计,竟然被他们老师改吧改吧发了SCI。http://en.wikipedia.org/wiki/Pancake_sorting#References
pancake sorting的一些变种问题,比如一面黄一面白的烙饼排序,还有查找烙饼数的问题,有时间我来看看。