费解的开关

费解的开关

题目描述

在这里插入图片描述


核心思路

这道题如果纯暴力枚举的话,肯定是会超时的。这题出现了0和1,应该是和位运算有密切关系。所以这道题可以从二进制思想方面进行优化,分析可以发现,这题有三个重要的性质。

  • 每一盏灯最多只会被按一次,因为如果按两次的话就相当于没有按,它又回到最初的状态了,多按了一次无用功,必然得不到最少步数的最优解。
  • 点击的先后顺序不影响结果。
  • 只要第0行所有灯的状态都已经确定了,则全部灯的状态也就确定了,满足题意的方案最多只有一种。因为:如果第0行所有灯的状态确定了,那么此时只能通过按第1行中的某些灯才能改变第0行灯的状态,第1行根据第0行的状态就可以知道自己要干什么,因此第1行操作完之后,第1行所有灯的状态也就确定了…依次类推,当第i行某一盏灯位0时,若前i行已经固定了, 那么只能点击第i+1行该位置上的灯才能使第i行的这一位变成1.于是可以从上往下一步步递推,当到了最后一行时,说明最后一行之前的所有行的灯的状态都被固定了,那么如果此时最后一行还有灯没有被点亮,则说明该方案无解(因为不存在通过下一行再来点亮最后一行灭着的灯了)。

对于第0行来说,有5盏灯,每盏灯有按/不按两种操作选择,因此第0行总共有 2 5 = 32 2^5=32 25=32操作,即从00000到11111。对于每一种枚举到的操作,都会得到第0行灯的状态。对于第0行的操作来说,0表示不按,1表示;不要和题目给定的初始的第0行灯的亮灭状态混淆了,对于灯的状态来说,由题意可知,0表示灭,1表示亮。也就是说, 我们需要枚举的是第0行所有灯的操作,总共有32种操作方案,对于每一种枚举到的操作方案,都能得到5个确定的二进制位,这些二进制位指明了所对应的灯应该执行的操作(按/不按)。即由操作可以修改第0行所有灯的亮灭情况。因为第0行有5盏灯,每盏灯都有2种操作(按/不按),所有总共有32个操作。于是我只需要枚举第0行的这32种操作即可 。我们可以利用二进制思想,从0到31枚举这32种操作。根据枚举到的这种操作,我们就可以相应地修改原始序列第0行的这5盏灯的亮灭状态,在根据这种操作所修改后的原始序列的第0行的这5盏灯的亮灭状态,那么此时第0行这5盏灯就已经处理完了,可以认为固定不动了。然后如果修改后地第0行地这5盏灯种还有灭地灯,那么这些灭的灯的状态只能通过下一行(即第1行)所对应的位置来点亮第0行灭这灯了。由上到下逐行递推,当到了最后一行时,如果最后一行还由灭着的灯,则说明对第0行的着5盏灯所枚举得到的这个点击方案是不合法的,应该再继续枚举另一种操作

虽然题目已经输入了全部灯的状态,即我们知道了第0行的这5盏灯的状态,但是我们不要管题目给定的第0行的这5盏灯的状态。我们先枚举第0行的某种操作,执行完该操作后,就可以得到此时第0行的这5盏灯的状态了。第0行灯的状态确定后,就固定不动了,那么可以通过下一行即第1行来改变第0行的状态了。依次类推,当到了最后一行时,如果最后一行还有灭灯,则说明第0行所枚举的这种操作不合法无解,那么就继续枚举下一种操作。如果该种操作有解 r e s res res,那么也继续枚举其他操作(因为其他操作也可能得到新的最优解), a n s = m i n ( r e s , a n s ) ans=min(res,ans) ans=min(res,ans)于是执行完32种操作后,可以得到最优解 a n s ans ans

因此,这题很灵活巧妙,只需要枚举第0行的这32种操作,通过从上到下的递推就能求解了。

问题:如何把字符的0变成字符的1,把字符的1变成字符的0呢?

这里有个技巧,可以利用异或。因此字符的’0’它的ASCII码是48,二进制就是110000,字符的’1’它的ASCII码是49,二进制就是110001。那么字符’0’想变成字符’1’,那么用与1进行异或,即110000^000001=110001,而这就是字符’1’。同理字符’1’变成字符’0’也是同样的道理。


代码

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=6,INF=0x3f3f3f3f;
char g[N][N],backup[N][N];
int dx[5]={0,-1,0,1,0},dy[5]={0,0,1,0,-1};
int ans;    //最少的操作步数
//按下开关
void turn(int x,int y)
{
    //枚举该点(x,y)周围四个方向和他自身
    for(int i=0;i<5;i++)
    {
        int a=x+dx[i];  //偏移后的横坐标
        int b=y+dy[i];  //偏移后的纵坐标
        //偏移后的点(a,b)越界了
        if(a<0||a>=5||b<0||b>=5)
            continue;
        //0变1  1变0
        g[a][b]^=1;
    }
}
int work()
{
    //枚举第0行的32种 “操作"
    for(int op=0;op<1<<5;op++)
    {
        int res=0;  //对于该种 "操作" 下所需要的步骤数
        //当前"操作" 完后就会改变g的原始状态,那么如果我们接着枚举另一种"操作" 时
        //就是用上一次"操作" 后改变的g,而不是用最初的g。因此需要先把最初的g拷贝到backup中
        memcpy(backup,g,sizeof g);
        //该种"操作" 得到5位二进制数  依次看看这5位二进制数中哪一位有1
        for(int j=0;j<5;j++)
        {
            //第j个二进制位有1 表示该盏灯需要被按下
            if(op>>j&1)
            {
                res++;  //被按 步骤数+1
                turn(0,j);  //按下第0行的第j盏灯
            }
        }
        //处理第0行到第3行
        for(int i=0;i<4;i++)
        {
            for(int j=0;j<5;j++)//处理每一行的这5盏灯
            {
                //如果第i行的第j盏灯是灭的,则需要由下一行的对应列的 那盏灯被按下
                if(g[i][j]=='0')
                {
                    res++;  //被按 步骤数+1
                    //通过按下一行即第i+1行的第j盏灯,这样才能使得第i行的第j盏灯被点亮
                    turn(i+1,j);//按下第i+1行的第j盏灯
                }
            }
        }
        //用来判断最后一行是否还有灭灯  初始化为没有灭灯 成功点亮
        bool is_successful=true;
        //处理最后一行的每盏灯
        for(int j=0;j<5;j++)
        {
            if(g[4][j]=='0')//还有灭灯
            {
                is_successful=false;
                break;  //只要有一盏灭灯就提前退出了
            }
        }
        //如果全部点亮
        if(is_successful)
            ans=min(ans,res);   //枚举完这32种"操作",最终得到最少的操作步骤数
        memcpy(g,backup,sizeof g);//把最初的状态拷贝回g中
    }
    if(ans>6)
        return -1;
    else
        return ans;
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        ans=INF;//每组测试数据开始前,都把最优解置为无穷大
        for(int i=0;i<5;i++)
            scanf("%s",g[i]);
        printf("%d\n",work());
    }
    return 0;
}




#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=6,INF=0x3f3f3f3f;
char g[N][N],backup[N][N];
int dx[5]={0,-1,0,1,0},dy[5]={0,0,1,0,-1};
int ans;    //最少的操作步数
//按下开关
void turn(int x,int y)
{
    //枚举该点(x,y)周围四个方向和他自身
    for(int i=0;i<5;i++)
    {
        int a=x+dx[i];  //偏移后的横坐标
        int b=y+dy[i];  //偏移后的纵坐标
        //偏移后的点(a,b)越界了
        if(a<0||a>=5||b<0||b>=5)
            continue;
        //0变1  1变0
        backup[a][b]^=1;
    }
}
int work()
{
    //枚举第0行的32种 “操作"
    for(int op=0;op<1<<5;op++)
    {
        int res=0;  //对于该种 "操作" 下所需要的步骤数
        //用backup去操作,这样g的最初状态不会改变
        memcpy(backup,g,sizeof g);
        //该种"操作" 得到5位二进制数  依次看看这5位二进制数中哪一位有1
        for(int j=0;j<5;j++)
        {
            //第j个二进制位有1 表示该盏灯需要被按下
            if(op>>j&1)
            {
                res++;  //被按 步骤数+1
                turn(0,j);  //按下第0行的第j盏灯
            }
        }
        //处理第0行到第3行
        for(int i=0;i<4;i++)
        {
            for(int j=0;j<5;j++)//处理每一行的这5盏灯
            {
                //如果第i行的第j盏灯是灭的,则需要由下一行的对应列的 那盏灯被按下
                if(backup[i][j]=='0')
                {
                    res++;  //被按 步骤数+1
                    //通过按下一行即第i+1行的第j盏灯,这样才能使得第i行的第j盏灯被点亮
                    turn(i+1,j);//按下第i+1行的第j盏灯
                }
            }
        }
        //用来判断最后一行是否还有灭灯  初始化为没有灭灯 成功点亮
        bool is_successful=true;
        //处理最后一行的每盏灯
        for(int j=0;j<5;j++)
        {
            if(backup[4][j]=='0')//还有灭灯
            {
                is_successful=false;
                break;  //只要有一盏灭灯就提前退出了
            }
        }
        //如果全部点亮
        if(is_successful)
            ans=min(ans,res);   //枚举完这32种"操作",最终得到最少的操作步骤数
    }
    if(ans>6)
        return -1;
    else
        return ans;
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        ans=INF;//每组测试数据开始前,都把最优解置为无穷大
        for(int i=0;i<5;i++)
            scanf("%s",g[i]);
        printf("%d\n",work());
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值