题目大意
有一个4×4的棋盘,上面放满棋子,棋子一面是白色,另一面是黑色。每次翻转一枚棋子,同时与这枚棋子相邻的上下左右的棋子也要翻转。问能否通过数次翻转使得所有的棋子全部白色面朝上或全部黑色面朝上呢?如果不可以,输出 “Impossible”(没有引用号),如果可以,输出最小翻转次数。
样例输入
bwwb
bbwb
bwwb
bwww
样例输出
4
思路
我们贪心地思考这个问题。假如最后我们的目标是全部为白色。对于棋盘第一行,我们可以尝试全部 2 4 = 16 2^4=16 24=16种可能的翻转操作(相当于为16棵搜索树分别建立了根节点)。然后对于第二行,我们需要通过翻转带动将第一行的黑色棋子全部翻转为白色。同时在第二行翻转的这一棋子也会带动左右和第三行的棋子状态改变,我们实时更新这一状态即可。以此类推,直到上面三行的棋子全部翻转为白色,我们再来看第四行,如果第四行的棋子也全为白色,则方案可行。否则,方案失败。假设最后目标是全部为黑色,也是同理。
如何实现这一过程呢?最快的方法是用位运算。由于0^0=0, 1^1=0, 0^1=1, 1^0=1,故1表示翻转,0表示不翻转。直接用0000~1111的二进制数按位异或第一行,即得到16种状态(天雾辰明流剑术中传)。
要表示翻转的棋子左边棋子也要翻转,设给出一种翻转情况为0110,则需要再按位异或((0110)<<1)=1100 ,即表示翻转状态向左侧传递一位(天雾辰明流剑术左传)。如果遇到1100这类反转情况,再让棋盘上这一行的状态按位异或((1100)<<1)=11000显然是错误的,这样棋盘上这一行的状态对应的二进制数从右向左第五位一定是0,0^1=1,这样按位异或后得到的状态对应的二进制数超出了1111,即不存在。为纠正这一错误,应令原状态按位异或(((1100)<<1)&1111)=1000,即通过与运算1111消除第五位以后的1带来的影响(1&0=0)。
要表示翻转的棋子右边棋子也要翻转,设给出一种翻转情况为0110,则需要再按位异或((0110)>>1)=0011 ,即表示翻转状态向右侧传递一位(天雾辰明流剑术右传)。
棋盘上某一行当前的状态对应的就是下一行和下下行(状态向下传递)应该按位异或的二进制串。均以某位为0表示无需翻转,为1表示需要翻转。在读入时可以分类讨论,一类终点是全白,就令白色为0,黑色为1;另一类是终点全黑,就令黑色为0,白色为1。具体细节见代码。
考点
位运算
状压
枚举和暴力
模拟
贪心
AC代码(含注释)
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn = 0x3f3f3f3f;
int ed_white[5], ed_black[5], num[(1<<4)+5];
int ans = maxn;
int get_step(int now) {//将十进制化为二进制统计1的个数
int cur_step = 0;
while (now) {
if (now & 1)cur_step++;
now = now >> 1;
}
return cur_step;
}
int search(int row[]) {
int cur_row[6], cur_ans = maxn;
for (int i = 0; i<(1 << 4); i++) {//遍历第一行的全部16种翻转可能
cur_row[1] = row[1] ^ ((i << 1) & 0xf) ^ i ^ (i >> 1);//翻转第一行
cur_row[2] = row[2] ^ i;//状态下传,带动翻转第二行
int step = num[i]; //记录这种翻转可能对应的翻转步数
for (int j = 2; j <= 4; j++) {
cur_row[j] = cur_row[j] ^ ((cur_row[j - 1] << 1) & 0xf) ^ cur_row[j - 1] ^ (cur_row[j - 1] >> 1);//翻转第j行
cur_row[j + 1] = row[j + 1] ^ cur_row[j - 1];//状态下传
step += num[cur_row[j - 1]];//记录步数
}
if (!cur_row[4])cur_ans = min(cur_ans, step);//方案成功,更新答案
}
return cur_ans;
}
int main() {
char x;
for (int i = 1; i <= 4; i++) {
for (int j = 0; j < 4; j++) {
scanf(" %c", &x);
if (x == 'b')ed_white[i] |= (1 << j);//记录二进制每一位
else ed_black[i] |= (1 << j);
}
}
for (int i = 1; i < (1 << 4); i++)num[i] = get_step(i);//异或数在0~15之间
int ans_white = search(ed_white), ans_black = search(ed_black);//分别统计全黑和全白的最小步数
ans = min(ans_white, ans_black);//取公共最小步数
ans == maxn ? puts("Impossible") : printf("%d", ans);//答案不为maxn,说明可以达成目标
return 0;
}
心得
本题是一道很好的二进制状态压缩的练手题。对上下左右状态的传递模拟用二进制语言描述尤其不好想。得出本题的思路也用到了贪心,即先给出一种可能的翻转情况,这样后续每行都可以用贪心的翻转方式去弥补上一行的不足,直到最后一行再去判断是否成立。本题值得仔细品味。