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

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

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

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

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

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

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

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

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

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

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


编程之美003——一摞烙饼的排序_第1张图片
穷举所有情况

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

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;

}

你可能感兴趣的:(编程之美003——一摞烙饼的排序)