openjudge 熄灯问题 利用二进制和强大的位运算进行子集枚举 C++

2811:熄灯问题

总时间限制: 

1000ms

内存限制: 

65536kB

描述

有一个由按钮组成的矩阵,其中每行有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

来源

1222

 思路简介:

在该5*6矩阵中,每一盏灯都有初始状态,而每一盏灯的按键状态只会是按或者不按,因为就像现实中的一样,如果一盏灯按两次,显然等于没按。所以初步确定每一盏灯只有按或者不按两种状态,即1或0。那么一共的按键可能总数为2^(5*6)=2^30次,依次枚举显然很低效且会超时。那么既然没法全部枚举出来,那么我们应该从局部看,或者说拆分来看。

此类题应该学会从头开始分析,分析题意可知,按下每一盏灯都会使其上下左右和自身的状态翻转。我们不妨从第一行开始看,如果第一行的按键状态确定了,还可能会有没有熄灭的灯,假设此没熄灭的灯为第j个。而此时只有按下第二行第j个,才能使其熄灭,而不能是其他,显然第三以及以后的行的灯对第一行的灯显然是没有影响的(根据题意想想为什么)。所以,我们可以暂时的总结出,我们可以通过先列举第一行的按键状态,再往下一行行的确定,一直确定到最后一行。此时能保证除了最后一行外前面的几行的灯都是熄灭的,所以只需要检查最后一行的灯的是否全部熄灭即可。经过如此分析,所以我们找到了解决问题的关键,接下来就是确定如何枚举第一行的所有的按键状态。

现在我们要枚举的量从2^30缩小到只需要枚举第一行的2^6=64种按键状态,即000000-111111。

接下来便介绍如何利用二进制和位运算 进行此类子集枚举。

暂不太了解位运算和数据在计算机中的存储方式的建议先戳以下链接

位运算——强大得令人害怕_Daioo 随笔-CSDN博客https://blog.csdn.net/deaidai/article/details/78167367 详解计算机内部存储数据的形式---二进制数_Mekeater的博客-CSDN博客_计算机内部数据的存储形式是https://blog.csdn.net/qq_34720818/article/details/107850007?spm=1001.2101.3001.6650.15&utm_medium=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~Rate-15.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2~default~BlogCommendFromBaidu~Rate-15.pc_relevant_default&utm_relevant_index=21

在此处,利用子集枚举的思路很简单,000000-111111是64种可能,每一种可能对应的二进制数都是唯一的,在高位补后,00000000对应0,00111111对应63,即对应的整型数范围是0到63,正好就是64种,且一行只有6位,所以可以通过用char类型,1字节,8个比特位的数据类型来形成一一对应。这样,一行灯的情况,我们只用一个二进制数就完成了一一对应,从而完成子集枚举。那么如何得到每一盏灯的状况和修改每一盏灯的状况呢?——通过强大的位运算。详情见代码注释。

AC代码如下:

#include<bits/stdc++.h>
using namespace std;
char orilights[5];//存录入的数据
char lights[5];//暂存变化中的灯的状态
char result[5];//存储结果
int GetBit(char c, int i) {//勿忘此处数组位从0开始,此处c的第i位,其实对应着二进制的第i+1位
//即此处的i都是下标
	return (c >> i) & 1;//获取c的每一位比特位,即每一个灯的状态
//当c右移i位,c的第i+1个比特位到了最后一位,再&1,1的二进制除了第一位为1其他都为0,从而得到了c的第i+1位,对应数组位为i,以下同理不赘述
}
void SetBit(char & c, int i, int v) {//&是引用c,直接修改实参的值
	if (v) {//此函数将C的第i个比特位修改成v,当v为1时
		c |= (1 << i);//先1左移i位,使得相应的比特位为1,其他位都为0
		//|操作是二者相应的二进制位中只要有一个为1,那么值就为1
		// 即0|0=0,1|0=1,1|1=1 ,其实也就是|0是不变的
		//所以如此操作可以使得c的第i位比特位修改成1且并不会改变其他位的值
	}
	else//如果要修改为0
		//先知晓 &运算中,只有相应的二进制位都为1才得1
		//即0&1=0,1&1=1,0&0=0,其实也就是,比特位&1操作不会变,&0则变0
		c &= ~(1 << i);//同理,先将1左移i位再取反
	//1二进制位 000000001
	//假如左移i=3位(下标为3其实对应着第四位),低位补0,即成 00001000	
	//再取反 11110111,然后再c&=,便实现了除了第四位变为0,其他位不变

}
void FlipBit(char& c, int i) {//翻转函数,因为按下某个灯,会使得自己以及周围的部分灯翻转
	c ^= (1 << i);//左移后,只有第i+1位为1,其他位为0
	//利用按位异或运算,二者相同则为0 否则位1
	//即0^0=0,1^0=1,1^1=0.其实也就是^1翻转,^0则不变
	//从而实现使第i+1位(即下标的第i位)翻转
}
void Outputresult( char result[]) {
	for (int i = 0; i < 5; i++) {
		for (int j = 0; j < 6; j++) {
			cout << GetBit(result[i], j);
			if (j < 5)
				cout << " ";
		}	
		cout << endl;

	}
}
int main(){
	for (int i = 0; i < 5; i++) {
		for (int j = 0; j < 6; j++) {
			int s;
			cin >> s;//暂存录入的0或1
			SetBit(orilights[i], j, s);
		}
	}
		for (int n = 0; n < 64; n++) {//开始第一行可能按的2^6=64种可能
			//因为每一行有6盏灯,每盏灯的状态只有是按或者不按两种,所以第一行按的情况有2^64种可能
			int switchs = n;//暂存第一行的开关结果
			memcpy(lights, orilights, sizeof(orilights));
			//因为是枚举尝试,所以不能直接在原数据上修改,否者修改后会造成数据改变
			for (int i = 0; i < 5; i++) {
				result[i] = switchs;//存储第i行的结果
				//接下来就是使得第i行的灯起作用,此行每一盏灯的状态都得查看
				for (int j = 0; j < 6; j++) {
					if (GetBit(switchs, j)) {//如果这一行的第j位是1,说明是打开的,要进行处理
						if (j > 0)//如果左边的灯存在
							FlipBit(lights[i], j - 1);//翻转左边的灯
						FlipBit(lights[i], j);//翻转本位灯,相当于按下
						if (j < 5)
							FlipBit(lights[i], j + 1);//翻转右边的灯

					}//以上都是改变本行的灯,下面是改变下一行的灯
				}
				if (i < 4)
					lights[i + 1] ^= switchs;
				//因为switch是暂存本行的灯的状态,^=即遇到1则翻转,0则不翻转
				//所以直接让下一行和本行进行按位异或,直接改变下一行的结果
				//至此处理完了本行和下一行的灯	接下来更行switchs继续循环
				switchs = lights[i];
			//至此确保了前四行灯都熄灭了,如今只需要判断最后一行是否为0即可
			}	if (lights[4] == 0) {
				Outputresult(result);
				break;
		}

}


	return 0;
}

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Prudento

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

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

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

打赏作者

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

抵扣说明:

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

余额充值