烙饼排序(Bounds for Sorting by Prefix Reversal)(上)-编程之美初学者参考文档 1.3

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;
};

读者阅读时建议顺序从后往前,更容易理解。

当烙饼不多的时候,我们已经可以很快地找出最优的反转方案。

我们还可以进一步优化,程序中有一个剪枝。更进一步的优化,将在(下)篇呈现。

//吃烙饼的时候,不妨思考下这个问题?^_^

猜你喜欢

转载自blog.csdn.net/qq_25982223/article/details/84166317