一道很有意思的枚举题目:如何熄灭所有的灯?


前言

最近学了一下算法,刷几个题还蛮有意思的,在此记录一下。


一、枚举算法是什么?

枚举就是逐个考虑会导致目标结果的所有可能。也就是列举出所有可能的答案,一个一个看他们是否符合要求。
比如求小于n的所有质数,可以从2到n-1一个一个去试,这就是枚举。当然也有更好的方法。
下面详解一道枚举题(听了郭炜老师的算法课后用C语言写的),该题出自POJ1222。

二、题目

有一个由按钮组成的矩阵,其中每行有6个按钮,共5行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上、下、左、右)的灯都会改变一次。即,如果灯原来是点亮的,就会被熄灭;如果灯原来是熄灭的,则会被点亮。在矩阵角上的按钮改变3盏灯的状态;在矩阵边上的按钮改变4盏灯的状态;其他的按钮改变5盏灯的状态。比如:
左上角按钮按下:
在这里插入图片描述
第一行第三个按钮按下:
在这里插入图片描述
第三行第四个按钮按下:
在这里插入图片描述
与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果;
现在给定矩阵中每盏灯的初始状态,求一种按按钮的方案,使得所有的灯都熄灭。
输入:
5行组成,每一行包括6个数字(0或1)。相邻两个数字之间用单个空格隔开。0表示灯的初始状态是熄灭的,1表示灯的初始状态是点亮的。
要求输出:
5行组成,每一行包括6个数字(0或1)。相邻两个数字之间用单个空格隔开。其中的1表示需要把对应的按钮按下,0则表示不需要按对应的按钮。
输入样例:(第一行的2代表有两组数据)
在这里插入图片描述
前五行是第一组数据,后五行是第二组。
输出样例:
在这里插入图片描述
PUZZLE#1代表第一组数据对应的输出。

三、解析

  首先要明确一个按钮需要按几次。对于一个灯的亮灭,按一次取反,再按一次会恢复到原来的状态,所以一个按钮最多按一次。
  其次,一共有30个按钮,每个按钮有两种状态(1:按下,0:不按),若枚举所有按钮的状态,一共有2^30个可能,这需要判断的次数太多,会超时。
  对于有些问题,如果确定了局部的一些状态,那么其余的状态也随之而定,那么可以对该局部进行枚举,这样可以省下很多时间。
  比如之前找小于n的全部质数,没必要从2一直枚举到n-1,从2枚举到根号n即可。因为:
  若在小于n的正整数中,存在a,b,使得n=a*b,假设a大于根号下n,那么b一定小于根号n,所以b一定小于a,因此在枚举2到根号n的时候,已经枚举了所有情况。

  在这道题中,第一行的按法决定了后面几行的按法。因为,第一行的按钮按下后,一定有些灯是亮着的,有些是灭了的。为了让第一行亮着的灯灭掉,必须按下对应的第二行的按钮。比如,第一行第三个灯是亮着的,那么第二行的第三个按钮必须按下才能让这个灯灭掉。所以第二行的按法由第一行决定,且第二行的按法唯一。在第二行的状态确定后,同理,第三行的按法要保证第二行的灯全是灭着的,所以第三行怎么按也是确定的,以此类推,后面几行的按法都是确定的。
  因此枚举第一行的按法就可以了。第一行一共有6个按钮,那么需要考虑2^6种按法。对于第一行的每一种按法,主要观察第5行按钮在保证第4行的灯都灭的同时,第5行的灯也都灭,那说明第一行的按法是可行的,否则需要考虑下一种按法。
  思路理清后,便可以开始写程序了。
  第一步是获取这30个按钮的初始状态。题目给的数据是一个5×6 的矩阵,因此可以用一个二维数组存放。
  但是,这些数据不是0就是1,而且每行的数据比较短,看到这样的数字,最好的存放方式应是将每个数字当作1bit,也就是位操作。第一行的每个数字占1bit,一共六个bit;而一个char类型的数据所占空间是8bit,所以可以用char类型变量的低6bit存放一行的数据。因此想存放这5×6个按钮的状态,只需要一个char类型的一维数组即可,数组长度是5,代表5行按钮;数组中每个元素对应一行的按钮状态。
  2^6=64,可以让一个char变量从00000000自增到00111111,每一个数的低六位就是一种按法,正好可以表示64种状态。
  位操作优点:占用空间小,运算速度快。因为按下按钮后,按钮所在位置的灯和周围的灯都取反,所以可以用1来和这些需要取反的灯(bit)进行异或,大大提高效率。

  注:位操作中,1和一个位相异或,可以把该位取反;0和一个位相与,可以把该位置0;1和一个位相或,可以把该位置1。
  位操作在嵌入式编程用的比较多,比如一个8位寄存器R1,想把最高位变成0,而其他位不变,那么可以用以下写法:

R1 &= ~(1 << 7);

1左移7位,变成10000000,取反,变为01111111,和R1相与后,则R1最高位变为0,其余位置不变。
这样不仅运算效率提高,而且程序的可读性增强,一眼便可看出要操作的是第几位。

四、编程

将需要用到的位操作封装成函数以便调用:

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>

char inilight[5]; //每个元素对应灯矩阵的一行  初始矩阵
char lights[5];  // 每次按下按钮后 变化后的灯矩阵
char result[5];  // 最终结果 按钮的矩阵

int getbit(char c,int i)  //取字符C的第i个比特
{
    return (c >> i) & 1;
}
void setbit(char *c,int i,int v)  //把字符C的第i个比特改成v
{
    if(v)
        (*c) |= (1<<i);
    else
        (*c) &= ~(1<<i);
}
void verbit(char *c,int i)  //把第i位反转
{
    (*c) ^= (1 << i);
}

  有关参数传递:改变函数的形参,不会改变实参,除非传地址。
  也就是说,一个函数f1调用另一个函数f2的时候,传入一个参数a,那么f2是不能改变这个a的值的,除非传地址。比如以上代码中的函数verbit(char *c,int i),目的是改变传入的字符c的第i个bit,那么必须传入字符c的地址才能实现。
然后按照输出格式要求,定义一个输出结果的函数,其中result数组用来保存最终按法的结果,1代表按下,0代表不按:

void output(int t,char result[])
{
    //数组中每个元素代表按钮矩阵的一行
    //矩阵第i行第j列的按钮就是数组第i行第j列的比特
    printf("PUZZLE #%d\n",t);
    int i,j;
    for(i=0;i<5;i++)
    {
        for(j=0;j<6;j++)
        {
            printf("%d",getbit(result[i],j));
            if(j<5)
            printf(" ");//数据用空格隔开
        }
        printf("\n");//每输出一行需要换行
    }
}

准备工作做完后,就该写main()函数了。下面是main()函数的全部代码:

int main()
{
    printf("Hello world!\n");
    int T; //测试数据的组数
    int i,j,s,t,n,now; //t:当前输入是第几组,now是当前按钮矩阵的状态
    scanf("%d",&T);
    for(t=1 ; t <= T;t++)
    {
        for(i=0; i<5; i++)
            for(j=0;j<6;j++)
            {
                scanf("%d",&s);  //s是第i行第j列的数据
                setbit(&inilight[i],j,s);
            } //这两层循环把数据都读了进来
        //接下来枚举第一行按钮的所有按法
        //一共64种 让一个二进制数int,从0变到63
        for(n=0; n<64; n++) //每个n代表第一行的一种状态
        {
            memcpy(lights,inilight,sizeof(inilight));//lights存放每次按钮作用后灯的状态
            //每一次枚举都要在原始的状态改变,所以要把inilight拷贝到lights。
            now = n; //当前行的按钮状态
            for(i = 0;i<5;i++) //假设行号从0开始
            {
                //第一行的每一种按法,余下四行都确定的与之对应
                result[i] = now; //先把第i行的按钮状态存起来
                for(j=0;j<6;j++) //第i行第j个灯
                {    //下面这段就是对i行的灯进行处理
                    if(getbit(now,j))//若是1,表示按下,按下才执行
                    {
                        if( j >0 )
                            verbit(&lights[i],j-1);//相邻左侧的灯取反
                        verbit(&lights[i],j);//自身取反
                        if( j<5 )
                            verbit(&lights[i],j+1);//相邻右侧的灯取反
                    }
                }
                if(i<4)
                    lights[i+1] ^= now; //反转下一行,1代表按下,与1异或就会取反
                now = lights[i];//此处now存放的是第i+1行的按钮状态
                //因为第i+1行的按钮需要保证第i行的灯全灭
                //所以第i行灯的状态是什么样的,那第i+1行按钮的状态就是什么样的
            }
            if(lights [4] == 0)
            {   //若最后一行lights [4] 全灭,则输出结果,其中t是第t个样例
                output(t,result);
                break;
            }
        }
    }
    return 0;
}

两组数据的运行结果:
(忘了删hello world了…)
在这里插入图片描述

总结

编程时,可根据问题本身特点,巧妙运用位运算,可大大执行算法效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值