0
点赞
收藏
分享

微信扫一扫

acw.95费解的开关【递推搜索+开关灯模型】

工程与房产肖律师 2022-02-04 阅读 39
算法c++

Date:2022.02.02
题意:
你玩过“拉灯”游戏吗?
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≤500
输入样例:
3
00111
01011
10001
11010
11100

11101
11101
11110
11111
11111

01111
11111
11111
11111
11111
输出样例:
3
2
-1

思路:直接搜铁t,本题涉及非常经典的开关灯模型。设行数为n、列数为m,通用做法如下:
首先明确一点,每个点不可能转换>=2次,因为等价于翻转0、1次。
①二进制枚举对第1行的操作,共 2 m 2^m 2m种。
②一行一行更改。例:要让值为1的 ( i , j ) (i,j) (i,j)全变成0,则要对 ( i + 1 , j ) (i+1,j) (i+1,j)操作一次,进行翻转,使其间接更改 ( i , j ) (i,j) (i,j)
③判断最后一行是否含有不合法的元素。若存在任意一个,则当前方案不合法;否则当前方案合法,与合法方案的最小操作次数比较。
这里①中,为什么要枚举第1行的操作?
答:枚举的是对第1行的操作,之后遍历第一行的每一位,当前位为1则表示需要翻转。我们要让所有元素变得统一,因此对第1行翻转之后,第1行的状态决定了第2行的所有操作;而第2行翻转之后的状态又决定了第3行的所有操作…依次递推,第1行的每一种操作方式即决定了一种变换方式,这样不会产生遗漏。
代码如下:

//思想:第一行固定按法后,第二行使第一行更新为全1,之后一直顺序更新;若最后一行存在一个0,无解。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10,INF=1e9+10;
typedef long long LL;
char g[N][N];
LL dx[5]={0,-1,0,1,0},dy[5]={0,0,1,0,-1};
void turn(LL x,LL y)//以(x,y)为中心的五个点,都需要对状态翻转【0->1;1->0】
{
    for(int i=0;i<5;i++)
    {
        LL ix=x+dx[i],iy=y+dy[i];
        if(ix>=0&&ix<5&&iy>=0&&iy<5)
            g[ix][iy]^=1;//'0'的ASCII为48,异或49为1。
    }
}
LL work()
{
    LL ans=INF;
    //枚举第一行的按法,即固定了第一行的初始状态;之后,第二行使第一行必须要变成0...使得行按法全固定。
    //因此,第一行的按法共2^5,每种按法都使得后续按法固定,之后在所有按法中找最小解。
    for(int k=0;k<1<<5;k++)//枚举第一行的按法,1表示需要按,0不需要按
    {
        //备份状态,每次状态不变
        LL res=0;//记录本次翻转次数
        char backup[10][10];
        memcpy(backup,g,sizeof g);
        //操作第一行
        for(int j=0;j<5;j++)
        //第一行按法中此位为1,需要按一次【这里是第一行主动翻转,后续第二行要使第一行变全0,即固定了后续的所有按法】
            if(k>>j&1)
            {res++;turn(0,j);}
            
        //接着依次枚举前n-1行,如果该行该列为0则翻转
        //【注意此时经过第一行按法后,第一行不一定为全0,因此第一行状态也要算入翻转中】
        for(int i=0;i<4;i++)
            for(int j=0;j<5;j++)
            //当前(行,列)位置状态为0,那么要把(下一行,列)的位置按一下,使得当前(行,列)变为0。
                if(g[i][j]=='0')
                {res++;turn(i+1,j);}
        //最终,如果最后一行某一位状态为0,一定不合法【因为最后一行无法再由某行转化而来】
        bool is_successful=true;
        for(int i=0;i<5;i++)
            if(g[4][i]=='0')
            {is_successful=false;break;}
        if(is_successful) ans=min(ans,res);
        //最后回溯,将g恢复原状
        memcpy(g,backup,sizeof g);
    }
    if(ans>6) return -1;
    return ans;
}
int main()
{
    LL t;cin>>t;
    while(t--)
    {
        for(int i=0;i<5;i++) cin>>g[i];
        cout<<work()<<endl;
    }
    return 0;
}
举报

相关推荐

0 条评论