編程之美-一摞烙餅的排序 《編程之美》讀書筆記02: 1.3 一摞烙餅的排序

最終代碼 

Code highlighting produced by Actipro CodeHighlighter (freeware)http://www.CodeHighlighter.com/-->//1.3_pancake_f.cpp   by  flyingheart # qq.com
#include<iostream>
#include<fstream>
#include<vector>
#include<algorithm>
#include<ctime>
using namespace std;

class Pancake{
 public:
  Pancake() {}
  void print() const;
  void process();               //顯示最優解的翻轉過程
  int run(const int cake_arr[], int size, bool show=true);
  void calc_range(int na, int nb);

 private: 
  Pancake(const Pancake&);
  Pancake& operator=(const Pancake&);
  inline bool init(const int cake_arr[], int& size);
  void search_cake(int size, int step, int least_swap_old);
  void reverse_cake(int index) { //翻轉0到index間的烙餅
    ++count_reverse; 
    std::reverse(&cake[0], &cake[index + 1]);
  }
  
  bool next_search_cake(int pos, int size, int step, int least_swap)
  {
    if (least_swap + step >= get_min_swap()) return true;
    cake_swap[step] = pos;
    reverse_cake(pos);
    search_cake(size,step,least_swap);
    reverse_cake(pos);
    return false;
  }
  
  int get_min_swap() const { return result.size();}
  
  void output(int i, const std::string& sep, int width) const {
    cout.width(width);
    cout << i << sep;
  }
  
  void output(const std::string& sep, int width) const {
    cout.width(width);
    cout << sep;
  }
  
  vector<int> cake_old;        //要處理的原烙餅數組
  vector<int> cake;            //當前各個烙餅的狀態
  vector<int> result;          //最優解中,每次翻轉的烙餅位置
  vector<int> cake_swap;       //每次翻轉的烙餅位置
  vector<int> cake_order;      //第step+1次翻轉時,翻轉位置的優先順序
  int min_swap_init;           //最優解的翻轉次數初始值
  int count_search;            //search_cake被調用次數
  int count_reverse;           //reverse_cake被調用次數
};


void Pancake::print() const
{
  int min_swap = get_min_swap();
  if (min_swap == 0) return; 
  cout << "minimal_swap initial: " << min_swap_init 
       << "  final: "<< min_swap 
       << "\nsearch/reverse function was called: " << count_search
       << "/" << count_reverse << " times\nsolution: ";
  for (int i = 0; i < min_swap; ++i) cout << result[i] << " ";
  cout<< "\n\n";
}

void Pancake::process()
{
  int min_swap = get_min_swap(); 
  if (min_swap == 0) return;
  cake.assign(cake_old.begin(), cake_old.end());
  int cake_size = cake_old.size();
  const int width = 3, width2 = 2 * width + 3;
  output("No.", width2);
  for (int j = 0; j < cake_size; ++j) output(j," ",width);   
  cout << "\n";     
  output("old:", width2);    
  
  for (int j = 0; j < cake_size; ++j) output(cake[j]," ",width);  
  cout << "\n";        
  
  for (int i = 0; i < min_swap; ++i){
    reverse_cake(result[i]);
    output(i + 1," ",width);  
    output(result[i],": ",width);  
    for (int j = 0; j < cake_size; ++j)  output(cake[j]," ",width);  
    cout << "\n";
  }
  cout << "\n\n";
}

bool Pancake::init(const int cake_arr[], int& size)
{
  result.clear();
  if (cake_arr == NULL) return false;
  cake_swap.resize(size * 2);
  cake_order.resize(size * size * 2);
  count_search = 0;
  count_reverse = 0;
  cake_old.assign(cake_arr,cake_arr + size);
  //去除末尾已就位的烙餅,修正烙餅數組大小。
  while (size > 1 && size - 1 == cake_arr[size - 1]) --size; 
  if (size <= 1) return false;
  
  cake.assign(cake_arr,cake_arr + size);
  for (int j = size - 1; ;) {           //計算一個解做爲min_swap初始值。
    while(j > 0 && j == cake[j]) --j;
    if (j <= 0) break;
    int i = j;
    while (i >= 0 && cake[i] != j) --i;
    if (i != 0) {
      reverse_cake(i);
      result.push_back(i);
    }
    reverse_cake(j);
    result.push_back(j);
    --j;
  }
  cake.assign(cake_arr,cake_arr + size); //恢復原來的數組
  cake.push_back(size);                 //多放一個烙餅,避免後面的邊界判斷
  cake_swap[0] = 0;                     //假設第0步翻轉的烙餅編號爲0
  min_swap_init= get_min_swap();
  return true;
}

int Pancake::run(const int cake_arr[], int size, bool show)
{
  if (! init(cake_arr, size)) return 0;
  int least_swap = 0;
  //size = cake.size() - 1;
  for (int i = 0; i < size; ++i)
    if (cake[i] - cake[i + 1] + 1u > 2) ++least_swap;  
  if (get_min_swap() != least_swap) search_cake(size, 0, least_swap);
  if (show) print();
  return get_min_swap();
}

void Pancake::search_cake(int size, int step, int least_swap_old)
{
  ++count_search;
  while (size > 1 && size - 1 == (int)cake[size - 1]) --size; //去除末尾已就位的烙餅
  int *first = &cake_order[step * cake.size()];
  int *last = first + size;
  int *low = first, *high = first + size;

  for (int pos = size - 1, last_swap = cake_swap[step++]; pos > 0; --pos){
    if (pos == last_swap) continue;
    int least_swap = least_swap_old ;
    if (cake[pos] - cake[pos + 1] + 1u <= 2) ++least_swap;
    if (cake[0] - cake[pos + 1] + 1u <= 2) --least_swap;

    if (least_swap + step >= get_min_swap()) continue;
    if (least_swap == 0) {
      cake_swap[step] = pos;
      result.assign(&cake_swap[1], &cake_swap[step + 1]);
      return;
    }
    
    //根據least_swap值大小,分別保存pos值,並先處理使least_swap_old減少1的翻轉
    if (least_swap == least_swap_old) *low++ =pos;
    else if (least_swap > least_swap_old) *--high =pos;
    else next_search_cake(pos, size, step, least_swap);   
  }

  //再處理使least_swap_old不變的翻轉
  for(int *p = first; p < low; p++)
    if (next_search_cake(*p, size, step, least_swap_old)) return;
    
  //最後處理使least_swap_old增長1的翻轉
  for(int *p = high; p < last; p++)
    if (next_search_cake(*p, size, step, least_swap_old + 1)) return;
}

void Pancake::calc_range(int na, int nb)
{
  if (na > nb || na <= 0) return;
  clock_t ta = clock();
  static std::vector<int> arr;
  arr.resize(nb);
  unsigned long long total_search = 0;
  unsigned long long total_reverse = 0;
  for (int j = na; j <= nb; ++j) {
    for (int i = 0; i < j; ++i) arr[i] = i;
    int max = 0;
    unsigned long long count_s = 0;
    unsigned long long count_r = 0;
    clock_t tb = clock();
    while (std::next_permutation(&arr[0], &arr[j])) {    
      int tmp = run(&arr[0],j,0);
      if (tmp > max) max = tmp;
      count_s += count_search;
      count_r += count_reverse;
    }
    total_search +=  count_s;
    total_reverse += count_r;
    output(j, " ",2);
    output(max,"     time: ",3);
    output(clock() - tb,"  ms  ",8);
    cout << " search/reverse: " << count_s << "/" << count_r << "\n";
  }
  cout << "  total  search/reverse: " << total_search
       << "/" << total_reverse << "\n"
       << "time :  " << clock() - ta << "  ms\n";
}

int main()
{
  int aa[10]={ 3,2,1,6,5,4,9,8,7,0};
  //int ab[10]={ 4,8,3,1,5,2,9,6,7,0};
 // int ac[]={1,0, 4, 3, 2};
  Pancake cake;
  cake.run(aa,10);
  cake.process();
  //cake.run(ab,10);
  //cake.process(); 
  //cake.run(ac,sizeof(ac)/sizeof(ac[0]));
  //cake.process();  
  cake.calc_range(1,9);
}

問題:ios

    星期五的晚上,一幫同事在希格瑪大廈附近的「硬盤酒吧」多喝了幾杯。程序員多喝了幾杯以後談什麼呢?天然是算法問題。有個同事說:「我之前在餐館打工,顧客常常點很是多的烙餅。店裏的餅大小不一,我習慣在到達顧客飯桌前,把一摞餅按照大小次序擺好——小的在上面,大的在下面。因爲我一隻手託着盤子,只好用另外一隻手,一次抓住最上面的幾塊餅,把它們上下顛倒個個兒,反覆幾回以後,這摞烙餅就排好序了。我後來想,這其實是個有趣的排序問題:假設有n塊大小不一的烙餅,那最少要翻幾回,才能達到最後大小有序的結果呢?」程序員

你可否寫出一個程序,對於n塊大小不一的烙餅,輸出最優化的翻餅過程呢?算法

 

n個烙餅通過翻轉後的全部狀態可組成一棵樹。尋找翻轉最少次數,至關於在樹中搜索層次最低的某個節點。編程

因爲每層的節點數呈幾何數量級增加,在n較大時,使用廣度優先遍歷樹,可能沒有足夠的內存來保存中間結果(考慮到每層的兩個節點,能夠經過旋轉,移位等操做互相轉換,也許每層的狀態能夠用一個函數來生成,這時能夠採用廣度優先方法),於是採用深度優先。但這棵樹是無限深的,必須限定搜索的深度(即最少翻轉次數的上限值),當深度達到該值時再也不繼續往下搜索。最少翻轉次數,必然小等於任何一種翻轉方案所需的翻轉次數,於是只要構造出一種方案,取其翻轉次數便可作爲其初始值。最簡單的翻轉方案就是:對最大的未就位的烙餅,將其翻轉,再找到最終結果中其所在的位置,翻轉一次使其就位。所以,對編號在n-12之間的烙餅,最多翻轉了2*(n-2)次,剩下01號烙餅最多翻轉1次,於是最少翻轉次數的上限值是:2*(n-2)+1=2*n-3(從網上可搜索到對該上限值最新研究結果:上限值爲18/11*n),固然,最好仍是直接計算出採用這種方案的翻轉次數作爲初始值數組

 

 

減小遍歷次數:函數

 

減少「最少翻轉次數上限值」的初始值,採用前面提到的翻轉方案,取其翻轉次數爲初始值。對書中的例子{3,2,1,6,5,4,9,8,7,0},初始值能夠取10優化

 

避免出現已處理過的狀態必定會減小遍歷嗎?答案是否認的,深度優先遍歷,必須遍歷完一個子樹,才能遍歷下一個子樹,若是一個解在某層比較靠後位置,若不容許處理已出現過的狀態時,可能要通過不少次搜索,才能找到這個解,但容許處理已出現過的狀態時,可能會很快找到這個解,並減少「最少翻轉次數的上限值」,使更多的分支能被剪掉,反而能減小遍歷的節點數。好比說,兩個子樹AB,搜索子樹A100次後可獲得一個對應翻轉次數爲20的解,搜索子樹B20次後可獲得翻轉次數爲10的解,不容許處理已出現過的狀態,就會花100次遍歷完子樹A後,纔開始遍歷B,但容許翻轉回上一次狀態,搜索會在AB間交叉進行,就可能只要70次找到子樹B的那個解(翻轉次數爲10+2=12),此時,翻轉次數上限值比較小,可忽略更多沒必要要的搜索。以書中的{3,2,1,6,5,4,9,8,7,0}爲例,按程序(1.3_pancake_1.cpp),不容許翻轉回上次狀態時需搜索195次,而容許翻轉回上次狀態時只要搜索116次。spa

 

若是最後的幾個烙餅已經就位,只須考慮前面的幾個烙餅。對狀態(0,1,3,4,2,5,6),編號爲56的烙餅已經就位,只須考慮前5個烙餅,即狀態(0,1,3,4,2)。若是一個最優解,從某次翻轉開始移動了一個已經就位的烙餅,且該烙餅後的全部烙餅都已經就位,那麼對這個解法,從此次翻轉開始獲得的一系列狀態,從中移除這個烙餅,可獲得一系列新的狀態。必然能夠設計出一個新的解法對應這系列新的狀態,而該解法所用的翻轉次數不會比原來的多。設計

 

估計每一個狀態還須要翻轉的最少次數(即下限值),加上當前的深度,若是大等於上限值,就無需繼續遍歷。這個下限值能夠這樣肯定:從最後一個位置開始,往前找到第一個與最終結果位置不一樣的烙餅編號(也就是說排除最後幾個已經就位的烙餅),從該位置到第一個位置,計算相鄰的烙餅的編號不連續的次數,再加上1每次翻轉最多隻能使不連續的次數減小1但不少人會忽略掉這個狀況:最大的烙餅沒有就位時,必然須要一次翻轉使其就位,而此次翻轉卻不改變不連續次數。(能夠在最後面增長一個更大的烙餅,使此次翻轉能夠改變不連續數。)如:對狀態(0,1,3,4,2,5,6)等同於狀態(0,1,3,4,2),因爲1342不連續,於是下限值爲2+1=3下限值也能夠這樣肯定:在最後面增長一個比全部烙餅都大的已經就位的烙餅,而後再計算不連續數。如:(0,1,3,4,2),能夠看做(0,1,3,4,2,5)1425這三個不連續,下限值爲3code

 

5多數狀況下,翻轉次數的上限值越大,搜索次數就越多。能夠採用貪心算法,經過調整每次全部可能翻轉的優先順序,儘快找到一個解,從而減小搜索次數。好比,優先搜索使「下限值」減小的翻轉,其次是使「下限值」不變的翻轉,最後才搜索使「下限值」增長的翻轉。對「下限值」不變的翻轉,還能夠根據其下次的翻轉對「下限值」的影響,再從新排序。因爲進行了優先排序,翻轉回上一次狀態能減小搜索次數的可能性獲得進一步下降。

 

其它剪枝方法:

假設進行第m次翻轉時,「上限值」爲min_swap

若是翻轉某個位置的烙餅能使全部烙餅就位(即翻轉次數恰好爲m),則翻轉其它位置的烙餅,能獲得的最少翻轉次數必然大等m,於是這些位置均可以不搜索。

若是在某個位置的翻轉後,「下限值」爲k,而且 k+m>=min_swap,則對全部的使新「下限值」kk大等於k的翻轉,都有 kk+m>=min_swap,於是均可以不搜索。該剪枝方法是對上面的「調整翻轉優先順序」的進一步補充。

 

另外,翻轉某個烙餅時,只有兩個烙餅位置的改變纔對「下限值」有影響,於是能夠記錄每一個狀態的「下限值」,進行下一次翻轉時,只須經過幾回比較,就能夠肯定新狀態的「下限值」。(判斷不連續次數時,最好寫成 -1<=x && x<=1, 而不是x==1 || x==-1。對於 int x; a<=x && x<=b,編譯器能夠將其優化爲 unsigned (x-a) <= b-a。)

 

 

結果:

 

對書上的例子{3,2,1,6,5,4,9,8,7,0}

 

翻轉回上次狀態

搜索函數被調用次數

翻轉函數被調用次數

1.3_pancake_2

不容許

29

66

1.3_pancake_2

容許

33

74

1.3_pancake_1

不容許

195

398

1.3_pancake_1

容許

116

240

(這個例子比較特殊,代碼1.3_pancake_2.cpp(與1.3_pancake_1.cpp的最主要區別在於,增長了對翻轉優先順序的判斷, 代碼下載),在不容許翻轉回上次狀態且取min_swap的初始值爲2*10-2=18時,調用搜索函數29次,翻轉函數56次)。

 

搜索順序對結果影響很大,若是將1.3_pancake_2.cpp第152行:

for (int pos=1, last_swap=cake_swap[step++]; pos<size; ++pos){

這一行改成:

for (int pos=size-1, last_swap=cake_swap[step++]; pos>=1; --pos){

僅僅調整了搜索順序,調用搜索函數次數由29次降到11次(對應的翻轉方法:9,6,9,6,9,6),求第1個烙餅數到第10個烙餅數,所用的總時間也由原來的38秒降到21秒。)


補充:

 

在網上下了《編程之美》「第6刷」的源代碼,結果在編譯時存在如下問題:

1 Assert 應該是 assert

2 m_arrSwap 未被定義,應該改成m_SwapArray

3 Init函數兩個for循環,後一個沒定義變量i,應該將改成 int i

另外,每運行一次Run函數,就會調用Init函數,就會申請新的內存,但卻沒有釋放原來的內存,會形成內存泄漏。if(step + nEstimate > m_nMaxSwap) 這句還會形成後面對m_ReverseCakeArraySwap數組的越界訪問,使程序不能正常運行。

 

書上程序的低效主要是因爲進行剪枝判斷時,沒有考慮好邊界條件,可進行以下修改:

1  if(step + nEstimate > m_nMaxSwap)  改成 >=

2  判斷下界時,若是最大的烙餅不在最後一個位置,則要多翻轉一次,於是在LowerBound函數return ret; 前插入一行:

if (pCakeArray[nCakeCnt-1] != nCakeCnt-1) ret++; 

3  n個烙餅,翻轉最大的n-2烙餅最多須要2*(n-2)次,剩下的2個最多1次,於是上限值爲2*n-3,所以,m_nMaxSwap初始值能夠取2*n-3+1=2*n-2,這樣每步與m_nMaxSwap的判斷就能夠取大等於號。

4  採用書上提到的肯定「上限值」的方法,直接構建一個初始解,取其翻轉次數爲m_nMaxSwap的初始值。

 

12任改一處,都能使搜索次數從172126降到兩萬多,兩處都改,搜索次數降到3475。若再改動第3處,搜索次數降到2989;若採用4的方法(此時初始值爲10),搜索次數可降到1045

相關文章
相關標籤/搜索