编程之美-一摞烙饼的排序 《编程之美》读书笔记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

相关文章
相关标签/搜索