filp game (枚举法,DFS深搜,位运算,高斯消元)详解
filp game (枚举法,DFS深搜,位运算,高斯消元)详解 bolg:moonl1ght.xyz
2018年8月4日
题目来源:ZOJ2025 或 POJ1753
题目大意:给定一个 4 × 4 的棋盘,每个位置给了放好棋子,棋子有两个面,按规则进行翻转,使所有棋子都是同一个面。
解题思路:第一次接触到这种题目,刚开始是一点思路都是没有的,这是实话。翻阅网上的题解,发现这一题最长出现的标签是枚举,和深搜,那么我们不妨从枚举与深搜这个角度去看着一题目(该题还有其他解法)
枚举:即列举出所有的可能性,然后进行判断,输出符号题目条件的,那么这一题该怎么枚举呢,这是一个4 × 4的棋盘,那么棋盘大小我们是很确定的知道的,很明显这个 4 × 4 必然与枚举的范围是有关的,因为是翻转棋子,所以每个棋子只有两种情况就是 翻 或者 不翻
2(第一个棋子两种可能) * 2(第二个棋子两种可能) * 2(第三个棋子两种可能)* … … * 2(第16个棋子两种可能)
那么总的可能数就得216,只需要枚举出所有的可能数就好了
(有人直接用for循环写,是真的刚)
那么就要开始于dfs结合(可以参考下dfs的模板,如果不太会写的话)
void dfs()//参数用来表示状态
{
if(到达终点状态)
{
...//根据题意添加
return;
}
if(越界或者是不合法状态)
return;
if(特殊状态)//剪枝
return ;
for(扩展方式)
{
if(扩展方式所达到状态合法)
{
修改操作;//根据题意来添加
标记;
dfs();
(还原标记);
//是否还原标记根据题意
//如果加上(还原标记)就是 回溯法
}
}
} 我们的终点状态很容易知道就是一个棋子都不翻,但16个棋子已经是同一个面了
棋盘有大小限制,所以越界状态也很容易清楚了,代码如下
bool dfs(int n, int i, int j){
//翻 n 个棋子,棋子的坐标为 i 行, j 列
if(n == 0)
{
return check();//判断是否符合条件
}
//从翻16个棋子开始逐一减少
//一个棋子都不用翻
j += 1; //下一列每次搜下一列,如果不加的话会超时,就是dfs出不去
//越界情况
if(j > 4){
i += 1;
j = 1;
}
if(i > 4){
return false;
}
//如果超过棋盘大小则重置
for(; i <= 4; i++){
for(; j <= 4; j++){
flip(i,j);//翻转
if( dfs(n-1, i, j)){
return true;//如果符合条件就直接返回true这个值了
}
//不符合条件那再翻转回去
flip(i,j);//翻转
}
j = 1;
}
return false;
}Q:如何处理输入的字符棋盘呢?
A:化简成数字 0 与 1,由于棋子只存在两种状态 0 态 与 1 态,异或便成为了一个很好地选择
关于异或的介绍
按位或运算符
按位或运算将两个运算分量的对应位按位遵照以下规则进行计算:
0 | 0 = 0, 0 | 1 = 1, 1 | 0 = 1, 1 | 1 = 1
即只要有1个是1的位,结果为1,否则为0。
注意:只比较最后一位,所以可以省略很多步骤
按位异或运算符(^)
按位异或运算将两个运算分量的对应位按位遵照以下规则进行计算:
0 ^ 0 = 0, 0 ^ 1 = 1, 1 ^ 0 = 1, 1 ^ 1 = 0
即相应位的值相同的,结果为 0,不相同的结果为 1。
这个则是每个位置都比较
接下来就是翻转部分的函数,每次我翻转(i, j)位置的棋子, 上下左右(需在范围内)的棋子都要发生翻转
不妨将每一行都看作是一个数(16进制数),因为16进制数每四位是一位数,刚好与棋盘大小相符合,例如 一行棋子1 0 0 1 如果要对第二位进行翻转的话就要用0 1 0 0(16进制)就是 8,那么我们很容易得到帮助翻转的状态数
int state[][4] ={ {8, 4, 2, 1},{12, 14, 7, 3}}; // 1000 , 0100, 0010, 0001//翻转函数
void flip(int i, int j){
--j;
//对(i, j)位置的棋子进行翻转
CheckerBoard[i-1] ^= state[0][j];
CheckerBoard[i+1] ^= state[0][j];
CheckerBoard[i] ^= state[1][j];
}判断函数,就是每行16进制数都等于0或者15(因为1111是15的16进制数)
bool check(){
/*
0 0 0 0
0 0 0 0
0 0 0 0
0 0 0 0
0
1 1 1 1
1 1 1 1
1 1 1 1
1 1 1 1
8+4+2+1 == 15
*/
return ((CheckerBoard[1] == 0 || CheckerBoard[4] == 15) && CheckerBoard[1] == CheckerBoard[2] && CheckerBoard[2] == CheckerBoard[3] && CheckerBoard[3] == CheckerBoard[4]);
}//ac代码poj是无法用万能头文件的,会ce
#include <iostream>
#include <algorithm>
#define REP(i, a, b) for(int i = a; i < b; i++)
#define REP_(i, a, b) for(int i = a; i <= b; i++)
#define sl(n) scanf("%lld", &n);
#define si(n) scanf("%d", &n);
#define RepAll(a) for(auto x: a)
#define cout(ans) cout << ans << endl;
typedef long long ll;
//位运算的解法
using namespace std;
int CheckerBoard[8];
void read(){
REP_(i, 1, 4){
REP_(j, 1, 4){
//cout << "after change :" << CheckerBoard[i] << '\n';
CheckerBoard[i] <<= 1;
//cout << "now :" << CheckerBoard[i] << '\n';
if(getchar() == 'b'){
CheckerBoard[i] |= 1; //与1相或,于是就只改变最后一位
}
} getchar();//读取换行符号
}
return;
}
//每一行压缩一个数字,对第 i 行第 j 列棋子进行翻转,
//比如j = 2 j = 2,则(i−1 、 i+1)行的棋子应该和4(0100)相异或(与1异或切换状态,与0异或不改变)
//,而第 i 行棋子应与14(1110)相异或。
int state[][4] ={ {8, 4, 2, 1},{12, 14, 7, 3}};//1000, 0100, 0010, 0001,
void flip(int i, int j){
--j;
//对(i, j)位置的棋子进行翻转
CheckerBoard[i-1] ^= state[0][j];
CheckerBoard[i+1] ^= state[0][j];
CheckerBoard[i] ^= state[1][j];
}
bool check(){
/*
0 0 0 0
0 0 0 0
0 0 0 0
0 0 0 0
0
1 1 1 1
1 1 1 1
1 1 1 1
1 1 1 1
8+4+2+1 == 15
*/
return ((CheckerBoard[1] == 0 || CheckerBoard[4] == 15) && CheckerBoard[1] == CheckerBoard[2] && CheckerBoard[2] == CheckerBoard[3] && CheckerBoard[3] == CheckerBoard[4]);
}
bool dfs(int n, int i, int j){
//翻 n 个棋子
if(n == 0)
{
return check();//判断是否符合条件
}
//从翻16个棋子开始逐一减少
//一个棋子都不用翻
j += 1; //下一列, 如果不加则会超时
if(j > 4){
i += 1;
j = 1;
}
if(i > 4){
return false;
}
//如果超过棋盘大小则重置
for(; i <= 4; i++){
for(; j <= 4; j++){
flip(i,j);//翻转
if( dfs(n-1, i, j)){
return true;
}
flip(i,j);//翻转
}
j = 1;
}
return false;
}
void slove(){
for(int i = 0; i <= 16; i++){
if(dfs(i, 1, 0)){
cout << i << '\n';
return ;
}
}
cout << "Impossible" << '\n';
}
int main(){
read();
slove();
return 0;
}

