1. 程式人生 > >排序演算法總結分析(三)——吃貨排序之烙餅排序

排序演算法總結分析(三)——吃貨排序之烙餅排序

目錄

今天先來個好玩點的,呃,確切說是好吃的點的問題。哈哈,就是如標題表明的烙餅排序。程式猿果然思維跟普通人就不一樣,連吃個餅都想的這麼多。問題描述是這樣的:把一摞餅按照大小次序擺好,要求是小的在上面,大的在下面,只能通過翻轉一摞餅進行排序,就像用鏟子插入某個位置,把這個位置之上的所有餅進行翻轉。那假設有N塊大小不一的烙餅,最少要翻轉幾次才能達到最終有序排列呢?


翻轉演示圖

與傳統排序不同的是,不能一張張抽出來,然後插入進去;也不能任意交換兩塊餅。說明基本的排序演算法都不太好用。是不是有點意思呢?想當年比爾·蓋茨也研究過這個問題~

目前這個問題的答案只有一個範圍,沒有確切的值,完成排序需要的最少次數在(15/14)N與(18/11)N之間。2011年的時候這個問題被定義為NP-Hard。什麼是NP-Hard?NP(Non-Deterministic)官方定義是非確定性多項式。而非確定性是指,可用一定數量的運算去解決多項式時間內可解決的問題。例如,著名的推銷員旅行問題(Travel Saleman Problem or TSP):假設一個推銷員需要從香港出發,經過廣州,北京,上海,…,等 n 個城市,最後返回香港。任意兩個城市之間都有飛機直達,但票價不等。假設公司只給報銷C元錢,問是否存在一個行程安排,使得他能遍歷所有城市,而且總的路費小於C?推銷員旅行問題顯然是NP的。因為如果你任意給出一個行程安排,可以很容易算出旅行總開銷。但是,要想知道一條總路費小於C的行程是否存在,在最壞情況下,必須檢查所有可能的旅行安排!這將是個天文數字。

好了介紹性的東西說的差不多了,下面主要講一下怎麼個排法~

由於每次操作都是針對最上面的餅,如果最底層的餅已經排好序,然後就只需要處理上面的N-1個餅了。


翻轉圖

首先,經過兩次翻轉,最大的餅已經在最下面了。接著次大的餅也需要兩次翻轉,最後剩兩張餅的時候只需要1次就可以,所以總的至少翻轉次數為2(n-1)-1即2n-3。

當然這是這個問題解的一種上限,非最優。

那麼下限呢?這裡也只說一種簡單的優化,非最佳。每一次翻轉最多使得一個烙餅與大小跟它相鄰的烙餅排到一起。如果當前狀態N個烙餅中,有M對相鄰的烙餅它們不相鄰,那麼至少需要M次才能排好。

下面稍微介紹下優化方法及演算法的實現。

假如這堆烙餅中有好幾個不同的部分相對有序,就可以先把小一些的烙餅翻轉使其有序。這樣就會減少翻轉次數。可以考慮每次翻轉的時候,把兩個本來應該相鄰的烙餅儘可能換到一起。這樣,當所有的烙餅都換到一起之後。實際上就完成了排序。這樣的話就會想到使用動態規劃或者遞迴的方法來實現。可以從不同的翻轉策略開始,遞迴所有可能性。這樣,肯定能找到最優解。

程式碼如下:

#include <stdio.h>
/************************************************************************/
/* 烙餅排序實現——By Sin_Geek 2014.04.12                               */
/************************************************************************/
class CPancakeSorting
{
public:
	CPancakeSorting()
	{
		m_nCakeCnt = 0;
		m_nMaxSwap = 0;
	}
	
	//計算烙餅翻轉資訊,pCakeArray 儲存烙餅索引陣列,nCakeCnt烙餅個數
	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("\nSearch Times : %d\n", m_nSearch);
		printf("Total Swap times = %d\n", m_nMaxSwap);
	}

private:
	//初始化陣列資訊,pCakeArray 儲存烙餅索引陣列,nCakeCnt烙餅個數
	void Init(int* pCakeArray, int nCakeCnt)
	{
		m_nCakeCnt = nCakeCnt;

        //初始化
		m_CakeArray = new int[m_nCakeCnt];

		for (int i = 0; i < m_nCakeCnt; i++)
		{
			m_CakeArray[i] = pCakeArray[i];
		}

		//設定最多交換次數資訊
		m_nMaxSwap = UpBound(m_nCakeCnt);

		//初始化交換結果陣列
		m_SwapArray = new int[m_nMaxSwap];

		//初始化中間交換結果資訊
		m_ReverseCakeArray = new int[m_nCakeCnt];
		for (i = 0; i < m_nCakeCnt; i++)
		{
			m_ReverseCakeArray[i] = m_CakeArray[i];
		}
		m_ReverseCakeArraySwap = new int[m_nMaxSwap];
	}

	//尋找當前翻轉的上界
	int UpBound(int nCakeCnt)
	{
		return nCakeCnt*2 - 3;
	}

	//尋找當前翻轉的下界
	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++;
			}
		}
		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_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 nCakeCnt)
	{
		for (int i = 1; i < m_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; i++,j--)
		{
			t = m_ReverseCakeArray[i];
			m_ReverseCakeArray[i] = m_ReverseCakeArray[j];
			m_ReverseCakeArray[j] = t;
		}
	}

private:
	int m_nCakeCnt;       //烙餅個數
	int m_nMaxSwap;       //最多交換次數
	int m_nSearch;        //當前搜尋次數
	int* m_CakeArray;     //烙餅資訊陣列
	int* m_SwapArray;     //交換結果陣列
	int* m_ReverseCakeArray;//當前翻轉烙餅資訊陣列
	int* m_ReverseCakeArraySwap;//當前翻轉烙餅交換結果陣列
	int m_arrSwap[10];

};
void main()
{
	int nCakeCnt = 10;
	int arrSwap[10] = {3,2,1,6,5,4,9,8,7,0} ;
	CPancakeSorting pan;
	pan.Run(arrSwap, nCakeCnt);
	pan.Output();
}

當然還可以把這個問題更復雜化一點,假定每個烙餅都有一面是烤過的,在原來排序的結果上附加一個條件,就是讓所有烤過的那一面都朝下~有興趣的可以思考一下哦~~


相關推薦

排序演算法總結分析——排序烙餅排序

目錄 今天先來個好玩點的,呃,確切說是好吃的點的問題。哈哈,就是如標題表明的烙餅排序。程式猿果然思維跟普通人就不一樣,連吃個餅都想的這麼多。問題描述是這樣的:把一摞餅按照大小次序擺好,要求是小的在上面,大的在下面,只能通過翻轉一摞餅進行排序,就像用鏟子插入某個位置,

從零開始Rtklib解讀篇-簡單的程式設計理論和演算法及結構分析

1. argc和argv argc和argv中的arg指的是"引數",首先是一個計算提供的引數到程式,第二個是對字串陣列的指標 argc: 整數,用來統計你執行程式時送給main函式的命令列引數的個數 * argv[ ]: 字串陣列,用來存放指向你

[演算法天天見]另類排序

另類排序 一、概述 一、概述 這裡主要介紹 桶排序,比如有一個實時排名系統,要對一萬個玩家的分數進行排序,分數為 0-9999分,應該怎麼排序呢?我們的方法是 建立 10000個桶 分別對應分數 0-9999的玩家,初始先遍歷一遍玩家分數放到對應桶

演算法設計與分析

53.Course Schedule There are a total of n courses you have to take, labeled from 0 to n-1. Some courses may have prerequisites, for

聚類分析 K中心點演算法k-mediods

K 中心點演算法( K-medoids ) 前面介紹了 k-means 演算法,並列舉了該演算法的缺點。而 K 中心點演算法( K-medoids )正好能解決 k-means 演算法中的 “噪聲”敏感這個問題。 如何解決的呢? 首先,我們得介紹下 k-means 演算法為什麼會對“噪聲”敏感。還記

HEVC位元速率控制演算法研究與HM相應程式碼分析——演算法及程式碼分析

在前兩篇文章中,首先介紹了HEVC標準和編碼流程,然後介紹了在HEVC中採用的全新的R-λ模型,本文將基於前面的內容和相應程式碼對位元速率控制演算法進行詳細的分析。 下面基於JCTVC-K0103提案詳細介紹一下HEVC中基於R-λ模型的位元速率控制方法。同時基於HM-10

模塊分析

1.7 space 後置 狀態 你在 處理 節點 要點 不同 做設計很重要一點就是要考慮匹配設計,就是你的設計一定要有一個意識,就是要回歸原始狀態;也可以稱之為閉環誰急,而且是小地方的閉環,以及異常情況的閉環;比如傳輸你在一個地方加密,解密一般都會考慮到;但是如果是壓縮,邏

JStorm與Storm源碼分析--Scheduler,調度器

系統 負責 bad 二維碼 sting storm return prepare end Scheduler作為Storm的調度器,負責為Topology分配可用資源。 Storm提供了IScheduler接口,用戶可以通過實現該接口來自定義Scheduler。 其定義如下

Java數據結構和算法——冒泡、選擇、插入排序算法

我們 逆序排列 pub 多少 img 目錄 http 最小 數據結構 目錄 1、冒泡排序 2、選擇排序 3、插入排序 4、總結   上一篇博客我們實現的數組結構是無序的,也就是純粹按照插入順序進行排列,那麽如何進行元素排序,本篇博客我們介紹幾種簡單的排序算

Ocata Neutron代碼分析——oslo_service中的ServiceLauncher和ProcessLauncher轉載

mic return cme down ice post you tin system 1.概述 Openstack中有一個叫Launcher的概念,即專門用來啟動服務的,這個類被放在了oslo_service這個包裏面。Launcher分為兩種,一種是ServiceL

大數據入門第二十二天——spark自定義分區、排序與查找

get buffer arr clas ron arm scala mut all 一、自定義分區   1.概述     默認的是Hash的分區策略,這點和Hadoop是類似的,具體的分區介紹,參見:https://blog.csdn.net/high2011/arti

Linux入侵分析清理木馬和問題用戶

入侵分析 雲安全 清除木馬 安全加固 Linux 1.查看哪些用戶擁有/bin/bash權限 cat /etc/passwd 2.檢查常用命令是否被篡改 (1)找到命令文件的路徑(whereis和which) whereis netstat which netstat which net

2018暑期周總結報告

輸出 long fin 不能 字母 算數 程序 3.3 簡單的 知識要點: 1,運行程序之前必須點保存(ctrl+s);2,定義由多單詞組成的變量時首字母小寫,後面的單詞首字母大寫,如int myAge;3, 定義常量時用關鍵字final寫在最前邊,標識符名稱全部大寫。

JAVA知識點總結

null 軟件開發 形式 業務需求 存儲 valueof 實現接口 運行 包裝 抽象類 使用規則 abstract定義抽象類; abstract定義抽象方法,只有聲明,不需要實現; 包含抽象方法的類是抽象類; 抽象類中可以包含普通方法,也可以沒有抽象方法; 抽象類不

Android ADB 原始碼分析

前言 之前分析的兩篇文章 Android Adb 原始碼分析(一) 嵌入式Linux:Android root破解原理(二)   寫完之後,都沒有寫到相關的實現程式碼,這篇文章寫下ADB的通訊流程的一些細節 看這篇文章之前,請先閱讀 Linux的SOCKET

Flume NG原始碼分析使用Event介面表示資料流

Flume NG有4個主要的元件: Event表示在Flume各個Agent之間傳遞的資料流 Source表示從外部源接收Event資料流,然後傳遞給Channel Channel表示對從Source傳遞的Event資料流的臨時儲存 Sink表示從Channel中接收儲存的Event

GCC原始碼分析——中間語言

原文連結:http://blog.csdn.net/sonicling/article/details/7915301 一、前言   很忙,很久沒更新部落格了,繼續沒寫完的gcc分析,爭取在傳說將要用C++重寫的gcc 5出來之前初略分析完。 二、符號表(GENERI

數值分析:C++實現線性方程組的高斯-賽德爾迭代法

線性方程組的直接解法之後,就輪到迭代解法了,直接解法針對的是低階稠密矩陣,資料量較少,而工程上有更多的是高階係數矩陣,使用迭代法效率更高,佔用的空間較小。 迭代法的最基本思想就是由初始條件,比如說初始解向量隨便列舉一個,就0向量也行,然後進行迭代,k到k+1,一步一步從k=1開始去逼近真實解

zigbee ZStack-2.5.1a原始碼分析無線資料傳送和接收

前面說過SampleApp_Init和SampleApp_ProcessEvent是我們重點關注的函式,接下來分析無線傳送和接收相關的程式碼: 在SampleApp_ProcessEvent函式中: if ( events & SYS_EVENT_MSG ) {  &nbs

java併發程式設計一一執行緒池原理分析

合理的設定執行緒池的大小 接著上一篇探討執行緒留下的尾巴。如果合理的設定執行緒池的大小。 要想合理的配置執行緒池的大小、首先得分析任務的特性,可以從以下幾個角度分析: 1、任務的性質:CPU密集型任務、IO密集型任務、混合型任務等; 2、任務的優先順序:高、中、低; 3、任務的執行時