AcWing 95. 费解的开关 (yxc代码保姆级题解+注释)

  • Post author:
  • Post category:其他



目录


题目


思路


一、分析


二、如何枚举第一行操作?


三、turn(x,y)函数


四、结果


代码


题目

你玩过“拉灯”游戏吗?

25盏灯排成一个 5×5 的方形。

每一个灯都有一个开关,游戏者可以改变它的状态。

每一步,游戏者可以改变某一个灯的状态。

游戏者改变一个灯的状态会产生连锁反应:和这个灯上下左右相邻的灯也要相应地改变其状态。

我们用数字 1表示一盏开着的灯,用数字 0表示关着的灯。

下面这种状态

10111
01101
10111
10000
11011

在改变了最左上角的灯的状态后将变成:

01111
11101
10111
10000
11011

再改变它正中间的灯后状态将变成:

01111
11001
11001
10100
11011

给定一些游戏的初始状态,编写程序判断游戏者是否可能在 6 步以内使所有的灯都变亮。


输入格式

第一行输入正整数 n,代表数据中共有 n 个待解决的游戏初始状态。

以下若干行数据分为 n组,每组数据有 5行,每行 5 个字符。

每组数据描述了一个游戏的初始状态。

各组数据间用一个空行分隔。


输出格式

一共输出 n行数据,每行有一个小于等于 6的整数,它表示对于输入数据中对应的游戏状态最少需要几步才能使所有灯变亮。

对于某一个游戏初始状态,若 6步以内无法使所有灯变亮,则输出 −1。


数据范围

0<n≤5000


输入样例:

3
00111
01011
10001
11010
11100

11101
11101
11110
11111
11111

01111
11111
11111
11111
11111


输出样例:

3
2
-1

思路


一、分析

  1. 开关按的顺序可以任意
  2. 每个开关最多按一次
  3. 由于第一行决定了第二行,第二行的按法是唯一确定的



  4. 以此类推,每一行开关的操作都由前一行灯的亮灭状态唯一确定



  5. 当操作到最后一行时,可以保证前n-1行全亮,再看最后一行,若最后一行不是全亮,则没有解决方案,否则有解决方案



二、如何枚举第一行操作?


  • 指数型枚举,可以写递归
  • 使用for循环来进行位运算枚举

5个数,每个数都取0/1,共有2^5=32种方案。

转换为二进制后发现,0~2^5-1(0~31)都能分别代表5个数排列组合形式,共32种。即二进制数是从00000B开始,到11111B结束



可以用for(op=0;op<32;op++)来枚举32种方案,op代表0~31,即(

00000B~11111B



注意



这里的op代表着每一种方案,即告诉你哪盏灯该按,哪盏灯不该按;


op从1~31,因为是位运算,所以是1~31的二进制和1的二进制从右向左进行与的操作,比如10100B的op,意思是第2、4位(右到左01234)需要按,不是代表灯有没有亮,32种状态其实是哪些灯要按,一种op对应一种固定的按法,所以是&1==1或者^1==0。



(引自ACwing答疑评论区)

e.g. 01101B 代表着第1/3/4盏灯要按,所以如果判断出第0/2/3位数字为1的话,则说明需要按亮这三盏灯,二进制数是从右往左数,从第0位开始算的,而在使用turn函数时,i循环是从左往右开始,故


turn(0,4-i)


判断第op种方案中的第k位数字是否是1?


op右移k位与1与运算 (op>>k&1)

三、turn(x,y)函数

turn(x,y),代表x行y列的坐标,该函数用于改变(x,y)坐标周围的灯的状态,需要使用dx,dy数组把周围的偏移量都记下来,分别记录本身+上下左右共五个坐标值,若超过了0或5边界,则直接忽略;然后与1进行异或^操作,取反


如(x,y)格子:

左边为(x+1,y) ===(1,0)

右边为(x-1,y) ===(-1,0)

上 (x,y+1) ===(0,1)

下 (x,y-1) ===(0,-1)

把字符串中的0或1取反:与1做异或操作 (x^=1)

四、结果

第一行的32种操作方案中有32种最终按灯结果,有的没法让灯全亮,有的可以,所以在每次一种方案计算完成后,如果能让灯全亮,就都取这一次和上一次的min值,

直到最后得出来的res就是32种方案里最小的按灯次数

代码

#include<bits/stdc++.h>
using namespace std;
const int N=6; ///字符串最后一位是'\0',因此要多定义一位
char g[N][N]; ///存方案
char backup[N][N];///存备份方案

///数据偏移量,左、右、上、下、本身
int dx[5] = {-1, 0, 1, 0, 0}, dy[5] = {0, 1, 0, -1, 0};

void turn(int x, int y){
    for (int i = 0; i < 5; i ++ ){
        int a = x + dx[i], b = y + dy[i];///(a,b)代表(x,y)周围的坐标
        if (a < 0 || a >= 5 || b < 0 || b >= 5) continue;/// 在边界外,直接忽略即可
        g[a][b] ^= 1; ///异或操作,将(a,b)取反
    }
}

int main(){
    int t;
    cin>>t;
    while(t--){
        for(int i=0;i<5;i++)
            cin>>g[i]; ///输入初始状态的二维数组
        int res=7;///结果数
        for(int op=0;op<32;op++){  ///枚举第一行的32种方案
            memcpy(backup,g,sizeof g);
            int step=0;///按灯的步骤数
            for(int i=0;i<5;i++){
                if(op>>i&1){ ///如果第i位是1的话,按亮该盏灯
                    step++;
                    turn(0,4-i); ///二进制数的位数和for循环i开始的位数相反,所以用4-i代表需要打开的灯
                }
            }
            for(int i=0;i<4;i++){  ///第i行决定了第i+1行
                for(int j=0;j<5;j++){
                    if(g[i][j]=='0'){ ///如果当前行的灯是灭的,
                            ///那必须要按亮下一行的灯,才能让它亮起来
                        step++;
                        turn(i+1,j);
                    }
                }
            }
            bool dark = false;
            for(int i=0;i<5;i++){ ///判断最后一行的每盏灯的状态
                if(g[4][i]=='0'){
                    dark=true;
                    break;
                }
            }
            if(!dark) res=min(res,step); ///如果最后一行全亮,则得出结果数
            memcpy(g,backup,sizeof backup);///恢复g数组的状态
        }
        if(res>6) res=-1;
        cout<<res<<endl;        
    }
    return 0;
}

收获:

  1. memcpy函数的用法:memcpy(des,src,sizeof * );
  2. 位运算:右移x位后与1与运算,op>>x&1;异或运算,x与1异或运算是取反,x=x^1
  3. 坐标存储:取偏移量,类似棋盘问题,把上下左右即自身的偏移量放进两个数组里存起来,然后使用for循环来调用值
  4. 分析代码时,先按主干来分析,一步一步分析出自己需要什么变量,什么函数,函数可以最后完成



版权声明:本文为weixin_45662399原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。