1. 问题描述

星期五的晚上,一帮同事在希格玛大厦附近的“硬盘酒吧”多喝了几杯。程序员多喝了几杯之后谈什么呢?自然是算法问题。有个同事说:

“我以前在餐馆打工,顾客经常点非常多的烙饼。店里的饼大小不一,我习惯在到达顾客饭桌前,把一摞饼按照大小次序摆好——小的在上面,大的在下面。由于我一只手托着盘子,只好用另一只手,一次抓住最上面的几块饼,把它们上下颠倒个个儿,反复几次之后,这摞烙饼就排好序了。

我后来想,这实际上是个有趣的排序问题:假设有n块大小不一的烙饼,那最少要翻几次,才能达到最后大小有序的结果呢?

你能否写出一个程序,对于n块大小不一的烙饼,输出最优化的翻饼过程呢?

2. 审题

首先明确,该问题的输入是什么?很显然,就是一摞大小不一且乱序的烙饼。那么,该问题的输入又该如何表示呢?通常我们会想到,用一个正整数序列来表示烙饼,每个正整数的大小表示烙饼的直径,如:[3,7,2,4,11,5]表示6个大小不同的乱序排列的烙饼。这是对问题的数学抽象,实质上我最开始也是这么做的。但是,如果我们再深入挖掘,会发现这样的抽象程度还不够,我们在排序的过程中,会关心最大的烙饼比次大的烙饼大多少吗?显然不会。因此,在表示输入的时候,正如书中所使用的方法那样,可以撇开烙饼具体大小,而是用一串连续的正整数来表示输入。也既是说,当烙饼排好序后,两相邻的烙饼不管大小相差多少,都用1来表示它们之间的不同,如[1,3,2,4,5,7,6]就表示了7个大小不同的烙饼。

那么,该问题的解又该如何表示呢?该问题问的是最少需要翻几次,以及输出最优的翻饼过程。翻几次当然很好表示了,但是如何表示翻饼过程呢?由于每次都只能翻顶部的若干个饼,而不能只翻中间的,因此,可以记录每次翻的饼的个数来表示翻饼过程。而输入又是用数组表示的,假设输入数组的第1个元素表示最顶上的饼,则也可以记录每次翻转对应的数组下标来表示翻饼过程。例如,假设最少翻5次,翻饼过程为[3,2,4,1,7],则表示第一次翻转了从数组下标0到数组下标3的4个饼,第二次翻转了从数组下标0到数组下标2的3个饼……第五次翻转了从数组下标0到数组下标7的8个饼。

问题的输入和输出都能够圆满地表示了,接下来就可以着手解决实际问题了。

3. 求解

该问题的难点在于“最优”,如果撇开这一限制,将会很容易想到书中的解法一。笔者也在未看答案时花了二十分钟写出了如下算法(本文中的代码都是C++描述,并通过VS2008测试):

/**

* Reverse a sequence of cakes.

* @param[in] start The start position to reverse.

* @param[in] end The next position of the last to reverse.

*/

void Reverse(int *start, int *end)

{

if (NULL == start || NULL == end || start >= end)

{

cerr << "Reverse(" << start << ", " << end << ") called. Parameters illegal!" << endl;

return;

}

--end;

while (start < end)

{

int tmp = *start;

*start = *end;

*end = tmp;

++start;

--end;

}

}

/**

* Using reverse method to sort a sequence of cakes.

* @param[in/out] arry The sequence of cakes.

* @param[in] n The number of cakes.

* @return int Times of reverses.

*/

int ReverseSort(int *arry, int n)

{

int reversesCounter = 0;

int minIdx;

for (int i = 0; i < n - 1; ++i)

{

minIdx = i;

for (int j = i + 1; j < n; ++j)

{

if (arry[j] < arry[minIdx])

{

minIdx = j;

}

}

if (minIdx != i)

{

Reverse(arry + minIdx, arry + n);

Reverse(arry + i, arry + n);

reversesCounter += 2;

}

}

return reversesCounter;

}

代码很简单,不再赘述。

但是这并不是原题想要的答案,如何才能找到最优的方案呢?自己“苦思冥想”了半个多小时也没能设计出一种有效的方法,最终还是忍不住看了答案,最后发现,实质上并没有有效的算法,可以一步到位得到最优解,书中用的是动态规划和分枝限界来穷举搜索。也就是说,穷举所有可能的翻转方法,并记录下最优的那一个,当然,明显不能成为最优解的方法就会被裁剪掉,以提高搜索效率。

那么,如何搜索所有的情况呢?首先,我们考察一下N个烙饼的一次翻转,它可以翻1个,2个……N-1个或者N个,当然仅翻转1个的情况对该题是无意义的。完成一次翻转后,烙饼进入下一个排列状态,然后我们以对该状态下的烙饼又可以进行前述的翻转操作,如此递归,因此,翻转的方式可能有无穷多种。幸好,我们已经找到了一个解决方案,以该方案的翻转次数作为上界,在搜索的过程中,如果翻转次数超过了该上界就直接被裁剪掉,不再无穷递归下去,它就是退出递归的一种情况;另一种退出递归的情况是在搜索的过程中,如果找到一个比当前更优的翻转方法,则记录下该最优方法,同时退出。

书中的Search函数完成了整个递归搜索过程,代码中的for循环计数i是从1开始的,因此,它避免了仅翻转一个烙饼的情况,在上面已经提到过这是无意义的。对于每一种翻转方法,烙饼序列都进入一个新的状态,然后递归调用Search函数,对处于新状态的烙饼继续搜索。

4. 纠错

书中为我们提供了很好的解题思路,但是源代码中的错误却比较多。一个显而易见的错误就是析构函数中动态数组的销毁,应该在每个delete后面加上[];另一个是为m_SwapArray指针分配的内存比为m_ReverseCakeArraySwap指针多1,这显然没有必要;还有一个很显然的错误,就是在Search函数中的for循环中进行递归时,调用了两次Reverse函数,末尾那次调用是不合逻辑的,应该删除。

另外存在一个缺陷是在判断退出递归时,使用的是step + nEstimate > m_nMaxSwap,实质上完全可以使用>=关系运算,这样会少做一次递归。其实更好的修改方案是直接修改UpperBound函数,使其返回(nCakeCnt - 1) * 2,这样每次会少做两次递归。

上面的错误都比较明显,但是在递归过程中存在一个潜在的致命错误。大家都知道,在进行递归时,我们必须将程序当前的状态压栈保存,以便回溯的时候继续按照原来的程序状态向下执行,为了分析代码中的错误,下面列出了书上递归循环的几句源码:

for (i = 1; i < m_nCakeCnt; i++)

{

Reverse(0, i);

m_ReverseCakeArraySwap[step] = i;

Search(step + 1);

       Reverse(0, i);

}

该递归过程首先调用Reverse(0,  i)翻转I + 1个烙饼,然后记录本次操作的下标,再递归调用Search(step + 1)。初看似乎没有任何问题,但是在调用Reverse进行翻转的时候,会对类中的私有成员——记录烙饼当前状态的数组m_ReverseCakeArray进行修改,本次修改也没有任何错误,然后递归调用Search(step + 1),问题就出在这里。当进入下一次Search后,同样会进入到该for循环中,也同样会调用Reverse(0, i)修改数组m_ReverseCakeArray,而无论递归到哪一层,数组m_ReverseCakeArray都是类中的私有成员,都是唯一的。那么问题就显而易见了,当递归完后,回溯到当前次调用,数组m_ReverseCakeArray中的内容是不是已经在向下递归时被糟蹋了?那么程序就无法回到递归前的状态继续向下执行了。

找到原因后,修改起来就比较容易了,只需要在每次递归前分配一块临时内在,保存好当前状态,再进入下一次递归即可。下面是修改并测试过的源代码,其框架与书上的源码相同,但变量名做了修改,测试环境是Win 7下的VS 2008。

#include "stdafx.h"

#include <iostream>

#include <cassert>

using namespace std;

class CPrefixSorting

{

public:

CPrefixSorting() : cakesCount(0), cakesArray(NULL), searchCount(0),

curCakesArrayReverse(NULL), cakesArrayReverse(NULL), reversesCount(0) {}

~CPrefixSorting();

void Run(int *, int);

private:

void Init(int *, int);

void Search(int *, int);

void PrintResult(void);

int UpperBound();

int LowerBound(int *);

bool IsSorted(int *);

void Reverse(int *, int, int);

private:

int cakesCount;          // The number of cakes.

int *cakesArray;     // The cakes array.

int searchCount;     // The counter of search steps.

int *curCakesArrayReverse;  // The current reverse information.

int *cakesArrayReverse;  // The result reverse information.

int reversesCount;          // The counter of reverses.

};

CPrefixSorting::~CPrefixSorting()

{

if (cakesArray)

delete [] cakesArray;

if (curCakesArrayReverse)

delete [] curCakesArrayReverse;

if (cakesArrayReverse)

delete [] cakesArrayReverse;

}

void CPrefixSorting::Init(int *pCakes, int cnt)

{

assert(NULL != pCakes && cnt > 0);

cakesCount = cnt;

cakesArray = new int[cakesCount];

assert(NULL != cakesArray);

for (int i = 0; i < cakesCount; ++i)

{

cakesArray[i] = pCakes[i];

}

reversesCount = UpperBound();

curCakesArrayReverse = new int[reversesCount];

assert(NULL != curCakesArrayReverse);

cakesArrayReverse = new int[reversesCount];

assert(NULL != cakesArrayReverse);

searchCount = 0;

}

int CPrefixSorting::UpperBound()

{

return 2 * cakesCount;

}

int CPrefixSorting::LowerBound(int *pCakesArray)

{

int count = 0;

for (int i = 0; i < cakesCount - 1; ++i)

{

int diff = pCakesArray[i] - pCakesArray[i + 1];

if (-1 != diff && 1 != diff)

{

++count;

}

}

return count;

}

bool CPrefixSorting::IsSorted(int *pCakesArray)

{

for (int i = 0; i < cakesCount - 1; ++i)

{

if (pCakesArray[i] > pCakesArray[i + 1])

{

return false;

}

}

return true;

}

void CPrefixSorting::Reverse(int *pCakesArray, int start, int end)

{

assert(start < end);

while (start < end)

{

int tmp = pCakesArray[start];

pCakesArray[start] = pCakesArray[end];

pCakesArray[end] = tmp;

++start;

--end;

}

}

void CPrefixSorting::Search(int *pCakesArray, int step)

{

++searchCount;

if (LowerBound(pCakesArray) + step >= reversesCount)

{

return;

}

if (IsSorted(pCakesArray) && step < reversesCount)

{

reversesCount = step;

for (int i = 0; i < reversesCount; ++i)

{

cakesArrayReverse[i] = curCakesArrayReverse[i];

cout << curCakesArrayReverse[i] << "  ";

}

cout << endl;

return;

}

for (int i = 1; i < cakesCount; ++i)

{

int *cakesArrayTmp = new int[cakesCount];

assert(NULL != cakesArrayTmp);

for (int j = 0; j < cakesCount; ++j)

{

cakesArrayTmp[j] = pCakesArray[j];

}

Reverse(cakesArrayTmp, 0, i);

curCakesArrayReverse[step] = i;

Search(cakesArrayTmp, step + 1);

if (cakesArrayTmp)

{

delete [] cakesArrayTmp;

}

}

}

void CPrefixSorting::PrintResult()

{

cout << "The search times is " << searchCount << endl;

cout << "The least reverse times is " << reversesCount << endl;

cout << "The reverse process is:" << endl;

for (int i = 0; i < reversesCount; ++i)

{

cout << cakesArrayReverse[i] << "  ";

}

cout << endl;

}

void CPrefixSorting::Run(int *pCakes, int cnt)

{

Init(pCakes, cnt);

Search(cakesArray, 0);

PrintResult();

}

int _tmain(int argc, _TCHAR* argv[])

{

int arry[] = {6, 8, 9, 5, 1, 7, 11, 10, 2, 3, 4, 12};

CPrefixSorting prefixSorting;

prefixSorting.Run(arry, sizeof(arry) / sizeof(arry[0]));

return 0;

}

FROM: http://www.cnblogs.com/kevin-tech/archive/2012/05/20/2510114.html

《编程之美》一摞烙饼问题详解与纠错相关推荐

  1. 编程之美 一摞烙饼的排序问题

    一摞烙饼问题其实是一个很有意思的问题,它的描述是让一摞随机顺序的烙饼通过单手翻转的方式进行排序,以达到这摞烙饼由小到大顺序放置在盘子上的目 的,其特点是每次翻转都会导致第一个烙饼到所要反转的那个烙饼之 ...

  2. 编程之美 一摞烙饼问题

    问题: 星期五的晚上,一帮同事在希格玛大厦附近的"硬盘酒吧"多喝了几杯.程序员多喝了几杯之后谈什么呢?自然是算法问题.有个同事说:"我以前在餐馆打工,顾客经常点非常多的烙 ...

  3. 编程之美之一摞烙饼的排序1

    拿到这个问题, 第一反应是利用分治的算法思想, 每次把当前的最大的一块烙饼放到指定位置 ,这样的思想非常简单,实现也非常容易.但是这只是提供了,问题的一个可行解,看完书中的内容之后发现,题目中要求的是 ...

  4. 编程之美-一摞烙饼的排序方法整理

    [问题描述] [方法]

  5. 网络编程中的SO_REUSEADDR和SO_REUSEPORT参数详解

    1.SO_REUSEADDR: 在BSD中,SO_REUSEADDR选项有两个用户: 如果有socket绑定了0.0.0.0:port:设置该参数后,其他socket可以绑定本机ip:port.(该功 ...

  6. 2021年美赛A题思路详解

    2021年数模美赛A题思路详解 题目分析 思路详解 由于和队友思路不一致,导致最后我的思路只算了前两问,而后几问用了我认为离题的PCA(主成分分析)的方法,我的建模思路没有得到完全实现,总体情况很不满 ...

  7. 计算机编程种常见的几种编码详解

    计算机编程种常见的几种编码详解 其实计算机编程离不开编码 但是大多数都不能真正全面了解各种编码 今天就来好好和几位编码熟悉熟悉 一.字符.字符集和字符编码方式 字符:字符是抽象的最小文本单位.它没有固 ...

  8. 编程之美 - 读书笔记 - 烙饼问题与搜索树

    前 面已经写了一些关于烙饼问题的简单分析,但因为那天太累有些意犹未尽,今天再充实一些内容那这个问题研究透.我想,通过这篇文章,我们就可以把这一类问题 搞懂.再遇到优化问题,如果我们想不到别的办法,就可 ...

  9. 高并发编程_高并发编程系列:7大并发容器详解(附面试题和企业编程指南)...

    不知道从什么时候起,在Java编程中,经常听到Java集合类,同步容器.并发容器,高并发编程成为当下程序员需要去了解掌握的技术之一,那么他们有哪些具体分类,以及各自之间的区别和优劣呢? 只有把这些梳理 ...

最新文章

  1. SVN中怎样忽略当前文件不提交
  2. 微信小程序项目,实现图书搜索高阶组件:
  3. Oracle如何建立多库,基于Oracle多库查询方法(分享)
  4. 判断数组、集合list、string、int、double等是否为空,判断是否为值类型
  5. .Net身份验证概述
  6. 【图像压缩】基于matlab BP神经网络图像压缩【含Matlab源码 741期】
  7. steamvr自定义按键_SteamVR SDK更新:带来运动平滑、自定义控制器键等多项功能
  8. 群晖python套件包_想在群晖上运行python该怎么弄?
  9. java山地车 故障,山地车故障的修理方法
  10. 投屏时 仅电脑屏幕、复制、扩展、仅第二屏幕的区别
  11. 小白算法练习 lanqiao SPFA+vector 模板
  12. dss数字签名技术java_DSS数字签名标准
  13. 【2020 ACM Fellow 华人学者】 任奎 浙江大学
  14. onRef在react中的使用
  15. 开唐贡献榜(凌烟阁二十四功臣)
  16. 致曾经那个优秀的女孩儿
  17. 上海数据分析师培训哪家好,大数据分析与数据分析师有什么区别?
  18. Android 12.0 通过驱动实现禁用usb鼠标和usb键盘功能
  19. 思考:高并发下的抽奖优化
  20. 一套完整的大型三甲医院信息管理(HIS)系统源码【免费分享源代码 】

热门文章

  1. Quartz定时任务使用小记(11月22日)
  2. java 反射 对象的方法_java通过反射创建对象并调用方法
  3. 信用卡号码规则和校验(java版)
  4. 实验一段有趣的js代码。
  5. 企业财务报表查询方式
  6. 大数据精准投放平台_基于大数据的广告精准投放方法与流程
  7. Codeforces Round #702 C. Sum of Cubes
  8. 使用深度学习检测DGA(demo、初探)
  9. 搭建speedtest server
  10. 2016年保密技术交流大会的璀璨之星—亿赛通