您的位置:首页 > 其它

POJ 1753 Flip Game(枚举+状态压缩BFS)

2017-03-30 23:13 429 查看

原题地址

http://poj.org/problem?id=1753

题意:十六宫格的棋盘,每个棋子有黑白两面,已知翻转某个棋子会造成其四周(若存在)的棋子都翻转。给定一个棋面,计算最少需要多少次能使整个棋盘的棋子全黑或全白。

解题思路

声明:本题的算法不是我自己的思路,学习了大神的方法,觉得是一种非常好的处理简单状态变迁的枚举问题,在这里做下笔记今后可以复习:

太长不看版

用16个二进制位来代表每个棋子(很奇妙,每个棋子刚好只有2种状态~可以视白色为0,黑色为1),那么这个棋盘的棋面就可以用16位的整数来唯一标识。

对状态的翻转用到了按位异或操作,利用了0^1=1和1^1=0这一规律。对16个位置中的每个位置k做翻转都形成一个唯一的翻转值,用这个翻转值来对当前状态做异或,就可以用位置k造成翻转的1来改变对应的位置。

广度搜索来枚举各种状态的可能情况

详细版

参照大神博客地址:http://blog.csdn.net/hackbuteer1/article/details/7392245


这道题明显是一道需要枚举的题目,棋盘所有可能的状态有2^16=65536种,因此在时间复杂度上一般不会造成问题。刚开始自己想的时候,很纠结怎么把这65536种矩阵存下来,要知道这不只是空间的问题,而且穷举时如何标记、索引到这个矩阵同样是个大问题。看看他的思考:

如果用一个4*4的数组存储每一种状态,不但存储空间很大,而且在穷举状态时也不方便记录。因为每一颗棋子都只有两种状态,所以可以用二进制0和1表示每一个棋子的状态,则棋盘的状态就可以用一个16位的整数唯一标识。而翻转的操作也可以通过通过位操作来完成。显然当棋盘状态id为0(全白)或65535(全黑)时,游戏结束。

对于棋盘的每一个状态,都有十六种操作,首先要判断这十六种操作之后是否有完成的情况,如果没有,则再对这十六种操作的结果分别再进行上述操作,显然这里就要用到队列来存储了。而且在翻转的过程中有可能会回到之前的某种状态,而这种重复的状态是不应该再次入队的,所以维护 Visit[i]数组来判断 id==i 的状态之前是否已经出现过,如果不是才将其入队。如果游戏无法完成,状态必定会形成循环,由于重复状态不会再次入队,所以最后的队列一定会是空队列。

由于0^1=1,1^1=0,所以翻转的操作可以通过异或操作来完成,而翻转的位置可以通过移位来确定。

看一个例子(愚钝的我仔细研究了这个例子才真正懂了这道题):

简单分析:根据输入要求,b代表黑棋(black),w代表白棋(white)。因为总共才16个位置,且只有黑白两种表示,此时,可对每一次状态进行二进制压缩(其中b代表1,w代表0),例如:

bwwb

bbwb

bwwb

bwww

即可表示为1001 1101 1001 1000,其十进制值为40344。同时,计算可知根据黑白棋的摆放情况,总共有2^16种不同的状态。每一次经过有效的操作后,状态都会发生改变,此时,可借助二进制位运运算实现状态的改变,即对原有状态(相应的十进制表示)进行异或操作,以此来改变其对应二进制数的相关位置的值(1变0,0变1)。

例如:

先假设前一个状态为:

wwww

wwww

wwww

wwww

即二进制表示为0000 0000 0000 0000,十进制对应为0。若此时选定左上角第一个棋子进行操作,根据规则,它右边和下边的也要同时进行变换(因为其左边和上边为空,不做考虑),之后,相应的状态用二进制表示,应变为:1100 1000 0000 0000,十进制值为51200。

这个过程相当于对十进制数51200进行对十进制数0的异或操作,即next=0^(51200),而51200这个数则可以根据对十进制数1进行相应的左移操作得到。同时,我们知道,棋牌总共有16个位置,也就是说相应的不同的操作也有16种,即有16个不同的数经过异或操作用来改变前一个状态的值。

编码时需要注意的点:

由于只要求输出最优解即次数,而不关心最优解是怎么来的,因此BFS就足够了。一般都用队列queue来实现BFS,栈stack来实现DFS。

自定义结构体NODE,来记录队列里每个节点的状态值、到达当前状态时所需要的代价。在队列操作时,用curr节点代表当前队首节点,对16个位置中的每个位置都试探next节点。

如果某个节点的状态已经在队列里就不必再入队(但其实有个疑惑,万一当前节点的到达代价cost<队列里对应状态的cost呢?到底需不需要更新,我还没想明白~)。

AC代码

#include <iostream>
#include <queue>
#include <cstring>
using namespace std;

typedef struct node
{
int state; //该节点代表的状态值
int cost;  //到这个状态需要的翻转次数
}NODE;

int change[16] =
{0xC800,0xE400,0x7200,0x3100,
0x8C80,0x4E40,0x2720,0x1310,
0x08C8,0x04E4,0x0272,0x0131,
0x008C,0x004E,0x0027,0x0013}; //每个位置做翻转造成的改变值,用于异或当前状态值

bool visited[65536]; //标记某个状态值是否已经在队列里,不需要再check

int BFS(int state) //队列实现广度搜索,找到第一个最优解
{
if (state == 0 || state == 0xFFFF)
return 0;
memset(visited, false, sizeof(visited)); //标记都没有被访问过
queue<NODE> q; //存放状态的队列
NODE curr,next;
curr.state = state; curr.cost = 0; //为输入状态构造结点
//输入状态入队
q.push(curr);
visited[state] = true;
int i;
while (!q.empty())
{
curr = q.front();
q.pop(); //curr不是指针,所以pop不会造成空指针问题
for (i = 0; i<16; ++i) //做第i位置的翻转,共16种可能操作
{
next.state = curr.state ^ change[i];
next.cost = curr.cost + 1; //翻转次数+1
if (visited[next.state] == true) //翻转过程中有可能会回到之前的某种状态,重复的状态不该再次入队
continue;
if (next.state == 0 || next.state == 0xFFFF) //翻转后的节点满足条件
return next.cost;
q.push(next); //翻转后的节点入队
visited[next.state] = true;
}
}
return -1;
}

int main()
{
//for(int i = 0; i<15; ++i) cout << change[i] << endl;
char field[5][5];
for(int i = 0; i<4; ++i)
cin >> field[i];
int state = 0; //读入的棋盘代表的状态值
for (int i = 0; i < 4; ++i) //计算该状态值
{
for (int j = 0; j < 4; ++j)
{
state <<= 1; //左移用于接收新的一位
if (field[i][j] == 'b')
state += 1; //末尾置1
}
}
int ans = BFS(state);
if (ans == -1)
cout << "Impossible" << endl;
else cout << ans << endl;
return 0;
}


内存占用: 788Kb 耗时:47ms

算法复杂度:取决于BFS的复杂度,最差情形下,BFS必须寻找所有到可能节点的所有路径,因此其时间复杂度为 O(|V| + |E|),其中 |V| 是节点的数目,而 |E| 是图中边的数目。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  位运算 BFS 枚举 队列