熄灯问题

程序设计与算法第一周 枚举

题目地址 熄灯问题

用二进制数进行枚举以及位运算的巧用

[I/O分析]

输入:

  • 第一行是一个正整数N, 表示需要解决的案例数
  • 每个案例由5行组成, 每一行包括6个数字
  • 这些数字以空格隔开, 可以是0或1
  • 0 表示灯的初始状态是熄灭的
  • 1 表示灯的初始状态是点亮的

输出:

  • 对每个案例, 首先输出一行,输出字符串 “PUZZLE #m”, 其中m是该案例的序号
  • 接着按照该案例的输入格式输出5行
    • 1 表示需要把对应的按钮按下
    • 0 表示不需要按对应的按钮
    • 每个数字以一个空格隔开

由于同一个开关按下两次会被抵消,所以对于同一个开关而言,要么按一次,要么不按。

[解题分析]

第2次按下同一个按钮时,将抵消第1次按下时所产生的结果,因此每个按钮最多只需要按下一次
各个按钮被按下的顺序对最终的结果没有影响
对第1行中每盏点亮的灯, 按下第2行对应的按钮, 就可以熄灭第1行的全部灯
如此重复下去, 可以熄灭第1, 2, 3, 4行的全部灯

第一想法: 枚举所有可能的按钮(开关)状态, 对每个状态计算一下最后灯的情况, 看是否都熄灭

  • 每个按钮有两种状态(按下或不按下)
  • 一共有30个开关, 那么状态数是 2 30 2^{30} 230 , 太多, 会超时。

关键问题在于如何减少枚举的状态数目呢?
基本思路: 如果存在某个局部, 一旦这个局部的状态被确定,那么剩余其他部分的状态只能是确定的一种, 或者不多的n种, 那么就只需枚举这个局部的状态即可。经过观察, 发现第1行就是这样的一个 “局部”,因为第1行的各开关状态确定的情况下, 这些开关作用过后, 将导致第1行某些灯是亮的, 某些灯是灭的,而要熄灭第1行某个亮着的灯(假设位于第i列), 那么唯一的办法就是按下第2行第i列的开关(因为第1行的开关已经用过了, 而第3行及其后的开关不会影响到第1行)为了使第1行的灯全部熄灭, 第2行的合理开关状态就是唯一的,第2行的开关起作用后,为了熄灭第2行的灯, 第3行的合理开关状态就也是唯一的,以此类推, 最后一行的开关状态也是唯一的,只要第1行的状态定下来, 记作A, 那么剩余行的情况就是确定唯一的了。

推算出最后一行的开关状态, 然后看看最后一行的开关起作用后,最后一行的所有灯是否都熄灭:

  • 如果是, 那么A就是一个解的状态
  • 如果不是, 那么A不是解的状态, 第1行换个状态重新试试
  • 只需枚举第1行的状态, 状态数是 2 6 = 64 2^6 = 64 26=64
  • 枚举第一列, 状态数是 2 5 = 32 ​ 2^5 = 32​ 25=32

由于结果矩阵中的数据非0即1,为了节约空间,可使用一个char类型的数组(5),每一个char类型对应着8位(>6),使用位运算。让一个整数从$0 $到 2 k − 1 2^k-1 2k1遍历,即0 $到 $ 2 k − 1 2^k-1 2k1之间的任一个整数对应着一种 k k k个比特的组合。

#include <iostream>
#include <cstring>
using namespace std;

//取c的第i位
int GetBit(char c, int i)
{
	return (c >> i) & 1;  //先右移再与1相与
}

//设置c的第i位为v
void SetBit(char &c, int i, int v)
{
	if(v)
		c |= (1 << i);  //将第i位设置为1
	else
		c &= ~(1 << i); // 第i位设置为0,其余为1,再进行与运算
}

//将c的第i位取反
void Flip(char &c, int i)
{
	c ^= (1 << i);	//第i位与1异或进行取反
}

void OutputResult(char result[]) //输出结果
{
	for(int i=0; i<5; i++) //5行
	{
		for(int j=0; j<6; j++)
		{
			cout << GetBit(result[i], j);	//得到第i行第j列元素
			if(j < 5)
				cout << " "; // 输出5个空格最后一个不输出空格
		}
		cout << endl;
	}
}

int main()
{
	char oriLights[5];		//最初灯矩阵,一个比特表示一盏灯,一个数组元素表示一行6个
	char lights[5];			//不停变化的灯矩阵
	char result[5];			//结果开关矩阵
	char switchs;			//某一行的开关状态
	memset(oriLights, 0, sizeof(oriLights));	//清零
	for(int i=0; i<5; i++)	//读入灯的初始状态
	{
		for(int j=0; j<6; j++)
		{
			int s;
			cin >> s;
			SetBit(oriLights[i], j, s);
		}
	}

	for(int n=0; n<64; n++)	//遍历首行开关的64种状态
	{
		memcpy(lights, oriLights, sizeof(oriLights));	//复制,每一次循环在原始数组上操作
		switchs = n;	//第i行的开关状态
		for(int i=0; i<5; i++)
		{
			result[i] = switchs;	//第i行的开关方案
			for(int j=0; j<6; j++)
			{
				if(GetBit(switchs, j))
				{
					if(j > 0)
						Flip(lights[i], j-1);	//改左灯
					Flip(lights[i], j);		//改开关位置的灯
					if(j < 5)
						Flip(lights[i], j+1);	//改右灯
				}
			}
			if(i < 4)
				lights[i+1] ^= switchs;	//改下一行的灯
			switchs = lights[i];	//第i+1行开关方案和第i行灯的情况相同
		}
		if(lights[4] == 0)
		{
			OutputResult(result);
			break;
		}
	}
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值