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; }