Sicily 1151魔板解题报告
- 原题大意;
- 算法思想及解题用到的主要数据结构
- 详细解题思路
- 逐步求精算法描述(含过程及变量说明)
- 程序注释清单(重要过程的说明)
- 测试数据(5-10组有梯度的测试数据,要考虑边界条件)
- 复杂度方面的分析、估算及程序优化的分析 和改进。
- 1515 魔板C 的代码
一、 原题大意
魔板由2*4个方块组成,用1到8的数字表示不同的块。 其初始状态是
1 2 3 48 7 6 5
对魔板可进行三种基本操作,这三种基本操作,可将任一种状态装换成另一种状态。
A操作(上下行互换):8 7 6 51 2 3 4B操作(行循环右移一格):4 1 2 35 8 7 6C操作(中间四小块顺时针转一格):1 7 2 48 6 3 5
Input: 输入步数上限和目标魔板状态,-1表示结束。
Output:在给定步数内,给出从初始状态到目标状态的步骤及步骤数。 若不能到达则输出-1.
二、 算法思想及主要数据结构
1、 解题思想 广搜 + 康托展开
2、 主要数据结构 队列 + 用int表示魔板状态。
三、 详细解题思路 BFS + 康托展开
- BFS
I. 将初始状态放入队列,
II.从队列中拿出队首元素,和目标状态比较,如果相等停止BFS。
III.对队首状态分别进行A、B、C 三种操作,只把得到的新状态放入队列。若该状态曾经到达过,则忽略。
IV. 只要队列不为空,重复II III操作。
- 康托展开
康托展开是全排列和整数的双射, 即给定一个长为n的排列,康托展开则得到该排列在全排列中的序号(0 ~ n!-1)。
例如 (2 1 3) 展开得 2, (1 2 3)展开得 0, (3 2 1)展开得5。
- 状态的存储和A、B、C操作的实现。
一个魔板 8 个数,每个数都小于16,只需4bit就能表示,所以一个int就能表示一个魔板。
因为是4bit,恰好可以直接使用 16进制数 做位操作的掩码。
初始状态 1 2 3 4 8 7 6 5 ----> 0x12348765
A操作: 上下行互换,就是高4位和低4位互换。 结果 0x87651234.
B操作: 循环右移, 就是第4,8位左移3位,其他位右移1位。结果 0x41235876
C操作: 中间顺时针转90度。 对比操作前后的结果 就知道哪些位要移动多少。
0x12348765
0x17248635
可以看出:第2位右移1位; 第3位右移4位, 第6位左移4位, 第7位左移1位。
四、 伪代码算法描述
由题目可知,每次BFS的初始状态,操作都没变。所以其实搜索树的内容是完全一样的。因此只需要 遍历魔板的 8! 种状态都一次。之后就在搜索队列中查找即可。//遍历 所有状态 initialize( queue, isVisited[] ); while( queue.notEmpty() ){for( op = A, B, C ){next_state = op(); order = cantor( next_state ); //计算康托展开if( isVisited(order) == FALSE ){ //是未访问的节点就放入队列。push();isVisited(order) = TRUE;}} } target = input(); index = search( target, queue ); //在队列中查找 目标状态 if( found >=0 && 步数小于限制 ) //found<0 说明没找到。print( queue[found] ) else print(-1);
五、 程序注释清单
- #include <cstdio>
- #include <string>
- using namespace std;
- struct node{
- unsigned int board;
- string ops;
- void show(){ //调试用的函数
- printf( "|%x %s\n", board>>16, ops.c_str() );
- printf( " %x \n", board& 0xffff );
- }
- };
- const int qsize=40322;
- struct node q[qsize];
- int fp=0, rp; //fp -- front pointer队头指针, rp -- rear pointer队尾指针。
- bool isVisited[qsize]={ false };
- unsigned int target = 0;
- unsigned opa( unsigned brd );
- unsigned opb( unsigned brd );
- unsigned opc( unsigned brd );
- unsigned (*funcp[3])( unsigned ) = { opa, opb, opc }; //函数指针,以便用循环调用 opa opb opc
- int cantor( unsigned brd ); //计算康托展开
- int isNew( unsigned brd ); //判断是否为新状态,是的话就标记为已访问。
- void print( node& x);
- int main(){
- int n, i,j, temp;
- unsigned t;
- /* 下面搜索 8! 个状态 */
- q->board = 0x12348765; //初始状态
- isVisited[ cantor( 0x12348765 ) ]=true;
- fp = 0, rp = 1;
- while( fp!=rp ){ //因为队列不可能满,所以 fp!=rp判断队空是可行的。
- for( i=0; i<3; ++i ){
- if( isNew( t = funcp[i](q[fp].board) ) ) { //用3种操作拓展节点。
- q[rp].board = t;
- q[rp].ops = q[fp].ops+ char('A'+i) ;
- ++rp;
- }
- }
- fp++;
- }
- while( scanf("%d", &n) && n!=-1 ){
- target = 0;
- for( i=0; i<8; ++i){ //根据输入的目标魔板状态 计算出 相应的 16 进制数
- scanf("%d", &temp);
- target <<= 4;
- target |= temp;
- }
- for( i=rp-1; i>=0; --i ) // 在队列中查找目标状态
- if( q[i].board == target ) break;
- if( i>=0 && q[i].ops.length()<=n ) print( q[i] );
- else printf("-1\n");
- }
- return 0;
- }
- /* 因为是16进制数,掩码的计算极度方便。 */
- unsigned opa( unsigned brd ){
- return brd<<16 | brd>>16;
- }
- unsigned opb( unsigned brd ){
- unsigned a=(brd & 0x000f000f)<<12,
- b=(brd & 0xfff0fff0)>>4;
- return a | b;
- }
- unsigned opc( unsigned brd ){
- unsigned a= (brd & 0x0f000000)>>4,
- b= (brd & 0x00f00000)>>16,
- c= (brd & 0x00000f00)<<16,
- d= (brd & 0x000000f0)<<4;
- brd &= 0xf00ff00f;
- brd |= a | b | c | d;
- return brd;
- }
-
- int isNew( unsigned brd ){
- int hash = cantor(brd);
- if( isVisited[hash] ) return 0;
- isVisited[hash] = true;
- return 1;
- }
- int cantor( unsigned brd ){
- int i,j,t;
- unsigned num=0, fac[8] = { 0,1, 2, 6, 24, 120,720, 5040 }, s[8];
- for( i=0; i<8; ++i ) s[i] = ( brd >>(4*i)) &0xf;
- /* 统计 排列的第x位后面有几个数比他小,假设有a个,则有 a*(x-1)! 种排列比给定排列小,
- 要注意数组下标
- */
- for( i=0; i<8; ++i) {
- t=0;
- for( j=i+1; j<8; ++j)
- if(s[j] < s[i]) ++t;
- num += fac[7-i]*t;
- }
- return num;
- }
- void print( node& x){
- int i, len = x.ops.size();
- printf("%d ", len );
- printf("%s\n", x.ops.c_str() );
- }
六、 测试数据
4
4 1 2 3
5 8 7 6
B
4
5 7 2 6
4 8 1 3
BCA
3
8 7 6 5
1 2 3 4
A
4
5 8 7 6
4 1 2 3
AB
3
5 1 8 6
4 2 7 3
ABC
5
5 8 3 2
4 1 6 7
accb
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
七、 复杂度分析及优化改进
因为状态数目40320,所以空间复杂度就是常数。
但是节点的体积是可以再减小的,节点无需存储所有操作,只需存储当前操作(1B),还有父节点下标(2B).
每个节点只需 7个字节,由于存在字节对齐,所以每个节点会占用8字节。
总内存: 8*40320 + 312KB(运行时系统) = 634KB
采用这种方法,出乎意料的是 运行时间也减小了。
原来 0.02 sec 2376 KB
现在 0.01 sec 640 KB 内存使用量和估计值十分接近 。
整个搜索树是3叉的, 对于第i层,要搜索3^i个节点(i从0开始)
通过判重来剪枝,可以显著减少搜索队列的长度。只留下 40320 个节点。
八、1515 魔板C 的代码
魔板c和 1151没有本质不同,仅仅是opa opb opc 有改变。三种操作修改如下。
- unsigned opa( unsigned brd ){
- unsigned a = (brd & 0xff00ff00)>>8,
- b = (brd & 0x00ff00ff)<<8;
- return a|b;
- }
- unsigned opb( unsigned brd ){
- unsigned a=(brd & 0xf000f000) >> 12,
- b=(brd & 0x0fff0fff) << 4;
- return a|b;
- }
- unsigned opc( unsigned brd ){
- unsigned a= (brd & 0x0f000000)>>16,
- b= (brd & 0x00f00000)<<4,
- c= (brd & 0x00000f00)>>4,
- d= (brd & 0x000000f0)<<16;
- return (brd &0xf00ff00f) | a | b | c | d ;
- }