《编程之美》读书笔记02: 1.3 一摞烙饼的排序

探讨烙饼排序问题,通过深度优先搜索算法寻找最小翻转次数。文章分析不同搜索策略的影响,并提供优化建议。

 

 

《编程之美》读书笔记021.3 一摞烙饼的排序

 

问题:

    星期五的晚上,一帮同事在希格玛大厦附近的“硬盘酒吧”多喝了几杯。程序员多喝了几杯之后谈什么呢?自然是算法问题。有个同事说:“我以前在餐馆打工,顾客经常点非常多的烙饼。店里的饼大小不一,我习惯在到达顾客饭桌前,把一摞饼按照大小次序摆好——小的在上面,大的在下面。由于我一只手托着盘子,只好用另一只手,一次抓住最上面的几块饼,把它们上下颠倒个个儿,反复几次之后,这摞烙饼就排好序了。我后来想,这实际上是个有趣的排序问题:假设有n块大小不一的烙饼,那最少要翻几次,才能达到最后大小有序的结果呢?”

你能否写出一个程序,对于n块大小不一的烙饼,输出最优化的翻饼过程呢?

 

n个烙饼经过翻转后的所有状态可组成一棵树。寻找翻转最少次数,相当于在树中搜索层次最低的某个节点。

由于每层的节点数呈几何数量级增长,在n较大时,使用广度优先遍历树,可能没有足够的内存来保存中间结果(考虑到每层的两个节点,可以通过旋转,移位等操作互相转换,也许每层的状态可以用一个函数来生成,这时可以采用广度优先方法),因而采用深度优先。但这棵树是无限深的,必须限定搜索的深度(即最少翻转次数的上限值),当深度达到该值时不再继续往下搜索。最少翻转次数,必然小等于任何一种翻转方案所需的翻转次数,因而只要构造出一种方案,取其翻转次数即可做为其初始值。最简单的翻转方案就是:对最大的未就位的烙饼,将其翻转,再找到最终结果中其所在的位置,翻转一次使其就位。因此,对编号在n-12之间的烙饼,最多翻转了2*(n-2)次,剩下01号烙饼最多翻转1次,因而最少翻转次数的上限值是:2*(n-2)+1=2*n-3(从网上可搜索到对该上限值最新研究结果:上限值为18/11*n),当然,最好还是直接计算出采用这种方案的翻转次数做为初始值

 

 

减少遍历次数:

 

1 减小“最少翻转次数上限值”的初始值,采用前面提到的翻转方案,取其翻转次数为初始值。对书中的例子{3,2,1,6,5,4,9,8,7,0},初始值可以取10

 

2 避免出现已处理过的状态一定会减少遍历吗?答案是否定的,深度优先遍历,必须遍历完一个子树,才能遍历下一个子树,如果一个解在某层比较靠后位置,若不允许处理已出现过的状态时,可能要经过很多次搜索,才能找到这个解,但允许处理已出现过的状态时,可能会很快找到这个解,并减小“最少翻转次数的上限值”,使更多的分支能被剪掉,反而能减少遍历的节点数。比如说,两个子树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次。

 

3 如果最后的几个烙饼已经就位,只须考虑前面的几个烙饼。对状态(0,1,3,4,2,5,6),编号为56的烙饼已经就位,只须考虑前5个烙饼,即状态(0,1,3,4,2)。如果一个最优解,从某次翻转开始移动了一个已经就位的烙饼,且该烙饼后的所有烙饼都已经就位,那么对这个解法,从这次翻转开始得到的一系列状态,从中移除这个烙饼,可得到一系列新的状态。必然可以设计出一个新的解法对应这系列新的状态,而该解法所用的翻转次数不会比原来的多。

 

4 估计每个状态还需要翻转的最少次数(即下限值),加上当前的深度,如果大等于上限值,就无需继续遍历。这个下限值可以这样确定:从最后一个位置开始,往前找到第一个与最终结果位置不同的烙饼编号(也就是说排除最后几个已经就位的烙饼),从该位置到第一个位置,计算相邻的烙饼的编号不连续的次数,再加上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)13 42 25这三个不连续,下限值为3

 

5多数情况下,翻转次数的上限值越大,搜索次数就越多。可以采用贪心算法,通过调整每次所有可能翻转的优先顺序,尽快找到一个解,从而减少搜索次数。比如,优先搜索使“下限值”减少的翻转,其次是使“下限值”不变的翻转,最后才搜索使“下限值”增加的翻转。对“下限值”不变的翻转,还可以根据其下次的翻转对“下限值”的影响,再重新排序。由于进行了优先排序,翻转回上一次状态能减少搜索次数的可能性得到进一步降低。

 

6 其它剪枝方法:

假设进行第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秒。)

  

 

ExpandedBlockStart.gif 最终代码
// 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   * =  first; p  <  low; p ++ )
    
if  (next_search_cake( * p, size, step, least_swap_old))  return ;
    
  
// 最后处理使least_swap_old增加1的翻转
   for ( int   * =  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 );
}

 

 

 

补充:

 

在网上下了《编程之美》“第6刷”的源代码,结果在编译时存在以下问题:

1 Assert 应该是 assert

2 m_arrSwap 未被定义,应该改为m_SwapArray

3 Init函数两个for循环,后一个没定义变量i,应该将i 改为 int i

另外,每运行一次Run函数,就会调用Init函数,就会申请新的内存,但却没有释放原来的内存,会造成内存泄漏。if(step + nEstimate > m_nMaxSwap) 这句还会造成后面对m_ReverseCakeArraySwap数组的越界访问,使程序不能正常运行。

 

书上程序的低效主要是由于进行剪枝判断时,没有考虑好边界条件,可进行如下修改:

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

判断下界时,如果最大的烙饼不在最后一个位置,则要多翻转一次,因而在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的判断就可以取大等于号。

采用书上提到的确定“上限值”的方法,直接构建一个初始解,取其翻转次数为m_nMaxSwap的初始值。

 

12任改一处,都能使搜索次数从172126降到两万多,两处都改,搜索次数降到3475。若再改动第3处,搜索次数降到2989;若采用4的方法(此时初始值为10),搜索次数可降到1045

 

 

转载于:https://www.cnblogs.com/flyinghearts/archive/2010/09/10/1823699.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值