题目
你玩过“拉灯”游戏吗?25盏灯排成一个5x5的方形。每一个灯都有一个开关,游戏者可以改变它的状态。每一步,游戏者可以改变某一个灯的状态。游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。
我们用数字“1”表示一盏开着的灯,用数字“0”表示关着的灯。下面这种状态
10111
01101
10111
10000
11011
在改变了最左上角的灯的状态后将变成:
01111
11101
10111
10000
11011
再改变它正中间的灯后状态将变成:
01111
11001
11001
10100
11011
给定一些游戏的初始状态,编写程序判断游戏者是否可能在6步以内使所有的灯都变亮。
题解
取出整数n在二进制表示下的第k位 (n>>k)&1
把整数n在二进制表示下的第k位取反 n xor (1<< k)
正确的做法有很多种,我用的是其中一种
根据题解:
在上述规则的01矩阵的点击游戏中,很容易发现两个性质:
1.每个位置至多只会被点一次
2.若固定了第一行(不能再对第一行进行操作),则满足题意的点击方案至多只有1种。其原因是当第i行某一位为1时,若前i行已被固定,只能点击第i+1行该位置上的数字才能使第i行的这一位变成0.从上到下按行使用归纳法可得上述结论。
于是,我们可以枚举第一行的点击方法,共2^5=32种。完成第一行的点击后,固定第一行,按照上述性质2从第一行开始递推。若到达第n行不全为1则说明这种点击方式不合法。在所有合法的点击方式中取点击次数最少的就是答案。对第一行的32次枚举涵盖了该问题的所有可能状态,因此该做法是正确的。
对于第一行点击方式的枚举可以采用位运算或递归的方式
位运算方式:枚举0—31这32个5位二进制数,若二进制的第k位为1,就点击第一行第k+1列的数字
代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int c[8]={1,2,4,8,16,32,64};
int n,ans;
int a[8],b[8];
char ch[8];
void change(int i,int j){
j--;
a[i]^=1<<j;
if (j+1<5) a[i]^=1<<(j+1);
if (j-1>=0) a[i]^=1<<(j-1);
a[i+1]^=1<<j;
a[i-1]^=1<<j;
}
int work(){
int cnt=0;
for (int i=2;i<=5;i++){
for (int j=1;j<=5;j++)
if (!((a[i-1]>>(j-1))&1)) change(i,j),cnt++;
}
if (a[5]==31) return cnt;
return 1000000;
}
void first(int k,int s){
if (k>=5){
memcpy(b,a,sizeof(b));
int kk=s+work();
memcpy(a,b,sizeof(a));
ans=min(kk,ans);
return;
}
for (int i=k+1;i<=5;i++){
change(1,i);
first(k+1,s+1);
change(1,i);
}
first(k+1,s);
}
int main(){
scanf("%d",&n);
for (;n;n--){
memset(a,0,sizeof(a));
for (int i=1;i<=5;i++){
scanf("%s",ch);
for (int j=4;j>=0;j--)
if (ch[j]=='1') a[i]+=c[4-j];
}
ans=100000;
first(0,0);
if (ans<=6) printf("%d\n",ans);
else printf("-1\n");
}
}