1.3 一摞烙饼的排序
参考《编程之美–1.3 一摞烙饼的排序》
问题描述:
一摞乱序摆放的烙饼,每次只能抓取最上面几块烙饼并翻转,多次翻转后能够实现烙饼的从小到大(从上往下)的有序摆放。
问题分析:
这里我们使用回溯法解决这个问题。直接用回溯法效率是低下的,因此要进行剪枝。这里的剪枝条件是利用翻转次数的上界和下界完成的。
上界:
[4,2,1,5,3] -> [5,1,2,4,3] -> [3,4,2,1,5]
两步可以按大小顺序将某块饼放到它应该在的位置。
同理,可以把其他四块烙饼摆放好,要注意最后一块烙饼会在最后第二块烙饼摆放正确后位于正确位置。假设烙饼个数为n,则翻转次数上界为2(n-1)。
下界:
上界我们已经得出了,下面考虑下界。在试验翻转的时候,我们可以发现,当烙饼堆部分有序时,翻转次数较少。若一摞烙饼中有m对相邻的烙饼半径不相邻,理想情况下需要m次翻转来排序,[3,4,2,1,5,6],此时就需要两次来翻转。
代码如下:
#include <iostream>
#include <windows.h>
#include <math.h>
#include <assert.h>
using namespace std;
class PreFixSorting
{
public:
PreFixSorting()
{
CakeCount = 0;
MaxSwap = 0;
}
~PreFixSorting()
{
if(CakeArray != NULL)
{
delete CakeArray;
}
if(SwapArray != NULL)
{
delete SwapArray;
}
if(ReverseCakeArray != NULL)
{
delete ReverseCakeArray;
}
if(ReverseCakeArraySwap != NULL)
{
delete ReverseCakeArraySwap;
}
}
//计算烙饼翻转信息
//@param
//pCakeArray 存储烙饼索引数组,索引大的烙饼个头大
//pCakeCount 烙饼个数
void Run(int* pCakeArray, int pCakeCount)
{
Init(pCakeArray, pCakeCount);
nSearch = 0;
Search(0);
}
void Output()
{
for(int i = 0; i < MaxSwap; i++)
{
cout<<SwapArray[i]<<" ";
}
cout<<'\n'<<"|Search Times|: "<<nSearch<<endl;
cout<<"Total Swap Times = "<<MaxSwap<<endl;
}
private:
//初始化数组信息
//@param
//pCakeArray 存储烙饼索引数组,索引大的烙饼个头大
//pCakeCount 烙饼个数
void Init(int *pCakeArray, int pCakeCount)
{
assert(pCakeArray != NULL);
assert(pCakeCount > 0);
CakeCount = pCakeCount;
//初始化烙饼数组
CakeArray = new int[CakeCount];
assert(CakeArray != NULL);
for(int i = 0; i < CakeCount; i++)
{
CakeArray[i] = pCakeArray[i];
}
//获取最多交换次数
MaxSwap = UpperBound(CakeCount);
//初始化交换结果数组
SwapArray = new int[MaxSwap + 1];
assert(SwapArray != NULL);
//初始化中间交换结果数组
ReverseCakeArray = new int[CakeCount];
for(int i = 0; i < CakeCount; i++)
{
ReverseCakeArray[i] = CakeArray[i];
}
ReverseCakeArraySwap = new int[MaxSwap];
}
//寻找当前翻转上界
int UpperBound(int pCakeCount)
{
return (pCakeCount-1)*2;
}
//寻找当前翻转下界
int LowerBound(int* pCakeArray, int pCakeCount)
{
int t, ret = 0;
//根据当前数组排序情况判断最少需要交换的次数
for(int i = 1; i < pCakeCount; i++)
{
//判断位置相邻的两个烙饼是否索引相邻
t = pCakeArray[i] - pCakeArray[i-1];
if((t == 1) || (t == -1))
{
}
else
{
ret++ ;
}
}
return ret;
}
//排序主函数
void Search(int step)
{
int i, nEstimate;
nSearch++;
//估算交换次数下界
nEstimate = LowerBound(ReverseCakeArray, CakeCount);
if(step + nEstimate > MaxSwap )
{
return;
}
//若已排好序则输出结果
if(IsSorted(ReverseCakeArray, CakeCount))
{
if(step < MaxSwap)
{
MaxSwap = step;
for(int i = 0; i < MaxSwap; i++)
{
SwapArray[i] = ReverseCakeArraySwap[i];
}
return;
}
}
//回溯法递归翻转
for(i = 1; i < CakeCount; i++)
{
Reverse(0, i);
ReverseCakeArraySwap[step] = i;
Search(step + 1);
Reverse(0, i);
}
}
//true: 已排序
//false: 未排序
bool IsSorted(int* pCakeArray, int pCakeCount)
{
for(int i = 1; i < pCakeCount; i++)
{
if(pCakeArray[i-1] > pCakeArray[i])
{
return false;
}
}
return true;
}
//翻转烙饼数组
void Reverse(int nBegin, int nEnd)
{
assert(nEnd > nBegin);
int i, j, t;
for(i = nBegin, j = nEnd; i < j; i++, j--)
{
t = ReverseCakeArray[i];
ReverseCakeArray[i] = ReverseCakeArray[j];
ReverseCakeArray[j] = t;
}
}
private:
int* CakeArray; //烙饼数组
int CakeCount; //烙饼个数
int MaxSwap; //交换次数上界
int* SwapArray; //翻转烙饼结果数组(翻转前几层烙饼,用于输出)
int* ReverseCakeArray; //当前转烙饼数组(中间状态)
int* ReverseCakeArraySwap; //当前翻转烙饼结果数组
int nSearch; //当前搜索次数
};
int main()
{
PreFixSorting pfs;
int CakeArray[] = {4,2,1,5,3};
int CakeCount = 5;
pfs.Run(CakeArray,CakeCount);
pfs.Output();
return 0;
}
注意: 关于这里的上下界,其实目前研究的结果是上界最小为(5n+5)/3向上取整,下界最大为15n/14向上取整。用这个上下界的搜索次数更少,效率更高(n较大时)。