问题:

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

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

n个烙饼经过翻转后的所有状态可组成一棵树。寻找翻转最少次数,相当于在树中搜索层次最低的某个节点。

由于每层的节点数呈几何数量级增长,在n较大时,使用广度优先遍历树,可能没有足够的内存来保存中间结果(考虑到每层的两个节点,可以通过旋转,移位等操作互相转换,也许每层的状态可以用一个函数来生成,这时可以采用广度优先方法),因而采用深度优先。但这棵树是无限深的,必须限定搜索的深度(即最少翻转次数的上限值),当深度达到该值时不再继续往下搜索。最少翻转次数,必然小等于任何一种翻转方案所需的翻转次数,因而只要构造出一种方案,取其翻转次数即可做为其初始值。最简单的翻转方案就是:对最大的未就位的烙饼,将其翻转,再找到最终结果中其所在的位置,翻转一次使其就位。因此,对编号在n-1和2之间的烙饼,最多翻转了2*(n-2)次,剩下0和1号烙饼最多翻转1次,因而最少翻转次数的上限值是:2*(n-2)+1=2*n-3(从网上可搜索到对该上限值最新研究结果:上限值为18/11*n),当然,最好还是直接计算出采用这种方案的翻转次数做为初始值。

减少遍历次数:

1 减小“最少翻转次数上限值”的初始值,采用前面提到的翻转方案,取其翻转次数为初始值。对书中的例子{3,2,1,6,5,4,9,8,7,0},初始值可以取10。

2 避免出现已处理过的状态一定会减少遍历吗?答案是否定的,深度优先遍历,必须遍历完一个子树,才能遍历下一个子树,如果一个解在某层比较靠后位置,若不允许处理已出现过的状态时,可能要经过很多次搜索,才能找到这个解,但允许处理已出现过的状态时,可能会很快找到这个解,并减小“最少翻转次数的上限值”,使更多的分支能被剪掉,反而能减少遍历的节点数。比如说,两个子树A、B,搜索子树A,100次后可得到一个对应翻转次数为20的解,搜索子树B,20次后可得到翻转次数为10的解,不允许处理已出现过的状态,就会花100次遍历完子树A后,才开始遍历B,但允许翻转回上一次状态,搜索会在A、B间交叉进行,就可能只要70次找到子树B的那个解(翻转次数为10+2=12),此时,翻转次数上限值比较小,可忽略更多不必要的搜索。以书中的{3,2,1,6,5,4,9,8,7,0}为例,按程序(1.3_pancake_1.cpp),不允许翻转回上次状态时需搜索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。)

结果:

对书上的例子{3,2,1,6,5,4,9,8,7,0}:

翻转回上次状态

搜索函数被调用次数

翻转函数被调用次数

1.3_pancake_2

不允许

29

66

1.3_pancake_2

允许

33

74

1.3_pancake_1

不允许

195

398

1.3_pancake_1

允许

116

240

(这个例子比较特殊,代码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<size; ++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数组的越界访问,使程序不能正常运行。

书上程序的低效主要是由于进行剪枝判断时,没有考虑好边界条件,可进行如下修改:

1  if(step + nEstimate > m_nMaxSwap)  > 改为 >=。

2  判断下界时,如果最大的烙饼不在最后一个位置,则要多翻转一次,因而在LowerBound函数return ret; 前插入一行:

if (pCakeArray[nCakeCnt-1] != nCakeCnt-1) ret++; 。

3  n个烙饼,翻转最大的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。

from:http://www.cnblogs.com/flyinghearts/archive/2010/09/10/1823699.html

编程之美读书笔记1.3 —— 一摞烙饼的排序相关推荐

  1. 编程之美--游戏之乐--1.3一摞烙饼的排序

    话不多说 直接上代码 #coding=utf-8 #author="HL"class Node():def __init__(self,cakes,level,steps):sel ...

  2. 编程之美 - 读书笔记 - 卖书折扣问题的贪心解法

    <编程之美>读书笔记(四):卖书折扣问题的贪心解法 每 次看完<编程之美>中的问题,想要亲自演算一下或深入思考的时候,都觉得时间过得很快,动辄一两个小时,如果再把代码敲一遍的话 ...

  3. 【编程之美/读书笔记】Chapter 1 游戏之乐

    这里就不写每个问题的题目了,只是记录一下自己的总结和心得. 1.1 让CPU占用率听你指挥 这个题目我刚接触的想法是和多核多线程要扯上关系的,因为自己写个死循环只能跑到CPU 35%左右的占用率,但是 ...

  4. Java并发编程之美读书笔记-并发编程基础2

    2019独角兽企业重金招聘Python工程师标准>>> 1.线程的通知与等待 Java中的Object类是所有类的父亲,鉴于继承机制,Java把所有类都需要的方法放到了Object类 ...

  5. 编程之美读书笔记2.1—求二进制数中1的个数

    解法一: 可以举一个8位二进制的例子.对于二进制操纵,我们除以一个2,原来数字就会减少一个0(向右移一位).如果除的过程中有余,那么久表示当前位置有一个1. 以10100010为例: 第一次除以2时, ...

  6. 编程之美--读书笔记--返回一个数组中所有元素被第一个元素除的结果

    笔试题目1:写一个函数,返回一个数组中所有元素被第一个元素除的结果 很多人会想到如下: void DivAarry(int *pArray,int size) { for(int i=size-1;i ...

  7. 编程之美读书笔记之-高效率的安排见面会

    问题一: n个同学,分别对m个招聘见面会感兴趣.为了满足所有学生的要求,hr希望让每个同学都能参加自己所有感兴趣的见面会.然后每个见面会的时间为t.问如何安排见面会能够使得所有见面会总的时间最短. 建 ...

  8. 中国象棋将帅问题java_编程之美读书笔记1.2——中国象棋将帅问题

    http://blog.csdn.net/pipisorry/article/details/36380669 问题:下过中国象棋的朋友都知道,双方的"将"和"帅&quo ...

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

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

  10. 编程之美读书笔记_1.1_让CPU占用率曲线听你指挥

    题目:写一个程序,让用户来决定Windows任务管理器(Task Manager)的CPU占用率.程序越精简越好,计算机语言不限.例如,可以实现下面三种情况:   1.    CPU的占用率固定在50 ...

最新文章

  1. 【星云测试】精准测试的软件产品质量效率变化分析
  2. linux 创建临时文件目录 mktemp 命令 简介
  3. Java基础-方法(2)和数组
  4. 判断字符串NSString是否是整形或者浮点型
  5. FLOPs衡量模型复杂度
  6. php错误日志和php-frm错误日志,错误amp;日志
  7. ubuntu下mysql数据库存储路径修改
  8. 02-C#(基础)基本的定义和说明
  9. 神经网络编程的34个案例,神经网络编程是什么
  10. win7计算机系统减肥,怎样给win7瘦身 win7瘦身攻略
  11. spring-retry使用以及源码
  12. dell电脑更新win11后黑屏但有鼠标(已解决)
  13. Elasticsearch 为什么会产生文档版本冲突?如何避免?
  14. 华视身份证阅读器SDK使用手册
  15. 新加坡国立大学Robby T. Tan教授招收计算机视觉方向博士生
  16. abp集成abp.Dapper
  17. 手机接收机的功能电路(3)---振荡电路、锁相环
  18. three.js小结
  19. Qt模型视图框架:QListView
  20. Android 项目架构设计深入浅出

热门文章

  1. 【C++实现python字符串函数库】strip、lstrip、rstrip方法
  2. linux -- 个人笔记
  3. 游戏设计亦或课件设计
  4. C编程语言中16位整型数据的取值范围介绍
  5. C++笔记之CopyFile和MoveFile的使用
  6. 记一次线上Zabbix对Redis监控实录
  7. 关于64位win7环境下VS连接oracle数据库的问题
  8. php中echo和print的区别
  9. 获得进程可执行文件的路径: GetModuleFileNameEx, GetProcessImageFileName, QueryFullProcessImageName...
  10. OLEDB, ODEB, ADO.NET Abbreviation