Gates,W. and Papadimitriou,C. "Bounds for Sorting by Prefix Reversal." Discrete Mathematics.27,47-57,1979.
据说这是Bill Gates发表的唯一学术论文。
这个排序问题非常有意思,不同于Hanoi塔问题。首先我们要弄清楚骚操作在于:单手每次抓n块饼,全部颠倒。
例如
-----------1------------
------2------
----3----
翻转后:
----3----
------2------
-----------1------------
基本的排序方法都不太好用。先说一个最简单的解法,如果最底层的饼已经排序,那我们只需要处理n-1个烙饼。这样,我们再简化为n-2,n-3,递归直到最上面的两个饼排好序。这种排序的算法思路大概是这样:
首先,对于每一堆饼,我们找到它里面最大的一张饼,把这张最大的饼与上面的n个饼全部翻转,这样最大的饼就在最上面了,之后我们把整堆再做一次翻转,这样最大的饼也就到了最底部。如图所示(笔者字不好,多多包涵哈哈)
之后,我们就可以轻松的通过递归将整堆饼排序。
这么简单就可以把问题解决吗?用脚丫子想都不可能。
我们注意到一个问题,上面的方法中,首先经过两次翻转可以把最大的烙饼翻转到最下面。因此,最多需要把上面n-1张烙饼翻转2(n-1)次就可以全部排序。这未免太麻烦了点,我的意思是,假设这堆烙饼里存在已经排好序的序列,那岂不是多费工夫?
凭直觉来猜想,我们应该从把小一点的烙饼序列进行排序开始做起。
我的意思是,在每次翻转的时候,把两个本来应该相邻的烙饼尽可能翻到一起。这样,当所有的烙饼都换到一起后,基本上整堆烙饼就排序了。但这样想有个问题,不一定成功,且一旦出一点差错就会耗费许多步数。例如:
再这样的基础上,本能的会想到穷举。
沿着这个思路去考虑,我们自然就会想到使用 动态规划 或 递归 的方法来实现了。
如果采用递归,就一定存在一个退出的条件,无非是烙饼序列已排序,或者翻转的次数超过2(n-1),这时这个算法就应该直接被放弃。
我们有这个基础思想之后,就很容易形成这样的一个搜索最优方案的程序。(C++实现)
方便理解,我尝试把排序主函数放在前面。
class CPrefixSorting
{
public:
CPrefixSorting()
{
m_nCakeCnt = 0;
m_nMaxSwap = 0;
}
~CPrefixSorting()
{
if (m_CakeArray != NULL)
delete m_CakeArray;
if (m_SwapArray != NULL)
delete m_SwapArray;
if (m_ReverseCakeArray != NULL)
delete m_ReverseCakeArray;
if (m_ReverseCakeArraySwap != NULL)
delete m_ReverseCakeArraySwap;
}
void Run(int* pCakeArray, int nCakeCnt)
{
Init(pCakeArray, nCakeCnt);
m_nSearch = 0;
Search(0);
}
void Output()
{
for (int i = 0; i < m_nMaxSwap; i++)
printf("%d", m_arrSwap[i]);
printf("\n |Search Times:%d| \n", m_nSearch);
printf("Total Swap Times=%d \n", m_nMaxSwap);
}
private:
void Init(int* pCakeArray, int nCakeCnt)
{
Assert(pCakeArray != NULL);
Assert(nCakeCnt > 0);
m_nCakeCnt = nCakeCnt;
m_CakeArray = new int[m_nCakeCnt];
Assert(m_CakeArray != NULL);
for (int i = 0; i < m_nCakeCnt; i++)
{
m_CakeArray[i] = pCakeArray[i];
}
m_nMaxSwap = UpBound(m_nCakeCnt);
m_ReverseCakeArray = new int[m_nMaxSwap + 1];
Assert(m_SwapArray != NULL);
m_ReverseCakeArray = new int[m_nCakeCnt];
for (i = 0; i < m_nCakeCnt; i++)
{
m_ReverseCakeArray[i] = m_CakeArray[i];
}
m_ReverseCakeArraySwap = new int[m_nCakeCnt];
}
int UpBound(int nCakeCnt)
{
return nCakeCnt * 2;
}
int LowerBound(int* pCakeArray, int nCakeCnt)
{
int t, ret = 0;
for (int i = 1; i < nCakeCnt; i++)
{
//判断相邻的两个烙饼尺寸是不是相邻的
t = pCakeArray[i] - pCakeArray[i - 1];
if ((t == 1) || (t == -1)) {}
else
{
ret++;
}
}
}
void Search(int step)
{
int i, nEstimate;
m_nSearch++;
nEstimate = LowerBound(m_ReverseCakeArray, m_nCakeCnt);
if (step + nEstimate > m_nMaxSwap)
return;
if (IsSorted(m_ReverseCakeArray, m_nCakeCnt))
{
if (step < m_nMaxSwap)
{
m_nMaxSwap = step;
for (i = 0; i < m_nMaxSwap; i++)
{
m_arrSwap[i] = m_ReverseCakeArraySwap[i];
}
return;
}
}
//通过递归反转
for (i = 1; i < m_nCakeCnt; i++)
{
Revert(0, i);
m_ReverseCakeArraySwap[step] = i;
Search(step + 1);
Revert(0, i);
}
}
bool IsSorted(int* pCakeArray, int cCakeCnt)
{
for (int i = 1; i < nCakeCnt; i++)
{
if (pCakeArray[i - 1] > pCakeArray[i])
return false;
}
return true;
}
void Revert(int nBegin, int nEnd)
{
Assert(nEnd > nBegin);
int i, j, t;
for (i = nBegin; j = nEnd; i++, j--)
{
t = m_ReverseCakeArray[i];
m_ReverseCakeArray[i] = m_ReverseCakeArray[j];
m_ReverseCakeArray[j] = t;
}
}
private:
int* m_CakeArray;
int m_nCakeCnt;
int m_nMaxSwap;
int* m_SwapArray;
int* m_ReverseCakeArray;
int* m_ReverseCakeArraySwap;
int m_nSearch;
};
读者阅读时建议顺序从后往前,更容易理解。
当烙饼不多的时候,我们已经可以很快地找出最优的反转方案。
我们还可以进一步优化,程序中有一个剪枝。更进一步的优化,将在(下)篇呈现。
//吃烙饼的时候,不妨思考下这个问题?^_^