编程之美003——一摞烙饼的排序

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

我们首先将问题简化一下,简化问题可以然我们集中精力在问题的本质上。我们假设就是10个烙饼,大小用1-10来表示,用一个数组存储,数组的下标表示相对位置关系。翻转烙饼就是将到数组中的0到i的元素的顺序调转,通过翻转操作,最后要数组是增序的。

先不考虑最优解的情况,先尝试着通过翻转,让数组排好序。很容易想到的是汉罗塔中的思路,先将最底下面的放好,再考虑上面的。通过递归来解决。将最大的烙饼放在最下面,可能的情况是:

1)最大的烙饼本身就在最下面,我们不用任何操作;

2)最大的烙饼在最上面,我们将所有烙饼一起翻转一下就可以了;

3)最大的烙饼在中间,那么我们先将烙饼所在位置上面包括该烙饼本身一起翻转一次,最大的烙饼就在最上面了,再如2)中一样操作。

将最大烙饼放好位置后,再考虑倒数第二个。重复上面的操作,最终可以完成对烙饼的排序。

这个方案中,对每个位置的调整,翻转次数为0,1或2。所以最多2*n次,我们就可以完成排序,再考虑上面的第一个位置,实际上,我们完成了第二个位置的排序时,最小的烙饼已经排好序了(和蜗牛爬电线杆类似)。实现就是还可以考虑第三个位置排好序了,最上面的两个烙饼最多需要一次翻转就可以了。所以最多次数为max(2n-3,0),考虑到n=1的情况,我们用个max。

好了,我们知道了排序的一个解决方案了,但是我们不知道是不是最优解。要知道最有解,最直接的办法是找到所有解,然后比较翻转次数。

如何找到所有解呢,很暴力的想法就是穷举。将所有翻转方案列举一下,找到能够排序的。


穷举所有情况

上图中演示了,怎样遍历所有翻转情况。但似乎好像有点问题。很明显上面的过程是个递归过程,但是没有看到递归的出口。我们分析一下出口情况:

1)我们知道了至少有一种方案的翻转次数是小于max(2n-3,0)的,那么最优方案肯定也小于这个值,所以第一个出口是step > max(2n-3,0)的时候,一点不是最优的,不用继续了。拓展一下,最佳方案的翻转次数不会大于目前已经找到的最佳方案的翻转次数,所以出口条件为,step > maxStep,maxStep为目前找到的最佳方案翻转次数。

2)目前,已经排好序了,找到了方案,不需要继续了。退出前更新maxStep。

用上述两个条件,我们已经可以给出初步的解决方案了。代码如下:

class CakeSwap

{

private:

int swapCount;

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

int *swapArray;

int *tempSwapArray;

public:

void init(){

swapCount = 20;//最多翻转次数

swapArray = new int[20];

tempSwapArray = new int[20];

search(0);

printResult();

}

void search(int step)

{

if(step > swapCount)

{

return;

}

if(isSorted(10))

{

if (swapCount > step)

{

swapCount = step;

for (int index = 0 ; index < step ; index ++)

{

swapArray[index] = tempSwapArray[index];

}

}

return;

}

for(int index = 1; index < 10 ; index++)

{

swap(index, cakeArray, 10);

tempSwapArray[step] = index;

search(step + 1);

swap(index, cakeArray, 10);

}

}

bool isSorted(int size)

{

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

{

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

{

return false;

}

}

return true;

}

void swap(int i, int *array, int size)

{

if (i < size)

{

for (int beginIndex = 0 , endIndex = i;  beginIndex < endIndex ; beginIndex++, endIndex--)

{

array[beginIndex] = array[beginIndex] + array[endIndex] ;

array[endIndex] = array[beginIndex] - array[endIndex] ;

array[beginIndex] = array[beginIndex] - array[endIndex] ;

}

}

else

{

cout << "out of array size";

}

}

void printResult()

{

for (int index = 0 ; index < swapCount; index++)

{

cout << swapArray[index] << "\\\\\\\\n";

}

cout << "end...\\\\\\\\n";

}

};



为减少不必要的查找,我们希望更早排除非最佳方案。有两个方面可以考虑:

1)根据目前的状态,预测至少需要翻转多少次才能排好序,如果预测值+目前已经翻转次数 > 目前最佳方案,则不用继续了。

2)考虑状态,如果当前状态之前出现过,则不用继续了。

对第一个问题,根据当前状态中的相连状态,可以预估一个最少翻转次数lowerBound。因为一次翻转最多使一个烙饼与大小和它相邻的烙饼排到一起,所以lowerBound的计算如下(实测过程中,对排列混乱的情况lowerBound的引入大大的提高效率):

lowerBound = 0;

for( index = 0; index < size - 1; index ++)

{

if(swapArray[index] - swapArray[index+1] !=1 && swapArray[index] - swapArray[index+1] != -1)

{

lowerBound++;

}

}

对第二问题,我们至少可以做到避免翻转会上一个状态。我们传入上次翻转位置,然后在本次中同一位置不翻转,直接退出。代码如下:

void search(int step,int lastIndex)

for(int index = 1; index < 10 ; index++)

{

if(index == lastIndex)

{

continue;

}

swap(index, cakeArray, 10);

tempSwapArray[step] = index;

search(step + 1,index);

swap(index, cakeArray, 10);

}

记录状态,我们容易想到的是将所有出现的状态缓存起来,然后搜素缓存中的状态和当前状态比较,如果状态中已经存在了,而且出现这个状态所用的翻转次数少于当前次数,直接退出。实际操作起来问题却很多,比如:

1) 怎样存储状态?

首先,存储数组是不合理的,不仅占用空间大,而且赋值操作多。需要转化一下。有几个转化思路

(1)将数组转化为字符串存储。

(2)将数组序列映射为整数。

2) 怎样搜索状态?

遍历搜索将大大的降低效率,因为搜索过程中比较太多。优化一下,可以用二分查找,这需要在缓存的时候,对状态排序。实际操作做还是进行可大量的比较,效率实际上降低了。转化一下,将数组序列映射的整数做为缓存数组的下标,在缓存中存bool值表示该位置对应的状态是否出现过。可是会消耗量的存储空间。

一番折腾,发现不是效率变低,就是存储空间太大(当然探索的过程中还是收获不少的)。所以缓存所有状态,好像不合理,那么我们退而求其次,只缓存当前翻转路径中的所有状态,也就是缓存从初始状态将到当前状态过程中的所以状态,然后搜索当前状态是否在缓存中出现过,这个是合理的,我们虽然在上面避免了马上回到上一个状态的过程,但是回到上两个、三个状态情况。

这里很适合用数据结构中的栈。进入下一个操作的时候,状态入栈,退出的时候出栈。

代码如下:

void search(int step,int lastIndex)

{

if(step + lowerBound() >  swapCount || step > swapCount)

{

return;

}

if(findState())

{

return;

}

if(isSorted(10))

{

if (swapCount > step)

{

swapCount = step;

for (int index = 0 ; index < step ; index ++)

{

swapArray[index] = tempSwapArray[index];

}

}

return;

}

stateArray.push_back(toKey());

for(int index = 1; index < 10 ; index++)

{

if(index == lastIndex)

{

continue;

}

swap(index, cakeArray, 10);

tempSwapArray[step] = index;

search(step + 1,index);

swap(index, cakeArray, 10);

}

if(stateArray.size() > 0)

stateArray.pop_back();

}

搜索缓存

bool findState(){

for(int i = 0 ;i < stateArray.size(); i++)

{

if(toKey() == stateArray[i] )

{

return true;

}

}

return false;

}

状态数组转化为整数

int toKey(){

int result = cakeArray[0];

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

{

result *= 10;

result += cakeArray[i];

}

return result;

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容