题目大意:有一个4*4的方格,每个方格中放一粒棋子,这个棋子一面是白色,一面是黑色。游戏规则为每次任选16颗中的一颗,把选中的这颗以及它四周的棋子一并反过来,当所有的棋子都是同一个颜色朝上时,游戏就完成了。现在给定一个初始状态,要求输出能够完成游戏所需翻转的最小次数,如果初始状态已经达到要求输出0。如果不可能完成游戏,输出Impossible。
主要思想:
1.如果用一个4*4的数组存储每一种状态,不但存储空间很大,而且在穷举状态时也不方便记录。因为每一颗棋子都只有两种状态,所以可以用二进制0和1表示每一个棋子的状态,则棋盘的状态就可以用一个16位的整数唯一标识。而翻转的操作也可以通过通过位操作来完成。显然当棋盘状态id为0(全白)或65535(全黑)时,游戏结束。
2.对于棋盘的每一个状态,都有十六种操作,首先要判断这十六种操作之后是否有完成的情况,如果没有,则再对这十六种操作的结果分别再进行上述操作,显然这里就要用到队列来存储了。而且在翻转的过程中有可能会回到之前的某种状态,而这种重复的状态是不应该再次入队的,所以维护isVis[i]数组来判断id==i的状态之前是否已经出现过,如果不是才将其入队。如果游戏无法完成,状态必定会形成循环,由于重复状态不会再次入队,所以最后的队列一定会是空队列。
3.由于0^1=1,1^1=0,所以翻转的操作可以通过异或操作来完成,而翻转的位置可以通过移位来确定。
# include <iostream>
# define N 65536
using namespace std;
int q[N*2];
int front, rear, i, j, id = 0, tmp;
bool vis[N]; //标记状态是否出现过
int step[N]; //记录到达id==i时状态翻转所需次数
char color;
int main(void)
{
for(i = 0; i < 4; i++)
for(j = 0; j < 4; j++)
{
cin >> color; //输入初始状态并转换为id
id <<= 1;
if(color == 'b')
id++;
}
if(id == 0 || id == 65535)
{
cout << 0 << endl; //如果初始状态满足直接输出
return 0;
}
q[rear++] = id; //初始状态id入队
vis[id] = true;
step[id] = 0; //到达初始状态所需次数置0
while(front < rear) //队列不为空继续操作
{
id = q[front++]; //队头元素出队
for(i = 0; i < 4; i++)
for(j = 0; j < 4; j++)
{
tmp = id; //需要遍历队头元素的16种操作 每次还原tmp
if(i == 0)
tmp ^= 1 << (11-4*i-j); //翻转的位置为15-(4*(i+1)+j)
else if(i == 3)
tmp ^= 1 << (19-4*i-j); //翻转的位置为15-(4*(i-1)+j)
else
{
tmp ^= 1 << (11-4*i-j);
tmp ^= 1 << (19-4*i-j);
}
if(j == 0)
tmp ^= 3 << (14-4*i-j); //翻转的位置为15-(4*i+j)、15-(4*i+j+1)
else if(j == 3)
tmp ^= 3 << (15-4*i-j); //翻转的位置为15-(4*i+j-1)、15-(4*i+j)
else
{
tmp ^= 7 << (14-4*i-j); //翻转的位置为15-(4*i+j-1)、15-(4*i+j)、15-(4*i+j+1)
}
if(tmp == 0 || tmp == 65535)
{
cout << step[id]+1 << endl;
return 0;
}
if(!vis[tmp]) //如果是新状态 入队
{
q[rear++] = tmp;
vis[tmp] = true;
step[tmp] = step[id]+1; // 当前次数=到达队头元素所需次数+1
}
}
}
cout << "Impossible" << endl;
return 0;
}
View Code
后记:
第一次做状态压缩的题,也是现学现卖,有些像位运算的公式什么的还是理解的不是很透彻,关于状态压缩的题还是需要再多做几个。