熄灯问题(OpenJ_Bailian - 2811)题解


题目描述

有一个由按钮组成的矩阵,其中每行有6个按钮,共5行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上边、下边、左边、右边)的灯都会改变一次。即,如果灯原来是点亮的,就会被熄灭;如果灯原来是熄灭的,则会被点亮。在矩阵角上的按钮改变3盏灯的状态;在矩阵边上的按钮改变4盏灯的状态;其他的按钮改变5盏灯的状态。

在这里插入图片描述

在上图中,左边矩阵中用X标记的按钮表示被按下,右边的矩阵表示灯状态的改变。对矩阵中的每盏灯设置一个初始状态。请你按按钮,直至每一盏等都熄灭。与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果。在下图中,第2行第3、5列的按钮都被按下,因此第2行、第4列的灯的状态就不改变。

在这里插入图片描述

请你写一个程序,确定需要按下哪些按钮,恰好使得所有的灯都熄灭。根据上面的规则,我们知道1)第2次按下同一个按钮时,将抵消第1次按下时所产生的结果。因此,每个按钮最多只需要按下一次;2)各个按钮被按下的顺序对最终的结果没有影响;3)对第1行中每盏点亮的灯,按下第2行对应的按钮,就可以熄灭第1行的全部灯。如此重复下去,可以熄灭第1、2、3、4行的全部灯。同样,按下第1、2、3、4、5列的按钮,可以熄灭前5列的灯。

输入要求

5行组成,每一行包括6个数字(0或1)。相邻两个数字之间用单个空格隔开。0表示灯的初始状态是熄灭的,1表示灯的初始状态是点亮的

输出要求

5行组成,每一行包括6个数字(0或1)。相邻两个数字之间用单个空格隔开。其中的1表示需要把对应的按钮按下,0则表示不需要按对应的按钮

样例输入

0 1 1 0 1 0
1 0 0 1 1 1
0 0 1 0 0 1
1 0 0 1 0 1
0 1 1 1 0 0

样例输出

1 0 1 0 0 1
1 1 0 1 0 1
0 0 1 0 1 1
1 0 0 1 0 0
0 1 0 0 0 0

算法分析

想法一:

  • 对于每个按钮按下与不按下都有两种情况,对于本题一共5*6=30个按钮,一共230种情况,很明显会超时,因此我们需要将枚举范围简化。

想法二:

  • puzzle[6][8]存放最终灯的状态
  • press[6][8]存放按钮状态
  • (数组为什么6*8后面解释)

1、如何简化枚举范围?

  • 我们可以看出:
  • 第二行的按钮会改变第一行的灯的亮与熄(第二行开关使得第一行的灯全灭)
  • 第三行的按钮会改变第二行的灯的亮与熄(第三行开关使得第二行的灯全灭)
  • 第四行的按钮会改变第三行的灯的亮与熄(第四行开关使得第三行的灯全灭)
  • 第五行的按钮会改变第四行的灯的亮与熄(第五行开关使得第四行的灯全灭)
  • (那么第五行的灯将由第五行的开关情况以及周围三个点的开关情况进行控制)
    因此对于第一行的每一个开关的按与不按,都会使得第二行的按钮按下情况确定,第二行确定后,会使得第三行的按钮按下情况确定,依次类推,因此只需要对于第一行的开关按下情况来进行枚举,一共26种情况

2、如何判断某种情况是正确的呢?

  • 我们从上面的推导过程可以看出来,第1-4行灯将全部熄灭,而第五行的灯由当前开关和其周围的开关状态(例如puzzle[5][j]是由press[5][j],press[4][j],press[5][j-1],press[5][j+1],即当前位置的 当前、上、左、右的开关状态)决定,因此只要第五行的灯全部熄灭,那么就意味着当前情况下的开关状态为答案
    如果puzzle[5] [j]原先亮着(值为1),那么决定它最终状态的按钮作用互相抵消后,某一个按钮仍然需要是按下的(值为1),如果puzzle[5] [j]原先熄灭的(值为0),那么决定它最终状态的按钮作用互相抵消后,没有按钮是按下的(值为0)。
for(j=1;j<=6;j++)
    {
        // 逐一判断第五行的灯是否都熄灭(如果puzzle[5][j]为1那么就需要按下开关为1,否则不需要按下为0)
        if( puzzle[5][j] != (press[5][j] + press[5][j-1] + press[5][j+1] + press[4][j]) %2 )
            return false;
    }
  return true;

3、如何枚举第一行的开关状态?

  • 如果单纯的使用六重循环来对6个开关进行枚举是可以实现。
  • 更加简单的方法是将第一行开关状态看成一串二进制码,用二进制码反复加1来遍历由000000~111111
void enumerate() {

    int c;
    for(c=1; c<=6; c++)
        press[1][c]=0;

    while( !guess() ) //guess()是判断当前情况是否合理的函数
    {
        // 采用二进制进位的算法,从000000 - 111111枚举第一行按钮的方式
        press[1][1]++;
        c=1;
        while(press[1][c]>1)
        {
            press[1][c]=0;
            c++;
            press[1][c]++;
        }
    }
}

在这里插入图片描述

4、怎么通过第一行来决定第二行的开关状态

  • 例如puzzle[1][j],要是(1,j)这个位置的灯熄灭,是由:
    (1)press(0,j)-----(1,j)的上按钮
    (2)press(1,j-1)-----(1,j)的左按钮
    (3)press(1,j+1)-----(1,j)的右按钮
    (4)puzzle(1,j)-----(1,j)本身灯的亮与灭
    (5)press(1,j)-----(1,j)本身的按钮情况
    (6)press(2,j)-----(1,j)的下按钮
    这六个参数决定的,但根据我们的分析,可以知道前5个状态值已经是确定的,因此我们只需要根据前5个参数便可以推断出第二行press(2,j)的开关情况,以此类推
for(i=2;i<=5;i++)
    {
        for(j=1;j<=6;j++)
        {
            // 根据同列的上一行灯的最后状态,来决定是否按按钮
            press[i][j] = ( press[i-1][j]
                            + puzzle[i-1][j]
                            + press[i-1][j-1]
                            + press[i-2][j]
                            + press[i-1][j+1] ) %2;
        }
    }

5、整个判断状态是否满足的函数

bool guess(){
    int i,j;
    for(i=2;i<=5;i++)
    {
        for(j=1;j<=6;j++)
        {
            // 根据同列的上一行灯的最后状态,来决定是否按按钮
            press[i][j] = ( press[i-1][j]
                            + puzzle[i-1][j]
                            + press[i-1][j-1]
                            + press[i-2][j]
                            + press[i-1][j+1] ) %2;
        }
    }

    for(j=1;j<=6;j++)
    {
        // 逐一判断第五行的灯是否都熄灭
        if( puzzle[5][j] != (press[5][j] + press[5][j-1] + press[5][j+1] + press[4][j]) %2 )
            return false; //不满足题意
    }

    return true;  //满足题意
}

优化

  • puzzle和press用6*8,可以将特殊的边界和顶点的开关作用情况统一,将边界的顶点的开关作用的灯的数目也为5个

在这里插入图片描述

解题标程

#include<stdio.h>
int press[6][8];
int puzzle[6][8];

bool guess(){
    int i,j;
    for(i=2;i<=5;i++)
    {
        for(j=1;j<=6;j++)
        {
            // 根据同列的上一行灯的最后状态,来决定是否按按钮
            press[i][j] = ( press[i-1][j]
                            + puzzle[i-1][j]
                            + press[i-1][j-1]
                            + press[i-2][j]
                            + press[i-1][j+1] ) %2;
        }
    }

    for(j=1;j<=6;j++)
    {
        // 逐一判断第五行的灯是否都熄灭
        if( puzzle[5][j] != (press[5][j] + press[5][j-1] + press[5][j+1] + press[4][j]) %2 )
            return false;
    }

    return true;
}

void enumerate() {

    int c;
    for(c=1; c<=6; c++)
        press[1][c]=0;

    while( !guess() )
    {
        // 采用二进制进位的算法,从000000 - 111111枚举第一行按钮的方式
        press[1][1]++;
        c=1;
        while(press[1][c]>1)
        {
            press[1][c]=0;
            c++;
            press[1][c]++;
        }


    }
}

int main(){
    int t,i,n,j;

    memset()
    // 初始化0行的所有元素
    for(i=0;i<8;i++)
    press[0][i]=puzzle[0][i]=0;
    // 初始化0列,7列的所有元素
    for(i=1;i<6;i++)
        press[i][0]=puzzle[i][0]=press[i][7]=puzzle[i][7]=0;

    for(i=1;i<=5;i++)
        for(j=1;j<=6;j++)
            scanf("%d",&puzzle[i][j]);

    enumerate();

    for(i=1;i<=5;i++){
        for(j=1;j<=6;j++){
            printf("%d ",press[i][j]);
        }
        printf("\n");
    }

    return 0;
}

错题分析

二进制串的遍历生成算法

void enumerate() {

    int c;
    for(c=1; c<=6; c++)
        press[1][c]=0;

    while( !guess() )
    {
        // 采用二进制进位的算法,从000000 - 111111枚举第一行按钮的方式
        press[1][1]++;
        c=1;
        while(press[1][c]>1)
        {
            press[1][c]=0;
            c++;
            press[1][c]++;
        }


    }
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

省下洗发水钱买书

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

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

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

打赏作者

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

抵扣说明:

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

余额充值