首页 > 代码库 > Sicily 1151 解题报告(魔板,广搜)

Sicily 1151 解题报告(魔板,广搜)

 

    I.     原题中文大意

魔板由2*4个方块组成,用1到8的数字表示不同的块。 其初始状态是

               1 2 3 4

               8 7 6 5

对魔板可进行三种基本操作,这三种基本操作,可将任一种状态装换成另一种状态。

 

A (上下行互换)

B (行循环右移一格)

C (中间四块顺时针转90)

8 7 6 5

1 2 3 4

4 1 2 3

5 8 7 6

1 7 2 4

8 6 3 5

 

 II.       算法思想及解题用到的主要数据结构

  1. 广度优先搜索,已搜索过的节点不再进行记录,保证第一个找到的解即为最优解。
  2. HASH表,采用康拓展开表示序号数,共8!种可能性,无碰撞问题。

 

 III.       详细解题思路

  • BFS实现
    1. 将初始状态放入队列。
    2. 从队列中拿出队首元素,和目标状态比较,如果相等停止BFS。
    3. 若不等,对队首状态分别进行ABC三种操作,只把得到的新状态放入队列。若该状态已到达过,则忽略。
    4. 只要队列不为空,重复II III操作。

 

  • 康托展开?

康托展开是全排列和整数的双射, 即给定一个长为n的排列,康托展开得到该排列在全排列中的序号(0 ~ n!-1)。?例如 (2 1 3) 展开得 2, (1 2 3)展开得 0, (3 2 1)展开得5。 ?因此它能对n个数的排列进行状态的压缩和存储。例如要对8的全排列进行判重,没有必要开一个87654321?这么大的数组。对于一个n的排列数,通过康托展开得到这个排列在全部的排列中排第几位。这个序号最大?也就 8!=40320。根据序号就能通过hash快速确定 该状态是否访问过。

 

  • 以16进制掩码进行状态的存储及A、B、C操作

   一个魔板 8 个数,每个数都小于15,只需4bit就能表示,所以一个int 32位就能表示一个魔板。

   因为是4bit,恰好可以直接使用 16进制数 做位操作的掩码。

   初始状态对应的16进制数:                          12348765     

                                                            0x12348765?

   A操作: 上下行互换,就是高4位和低4位互换。 结果 0x87651234.

   B操作: 循环右移, 就是第4,8位左移3位,其他位右移1位。结果 0x41235876

   C操作: 中间顺时针转90度。 对比操作前后的结果 就知道哪些位要移动多少。        

                         0x12348765

                         0x17248635

   可以看出:第2位右移1位; 第3位右移4位, 第6位左移4位, 第7位左移1位。

 

 IV.       逐步求精算法描述(含过程及变量说明)

  • 变量:
/** * 魔板状态节点 * @board 当前魔板状态, @pre 到达此状态的前一状态在队列中的位置, * @op 到达此状态的操作 */struct node{    unsigned int board;    unsigned short pre;    char op;};/** * @fp 队头下标, @rp 队尾下标, * @q 广搜得到的所有序列, @isVisited 标记某序列是否访问过, * @QSIZE 队列大小,由前知为40320 */int fp = 0, rp = 1;struct node q[QSIZE];bool isVisited[QSIZE] = { false };

 

  • 方法
/** initialize 遍历所有8!个状态 opa, opb, opc 三种操作,用函数指针循环调用 cantor 计算康托展开 isNew 判断是否是新的状态 print 输出所求字符串 */void initialize();unsigned opa( unsigned brd );unsigned opb( unsigned brd );unsigned opc( unsigned brd );unsigned (*funcp[3])( unsigned ) = {opa, opb, opc};int cantor( unsigned  brd );int isNew( unsigned brd );void print( node& x, int limit );

 

V.       程序注释清单(重要过程的说明)

  1 #include <cstdio>  2 #include <string>  3 #define QSIZE 40320  4 using namespace std;  5   6 struct node{  7     unsigned int board; //  魔板序列  8     unsigned short pre; //  前一个状态在q中的下标  9     char op; //  操作 10     /* 11     void show() {  // 用于调试 12         printf( "|%x   %c\n", board>>16, op ); 13         printf( " %x \n", board& 0xffff ); 14     } 15     */ 16 }; 17  18 /** 19  * @fp 队头下标, @rp 队尾下标, 20  * @q 广搜得到的所有序列, @isVisited 标记某序列是否访问过, 21  */ 22 int fp = 0, rp = 1; 23 struct node q[QSIZE]; 24 bool isVisited[QSIZE] = { false }; 25  26 /** 27  initialize 遍历所有8!个状态 28  opa, opb, opc 三种操作 29  cantor 计算康托展开 30  isNew 判断是否是新的状态 31  print 输出 32  */ 33 void initialize(); 34 unsigned opa( unsigned brd ); 35 unsigned opb( unsigned brd ); 36 unsigned opc( unsigned brd ); 37 unsigned (*funcp[3])( unsigned ) = {opa, opb, opc}; //  函数指针,便于循环调用abc三种操作 38 int cantor( unsigned  brd ); 39 int isNew( unsigned brd ); 40 void print( node& x, int limit ); 41  42  43 int main(){ 44     int n, i, temp; 45     unsigned int target = 0; 46      47     initialize(); 48      49     //  输入状态进行查找 50     while(scanf("%d", &n) && (n != -1)){ 51         target = 0; 52         for(i = 0; i < 8; ++i) { //  计算新状态对应16进制数 53             scanf("%d", &temp); 54             target <<= 4; 55             target |= temp; 56         } 57          58         for(i = 0; i < QSIZE; i++) //  在队列中查找 59             if(q[i].board == target) break; 60          61         print( q[i], n ); 62     } 63      64     return 0; 65 } 66  67 void initialize() { 68     q->board = 0x12348765; 69     q->op = 0; 70     q->pre = 65535; 71     isVisited[cantor(0x12348765)]=true; 72     fp = 0, rp = 1; 73      74     unsigned t; 75     while(fp != rp){  //  队列不可能满,所以 fp!=rp是可行的 76         //  对fp进行abc三种操作拓展,存入rp 77         for(int i = 0; i < 3; ++i){ 78             if(isNew(t = funcp[i](q[fp].board))) { 79                 // q[rp-1].show(); 80                 q[rp].board = t; 81                 q[rp].op = A+i ; 82                 q[rp].pre = fp; 83                 ++rp; 84             } 85         } 86         fp++; 87     } 88     //  printf("----------%d\n", rp ); 89     //  end 90 } 91  92 //  采用掩码进行ABC操作 93 unsigned opa( unsigned brd ){ 94     return (brd << 16) | (brd >> 16); 95 } 96 unsigned opb( unsigned brd ){ 97     unsigned a = (brd & 0x000f000f) << 12, b = (brd & 0xfff0fff0) >> 4; 98     return a | b; 99 }100 101 unsigned opc( unsigned brd ){102     unsigned a = (brd & 0x0f000000) >> 4,103              b = (brd & 0x00f00000) >> 16,104              c = (brd & 0x00000f00) << 16,105              d = (brd & 0x000000f0) << 4;106     107     brd &= 0xf00ff00f;108     brd |= a | b | c | d;109     return brd;    110 }111 112 int isNew( unsigned brd ){113     int hash = cantor(brd);114     if(isVisited[hash]) return 0;115     116     isVisited[hash] = true;117     return 1;118 }119 120 int cantor( unsigned brd ){121     int i, j, t;122     unsigned num=0, fac[8] = {0, 1, 2, 6, 24, 120, 720, 5040}, s[8];123     124     for(i = 0; i < 8; ++i ) s[i] = ( brd >> (4*i)) & 0xf;125     num = (s[0] - 1) * 5040;126     for(i = 1; i < 7; ++i) {127         t = 0;128         for (j = i + 1; j < 8; ++j)129             if(s[j] < s[i])  ++t;130         num += fac[7-i] * t;131     }132     return num;133 }134 135 void print(node& x, int limit) {136     int i, len = 0, str[23] = {x.op};137     for(i = x.pre; i != 65535; i = q[i].pre){138         str[++len] = q[i].op;139     }140     141     if(len > limit)142         printf("-1");143     else {144         printf("%d ", len);145         for(i = len - 1; i >= 0; i--)146             printf("%c", str[i] );147     }148     printf("\n");149 }

 

VI.       测试数据(5-10组有梯度的测试数据,要考虑边界条件)

测试数据及答案:

 15 5 8 2 3 4 1 6 7 -1  25 5 8 2 3 4 1 6 7 20  BBCBBCABCBCBBBCBCBCB  20 3 7 2 1 6 5 8 4 8 CCBBCABC  20 4 6 5 8 1 3 7 2 10 CCBBCABCAB  30 4 5 2 7   3 8 1 6 22 ACBBBCBCBCBCBCABCABCBC

  

VII.     对时间复杂度,空间复杂度方面的分析、估算及程序优化的分析和改进

  • 空间消耗:

因为状态数目40320,所以空间复杂度就是常数。

但是节点的体积是可以再减小的,节点无需存储所有操作,只需存储当前操作(1B),还有父节点下标(2B).

每个节点只需 7个字节,由于存在字节对齐,所以每个节点会占用8字节。

总内存: 8*40320 + 312KB(运行时系统) = 634KB

采用这种方法,出乎意料的是 运行时间也减小了(0.01sec)。

整个搜索树是3叉的,对于第i层,要搜索3^i个节点(i从0开始)

通过判重来剪枝,可以显著减少搜索队列的长度。最后只留下 40320 个节点。 

  • 时间消耗:

采用位移的方法代替数组内部负值(如老师课堂所讲),可以减少时间开支;

状态节点使用一个char记录当前操作,一个int记录前一操作下表,代替用string记录到当前状态的所有操作以减小时空开支。

  •  可能的优化方案:

640KB的空间消耗可以接受,考虑减小时间消耗。

最直观的考虑可以优化哈希函数cantor()。可以使用非康托展开的其他哈希函数来减少操作量,但考虑到可能有碰撞问题,有可能需要开一个较大的数组,加大空间消耗,但参考其他同学的成果,使用1000kb左右内存时,时间消耗可以降至0.00sec。 

Sicily 1151 解题报告(魔板,广搜)