程序设计与算法第一周 枚举
题目地址 熄灯问题
用二进制数进行枚举以及位运算的巧用
[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个开关, 那么状态数是230 , 太多, 会超时。
关键问题在于如何减少枚举的状态数目呢?
基本思路: 如果存在某个局部, 一旦这个局部的状态被确定,那么剩余其他部分的状态只能是确定的一种, 或者不多的n种, 那么就只需枚举这个局部的状态即可。经过观察, 发现第1行就是这样的一个 “局部”,因为第1行的各开关状态确定的情况下, 这些开关作用过后, 将导致第1行某些灯是亮的, 某些灯是灭的,而要熄灭第1行某个亮着的灯(假设位于第i列), 那么唯一的办法就是按下第2行第i列的开关(因为第1行的开关已经用过了, 而第3行及其后的开关不会影响到第1行)为了使第1行的灯全部熄灭, 第2行的合理开关状态就是唯一的,第2行的开关起作用后,为了熄灭第2行的灯, 第3行的合理开关状态就也是唯一的,以此类推, 最后一行的开关状态也是唯一的,只要第1行的状态定下来, 记作A, 那么剩余行的情况就是确定唯一的了。
推算出最后一行的开关状态, 然后看看最后一行的开关起作用后,最后一行的所有灯是否都熄灭:
- 如果是, 那么A就是一个解的状态
- 如果不是, 那么A不是解的状态, 第1行换个状态重新试试
- 只需枚举第1行的状态, 状态数是26=64
- 枚举第一列, 状态数是25=32
由于结果矩阵中的数据非0即1,为了节约空间,可使用一个char类型的数组(5),每一个char类型对应着8位(>6),使用位运算。让一个整数从$0 $到 2k−1遍历,即0 $到 $2k−1之间的任一个整数对应着一种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;
}