Vijos1197 费解的开关 枚举+位运算

题目

你玩过“拉灯”游戏吗?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");
    }
}

猜你喜欢

转载自blog.csdn.net/yjy_aii/article/details/81632088