posts - 274,  comments - 1258,  trackbacks - 0

题目链接:
http://202.116.77.69/sicily/show_problem.php?pid=1151
(非中大的读者请下载附件

题目分析:
  这是一道典型的搜索题,虽然题目没有明确指出求最短指令序列,但关于步数的严格限定使得广搜(BFS)成为了我们的最佳选择。对于广搜,我们首先要解决以下三个问题:

1、队列API
  广搜的一般操作流程为:
 1)将初始状态压入队列;
 2)测试队列是否非空,若空则成功退出
 3)取出队列首元素;
 4)根据游戏规则枚举所有可能由此状态得到的新状态;
 5)逐个检查各新状态是否进入过队列,若是,跳过;若否,标记为进入过,并压之入队列尾;
 6)跳转到步骤2;
  由此可见它需要用到队列的压元素、取元素、测试非空操作。队列的实现既可借助于STL list,也可通过自写循环队列实现。笔者基于效率考虑,选择了后者。其实,前者也不过0.05s而已,用时多一倍都不到,也是相当高效的。


2、状态压缩储存
  题目的输入给出了8个数作为一个状态,但如果我们直接用之进行储存和操作,既浪费时间,也浪费空间,实为不智。所以要把它进行压缩,对于本题,压缩方法有两种:
  1)用一个8位数来表示,即用12348765来表示状态“1 2 3 4 8 7 6 5”。优点是简明易懂,缺点是比较低效(要通过乘、除、模来取状态位,学过组成原理的同学们都清楚这几个操作是比较费时的);还有是冗余状态多。本题的有用状态其实只有8!,即约4万个,但采用该法压缩时,状态共有约8765万个。
  2)考虑到一个状态的每一个位只有1~8这8种选择,所以可以使用3个二进制位进行保存(还记得数字电路的相关内容吗?)即用000表示1,001表示2,010表示3等,如此类推,则“1 2 3 4 8 7 6 5”用二进制数“100,101,110,111,011,010,001,000”(注意是低字在低位,所以1在最后),(补充说明:其实也就相当于八进制数45673210,发现它与原状态的关系了吗?)即十进制的9926280表示。优缺点刚好与前相反。该法的状态共有2^(3*8)=2^24个,即约1670万个(16M),而且比较省时(位操作一般只要一个CPU周期),对于熟练者来说,代码也比较简短。
  笔者基于时间和空间的考虑选择了后者,具体方法如下:
  设我用一个int s来保存状态,并把数组a[i]保存到s里边去,可以这样写:s|=(a[i]-1)<<3*i; 其中<<是左移运算符,目的是把a[i]-1的值移动到适当的位中去,然后再通过|=运算写到s中去。举个例子,s原来的值是6,写成二进制是……0000110

3、状态查询
  回顾广搜操作的第5步,它要求我们能查询一个状态是否进入过队列(在本文中,简称为该状态“是否被扩展过”),由于每生成一个状态都要查询一次,即等于3*8!,约12万次,它是那么的频繁,使得我们必须在O(1)常数时间内返回查询结果,这令我们想起了hash表。
  对于第一种状态压缩方法,老老实实地写一个hash表是必需的,因为没有足够的空间来保存这么多状态(题目内存限制为32MB,而这种压缩方法的内存需求高达80MB)具体选择可以使用拉链法,既高效,也不易错。
  而对于第二种状态压缩方法,问题就简单得多了,因为总共只需要16M空间,开一个字符数组char hash[1<<24]足矣!这样不但高效(可能10倍左右吧)而且可以减少达30行的代码!那这个字符数组用来保存什么呢?用来保存生成该状态所用的操作。比如说状态“8 7 6 5 1 2 3 4”(压缩为6850935)是用A操作得到的,则令hash[6850935]='A';//这样的“hash表”是不是很简洁?

  解决了对于广搜必需解决的问题后,我们再说说一个重要的优化:通常做搜索题时,我们的程序是读入一组数据,就进行一次广搜,但我们注意到,每一次开始搜索时的初始状态都是相同的“1 2 3 4 8 7 6 5”,进而得出结论:每一次搜索后hash表的内容都是相同的!(假如你没有自作聪明做了一些很特别的剪枝的话),这样我们自然会想到:只搜索一次,以后每读入一组数据,就在hash表里查询一下然后输出就可以了~~这一个优化,使总复杂度由O( TestCase * 8! ) 变为 O( 8! + TestCase ),其效率自然成倍地提高。

  那怎么通过hash表来查询所求状态的操作序列呢?我们观察一下A、B、C三个操作就会发现,它们分别是二循环、四循环、四循环的操作,也就是说对于任意的i,总有A(A(i))=B(B(B(B(i))))=C(C(C(C(i))))=i,于是,若令A(i)=x, B(i)=y, C(i)=z,A(x)=B(B(B(y)))=C(C(C(z)))=i,于是,我们就找到x、y、z的父亲i了!

  讲到这里,我们已经把各个关键环节全部打通,然后我们的算法自然就呼之欲出了:
  1)把初始状态1 2 3 4 8 7 6 5”先压缩,再压入队列,开始广搜,结果存入hash表。
  2)对于每组数据,先压缩,再查询hash表中记录的生成它所使用的操作。
  3)不断地回溯到其父状态,直到当前状态等于初始状态为止,记录操作序列和所用步数
  4)输出结果
参考代码:

#include < cstdio > // 用于scanf()、putchar()、printf()等I/O函数
char  hash[ 1 << 24 ]; // hash[i]=得到状态i所用的操作
inline  int  A( int  n) { return  (n & 4095 ) << 12 | n >> 12 ;} // 4095=2^12-1
inline  int  B( int  n) { return  (( 7 << 9 | 7 << 21 ) & n) >> 9   |  ( ~ ( 7 << 9 | 7 << 21 ) & n) << 3 ;}
inline 
int  C( int  n) { return  ( 7 | 7 << 9 | 7 << 12 | 7 << 21 ) & |  (( 7 << 3 ) & n) << 3   |  (( 7 << 6 ) & n) << 12   |  (( 7 << 18 ) & n) >> 3   |  (( 7 << 15 ) & n) >> 12 ;}
inline 
int  zip( int  a[]) { // 用于将8个数压缩为1个小于2^24(约1670万)的整数
    int  s = 0 ;
   
for ( int  i = 0 ;i < 8 ; ++ i) s |= (a[i] - 1 ) << ( 3 * i);
   
return  s;
}

int  a[] = { 1 , 2 , 3 , 4 , 8 , 7 , 6 , 5 } ; // 初始状态
const   int  QLen = 10000 ; // 队列长度
int  q[QLen],b = 0 ,e = 0 ; // 循环队列及首、尾指针
inline  void  inc( int &  p)if(++ p ==QLen) p=0;} //用于移动队列首尾指针
int main(){//因为用记事本写的,排版不是很好,请见谅
   int i,j,n,bgn=zip(a);//bgn是初始状态(压缩后)
    hash[bgn]='E';
      q[b]
=bgn; inc(b);
      
while(b!=e){
         i
=q[e]; inc(e);//取队首
         j=A(i);//生成新状态,未访问过则压入hash表和队列
         if(!hash[j]) hash[j]='A', q[b]=j, inc(b);
         j
=B(i);
         
if(!hash[j]) hash[j]='B', q[b]=j, inc(b);
         j
=C(i);
         
if(!hash[j]) hash[j]='C', q[b]=j, inc(b);
      }

      
char s[100];//s[]用于保存生成的操作序列
      while(scanf("%d",&n),n!=-1){
         
for(i=0; i<8++i) scanf("%d",&a[i]);
         
for(i=zip(a),j=0; i!=bgn; ++j){
            s[j]
=hash[i];
            
switch(s[j]){//逐步回溯到前一状态
               case 'A': i=A(i);        break;
               
case 'B': i=B(B(B(i)));    break;
               
case 'C': i=C(C(C(i)));    break;
            }

         }

         
if(j>n) printf("-1\n");
         
else{
            printf(
"%d ",j);
            
while(j--) putchar(s[j]);
            putchar(
'\n');
         }

      }

      
return 0;
}

写在最后:
    这是我第一次写解题报告,用了整整一个上午的时间,希望大家踊跃支持一下。如果你觉得有所获益,请转告你的朋友,不足之处,请不吝赐教,不明之处,欢迎提问

注:本程序有部分灵感来自辽哥牛牛的同题程序,在此特别鸣谢!

posted on 2006-09-25 12:21 踏雪赤兔 阅读(2041) 评论(11)  编辑 收藏 引用 所属分类: 玩转编程

FeedBack:
# re: 解题报告:手把手教你如何用0.03秒和50行代码水掉大小魔板[原] by 赤兔
2006-09-25 12:38 | 小bug
友情支持~  回复  更多评论
  
# re: 解题报告:手把手教你如何用0.03秒和50行代码水掉大小魔板[原] by 赤兔
2006-09-25 17:23 | ling
居然是c,还得改啊,好奇怪的语句case 'C': i=C(C(C(i)));  回复  更多评论
  
# re: 解题报告:手把手教你如何用0.03秒和50行代码水掉大小魔板[原] by 赤兔
2006-09-25 18:45 | 踏雪赤兔
这个程序是用C++语言编写的,用C编译器不能通过。至于case 'C': i=C(C(C(i))); 是进行回溯操作,每执行一次,i回溯到其父状态,这在“题目分析”的倒数第2段有详细说明  回复  更多评论
  
# re: 解题报告:手把手教你如何用0.03秒和50行代码水掉大小魔板[原] by 赤兔
2006-09-25 23:09 | zhouditty
支持! 有无pku对应题目啊?系中大自己出既题来嘎?  回复  更多评论
  
# re: 解题报告:手把手教你如何用0.03秒和50行代码水掉大小魔板[原] by 赤兔
2006-09-25 23:37 | 踏雪赤兔
系中大自己出嘅题~仲要系佢哋算法课嘅第一个作业!!唔知道郭老有咩谂法,咁样实吓走好多本来可以中意算法嘅人啦~~  回复  更多评论
  
# re: 解题报告:手把手教你如何用0.03秒和50行代码水掉大小魔板[原] by 赤兔
2006-09-28 09:27 | LZW
看了你的blog了,真的很棒,大开眼界,原来编程可以这样玩的,比如说你那个用3位二进制来保存状态,连数字电路的理论都搬出来了,哈哈, ,还有那个专门对付赛题的方法,第二次开始直接查询hash表,真的有点脑筋急转弯的感觉!原来做个hash表还有这样的作用。
  回复  更多评论
  
# re: 解题报告:手把手教你如何用0.03秒和50行代码水掉大小魔板[原] by 赤兔
2006-11-09 13:33 | [Optimistic]
[quote]队列的实现既可借助于STL list,也可通过自写循环队列实现。笔者基于效率考虑,选择了后者。[/quote]

不试试 DEQUE么?可能时间要好些   回复  更多评论
  
# re: 解题报告:手把手教你如何用0.03秒和50行代码水掉大小魔板[原] by 赤兔
2006-11-09 14:07 | 踏雪赤兔
deque效率上可能好过list,但比不上自写的循环队列~  回复  更多评论
  
# re: 解题报告:手把手教你如何用0.03秒和50行代码水掉大小魔板[原] by 赤兔
2006-11-09 17:49 | [Optimistic]
问个问题:
1。Qlen=10000是怎么估计出来的?状态如此之大 不怕满么?
2。我觉得a[]放到s[]中去的时候 把第一个放到最低位还是最高位都一样吧 而且也可以不用每一位减1的 不知道我理解的对不对?  回复  更多评论
  
# re: 解题报告:手把手教你如何用0.03秒和50行代码水掉大小魔板[原] by 赤兔
2006-11-09 18:29 | [Optimistic]
哦。。。要3位2进制的话 确实要-1的呵呵 没有0题目里面。。  回复  更多评论
  
# re: 解题报告:手把手教你如何用0.03秒和50行代码水掉大小魔板[原] by 赤兔
2006-11-09 23:36 | 踏雪赤兔
呵呵~~既然第2个你看明了,我就不用说啦,至于第1个问题嘛,可以算得有效状态为8!=40320个,所以保险起见应该开足4万个的~~这个10000嘛~~完全是随手拈来的~~嘻嘻~~  回复  更多评论
  
只有注册用户登录后才能发表评论。

百度空间| 见闻日记| 编程感悟
我的twitter


LOGO

自我介绍:百度厂基础平台车间的一名挨踢民工。擅长C++、算法、语言设计、分布式计算,也用过Java,Python, PHP,JS/AS等语言开发。请关注我的twitter (免翻墙版) 发QQ消息


添加到收藏夹 Locations of visitors to this page

常用链接

随笔分类(300)

随笔档案(274)

文章分类(38)

相册

收藏夹(54)

与博主互动

博客手拉手

搜索

  •  

积分与排名

  • 积分 - 400205
  • 排名 - 10

最新评论

阅读排行榜

评论排行榜