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

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

    原文作者:国境之南Fantasy
    原文地址: https://blog.csdn.net/fivedoumi/article/details/7627044
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞