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