一摞烙饼的排序

来源:互联网 发布:怎么能找到编程人才 编辑:程序博客网 时间:2024/04/27 18:17

一,问题:     

        星期五的晚上,一帮同事在希格玛大厦附近的“硬盘酒吧”多喝了几杯。程序员多喝了几杯之后谈什么呢?自然是算法问题。有个同事说:“我以前在餐馆打工,顾客经常点非常多的烙饼。店里的饼大小不一,我习惯在到达顾客饭桌前,把一摞饼按照大小次序摆好——小的在上面,大的在下面。由于我一只手托着盘子,只好用另一只手,一次抓住最上面的几块饼,把它们上下颠倒个个儿,反复几次之后,这摞烙饼就排好序了。我后来想,这实际上是个有趣的排序问题:假设有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的判断就可以取大等于号。

这个我很赞同,也很佩服博主的思考问题的严谨程度。

最后贴出运行结果:



0 0
原创粉丝点击