题目描述
黑白棋游戏的棋盘由4×4方格阵列构成。棋盘的每一方格中放有1枚棋 子,共有8枚白棋子和8枚黑棋子。这16枚棋子的每一种放置方案都构成一个游戏状态。在棋盘上拥有1条公共边的2个方格称为相邻方格。一个方格最多可有4 个相邻方格。在玩黑白棋游戏时,每一步可将任何2个相邻方格中棋子互换位置。对于给定的初始游戏状态和目标游戏状态,计算从初始游戏状态变化到目标游戏状态的最短走棋序列。
算法分析
状态的产生:每个位置可以跟上、下、左、右四个相邻位置进行交换,产生新的状态,但要考虑边界问题。
易知:
水平方向的对换共有:3×4种,
垂直方向的对换共有:3×4种,
每个节点能够产生的新节点(状态)数最多为24种
容易想到的是我们要用BFS也就是广(宽)度优先搜索(点击查看模板)来进行模拟,但要解决一些问题:
数据如何存储?
节点如何判重?
关于棋盘状态的存储,我们考虑将每个棋盘保存为一个二维数组,在判定新节点的时候需要将新产生的棋盘与前面已有的棋盘逐个比对,但空间和时间消耗都比较大。因为棋盘本身并不复杂,每个格子都只有0和1两种状态值,于是可以考虑使用二进制位压缩状态,将棋盘用二进制位存储。
这样首先空间复杂度缩小32倍;状态判重也不用将数组各个元素进行对比,直接比较二进制数即可。
那么我们首先来学习一下位运算的基础知识:
认识位运算–基本运算符
运算 | c++符号 | 解释 |
---|---|---|
左移位 shl | << | 向左移位,低位补零,相当于乘二。如1<<2 = 4 =(100) ₂ |
右移位 shr | >> | 向右移位,高位补零,相当于除与二。如8>>2 = 4 = (100) ₂ |
按位与 and | & | 按二进制位与。如10&3 = (1010) ₂ &(0011) ₂ = (0010) ₂ = 2 |
按位或 or | 丨 | 按二进制位或。如10丨3=(1010) ₂ 丨(0011) ₂ =(1011) ₂ =11 |
按位取反not | ~ | 按二进制位取反。如~10= ~ (1010)2 = (0101)2 |
异或 xor | ^ | 按位加,不进位(相同返回1,不相同返回0)。 如10 ^ 3=(1010)2 ^ (0011)2=(1001)2 |
(其实还有同或)
那么,棋盘的状态存储可以转化为二进制,用一个16位的二进制数表示。因为每个位都要用到,对于C++而言可以设置无符号短整数类型(unsigned short int),刚好16位二进制,记录一个棋盘状态。
找到棋子状态
设坐标(x,y)坐标轴见下图 ,A为16位二进制表示的棋盘状态
取棋子状态则可以用位运算表达式:A &(1<<(x*4+y)) 。如下图:
对于棋盘而言,每次节点产生只需要枚举4×4棋盘每一个位置,如果这个位置是1,就可以进行四个方向的交换。对应位存储的状态,实际上就是枚举16位然后判断,考虑是否交换。
代码奉上
k=qa[f]; //当前棋盘状态
for(int i=0;i<16;i++) //枚举16个二进制位
if((k&(1<<i))>0)
{
if((i/4>0)&&((k&(1<<i-4))==0)) push(k^((1<<i)+(1<<i-4))); //向下交换
if((i/4<3)&&((k&(1<<i+4))==0)) push(k^((1<<i)+(1<<i+4))); //向上交换
if((i%4>0)&&((k&(1<<i-1))==0)) push(k^(3<<i-1)); //向左交换
if((i%4<3)&&((k&(1<<i+1))==0)) push(k^(3<<i)); //向右交换
}//if语句中考虑了边界和重复问题,是位运算,需要细细体会
那么还有什么可优化的点呢?
小小优化
标记数组(标记状态是否出现过)的基础类型是bool,每个单元要用一个字节存储,而里面只存储了0和1,实际上浪费了很多空间,有没有办法可以进一步优化呢?用二进制位表示标记。
//进队列的参考代码如下:
void push(unsigned short int x) //对x进行入队处理
{
if((visit[x/32]&(1<<(x%32)))==0) //用位运算判重
{
qa[t]=x;
qb[t]=qb[f]+1;
t++;
visit[x/32]=visit[x/32]|(1<<x%32); //标记x为已出现过的状态
}
}