问题描述
星期五的晚上,一帮同事在希格玛大厦附近的“硬盘酒吧”多喝了几杯,程序员多喝了几杯之后谈什么呢?自然是算法
问题。有个同事说:
“我以前在餐厅打工,顾客经常点非常多的烙饼。店里的烙饼大小不一,我习惯在到达顾客饭桌前,把一摞饼按照大小
次序摆好---小的在上面,大的在下面。由于我一只手托着盘子,只好用另一只手,一次抓住最上面的几块饼,把它们
上下颠倒个个儿,反复几次之后,这摞烙饼就排好序了。我后来想,这实际上是个有趣的排序问题:假设有n块大小
不一的摞饼,那最少要翻几次,才能达到大小有序的结果呢?”
从这段描述中我们可以很容易的就把问题抽象出来:给你一个由 n 个连续整数组成的数组,数组是无序的,现在要你
对这个数组进行升序排序,但只能对数组进行一种操作,就是只能对从数组第一个元素开始到数组中任意一个元素之
间的所有元素进行翻转。只是这样的话,问题还不是很麻烦。这里还要求我们写程序输出最优的翻转过程。
思路:
一般遇到最优解问题我们首先想到的就是动态规划、贪心以及分支界限三种方法。书中提到了动态规划和分支界限这
两种方法,但只给出了分支界限方法的思路以及代码,所以我就以书上的方法来讲讲自己的理解吧。
书上的代码用的是递归遍历+剪枝。既然是递归,就得有个退出的条件,不然就死循环了。在这个问题中,第一个递
归退出的条件理所当然就是所有的烙饼都已经排好序了。如果只是这样的,就不涉及到剪枝了。我们知道递归的特点
是简洁,但是其效率相对来说是比较低的,如果遇到很大的问题,在现实中就不适用了,而提高递归效率的唯一方法
就是剪枝,所以我们必须想办法找到剪枝的策略。
一般来说我们都是通过去除一些不必要的遍历来进行剪枝的,分析这个问题,我们不难发现,最多要翻转的次数不会
超过2*(n-1)次,至于这个数字是怎么来的,书上已经写的很明白了,这里就不多作累赘啦。如果只是等某种翻转次数
超过2*(n-1)再来放弃这种遍历的话,显然意义不大。按照书上给出的思路,以2*(n-1)作为上界(UpperBound),表
示最差方案;而下界(LowerBound)则是动态的,下界的取值是当前排序状态下,我们至少还要翻转的次数,程序
中我们用变量 nEstimate 表示。当当前翻转次数+nEstimate>UpperBound()时,说明继续遍历下去最后的结果会超过
上界,这时我们直接对其进行剪枝。
代码:
#include<cassert>
#include<iostream>
using namespace std;
class PrefixSorting
{
int* m_CakeArray; //烙饼信息数组
int m_nCakeCnt; //烙饼个数
int m_nMaxSwap; //最多翻转次数,即上界
int* m_SwapArray; //交换结果数组,即保存最优解的每一步交换
int* m_ReverseCakeArray; //当前翻转烙饼信息数组
int* m_ReverseCakeArraySwap; //当前翻转烙饼交换结果数组
int m_nSearch; //当前遍历次数信息
public:
PrefixSorting()
{
m_nCakeCnt = 0;
m_nMaxSwap = 0;
}
~PrefixSorting()
{
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\n", m_SwapArray[i]);
printf("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 = UpperBound (m_nCakeCnt);
m_SwapArray = new int[m_nMaxSwap];
assert (m_SwapArray != NULL);
m_ReverseCakeArray = new int[m_nCakeCnt];
for (int i = 0; i < m_nCakeCnt; i++)
{
m_ReverseCakeArray[i] = m_CakeArray[i];
}
m_ReverseCakeArraySwap = new int[m_nMaxSwap];
}
/*计算上界*/
int UpperBound(int nCakeCnt)
{
return (nCakeCnt - 1) * 2; //书上这里是 nCakeCnt * 2
}
/*计算下界*/
int LowerBound(int* pCakeArray, int nCakeCnt)
{
int t;
int ret = 0;
for (int i = 1; i < nCakeCnt; i++)
{
t = pCakeArray[i] - pCakeArray[i - 1];
if ((t == 1) || (t == -1))
{
}
else
{
ret++;
}
}
return 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_SwapArray[i] = m_ReverseCakeArraySwap[i];
}
return ;
}
/*递归遍历*/
for (int i = 1; i < m_nCakeCnt; i++)
{
Reverse (0, i);
m_ReverseCakeArraySwap[step] = i;
Search (step + 1);
Reverse (0, i);
}
}
/*判断是否已经排好序*/
bool IsSorted(int* pCakeArray, int nCakeCnt)
{
for (int i = 1; i < nCakeCnt; 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 = m_ReverseCakeArray[i];
m_ReverseCakeArray[i] = m_ReverseCakeArray[j];
m_ReverseCakeArray[j] = t;
}
}
};
int main()
{
int a[] = {3, 2, 1, 6, 5, 4, 9, 8, 7, 0};
PrefixSorting test;
test.Run(a, 10);
test.Output();
system("pause");
return 0;
}