熄灯问题
有一个由按钮组成的矩阵, 其中每行有6个按钮, 共5行– 每个按钮的位置上有一盏灯– 当按下一个按钮后,,该按钮以及周围位置(上边, 下边,左边, 右边)的灯都会改变状态。
– 如果灯原来是点亮的, 就会被熄灭
– 如果灯原来是熄灭的, 则会被点亮
• 在矩阵角上的按钮改变3盏灯的状态
• 在矩阵边上的按钮改变4盏灯的状态
• 其他的按钮改变5盏灯的状态
与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果
给定矩阵中每盏灯的初始状态,求一种按按钮方案,使得所有的灯都熄灭
输入:
– 第一行是一个正整数N, 表示需要解决的案例数
– 每个案例由5行组成, 每一行包括6个数字
– 这些数字以空格隔开, 可以是0或1
– 0 表示灯的初始状态是熄灭的
– 1 表示灯的初始状态是点亮的
输出:
– 对每个案例, 首先输出一行,
输出字符串 “PUZZLE #m”, 其中m是该案例的序号
– 接着按照该案例的输入格式输出5行
• 1 表示需要把对应的按钮按下
• 0 0 表示不需要按对应的按钮
• 每个数字以一个空格隔开
样例输入
2
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
0 0 1 0 1 0
1 0 1 0 1 1
0 0 1 0 1 1
1 0 1 1 0 0
0 1 0 1 0 0
样例输出
PUZZLE #1
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
PUZZLE #2
1 0 0 1 1 1
1 1 0 0 0 0
0 0 0 1 0 0
1 1 0 1 0 1
1 0 1 1 0 1 31
解题分析
• 第2次按下同一个按钮时,
将抵消第1次按下时所产生的结果
每个按钮最多只需要按下一次
• 各个按钮被按下的顺序对最终的结果没有影响
• 对第1行中每盏点亮的灯, 按下第2行对应的按钮, 就可以熄灭第1行的全部灯
• 如此重复下去, 可以熄灭第1, 2, 3, 4行的全部灯
• 第一想法: 枚举所有可能的按钮(开关)状态, 对每个状态计算一下最后灯的情况, 看是否都熄灭
– 每个按钮有两种状态(按下或不按下)
– 一共有30个开关, 那么状态数是2 30 , 太多, 会超时
• 如何减少枚举的状态数目呢?
基本思路: 如果存在某个局部, 一旦这个局部的状态被确定, 那么剩余其他部分的状态只能是确定的一种, 或者不多的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
下面有两种方案,第一种方案是本人的笨的办法,使用的Iint型二维数组,很浪费空间,为了减少每一步边界断影响运行效率,数组的0行0列,尾行尾列都不管,只检查中间要用到的行列情况,需要将数组开空间扩大。
#include
using namespace std;
int a[7][8]; //初始状态 ,数组开大一点,头尾行列都不管,省去每执行一步都去判断边界以提高运行效率;
int b[7][8]; //用于存储变化中的状态
int c[7][8]; //用于最后结束输出;
void fun(int n) {//第一行的第N个操作状态
for(int i=1;i<7;++i)
{
c[1][i]=n%2;
n=n/2;
}
}
void set(int i,int j){//该行列灯被按下后相邻各灯的变化
b[i][j]^=1;
b[i+1][j]^=1;
b[i-1][j]^=1;
b[i][j+1]^=1;
b[i][j-1]^=1;
}
int main(){
int y=0;
// freopen("c:\\zdwt.txt","r",stdin);//读入测试数据
cin>>y;
for(int t=1;t<=y;++t){
for(int i=1;i<6;++i){
for(int j=1;j<7;++j){
cin>>a[i][j];
}
}
for(int n=0;n<64;n++){
for(int i=1;i<6;++i)//把灯的初始状态放到b,且初始化C
for(int j=1;j<7;++j)
{ b[i][j]=a[i][j]; c[i][j]=0; }
fun(n);//生成第0行的第n种方案的操作
//对第1行进行操作
for(int j=1;j<7;++j) {
if(c[1][j]) set(1,j);//该灯按下了就执行
}
//操作第2~5行
for(int i=2;i<6;++i){
for(int j=1;j<7;++j)
if(b[i-1][j]) { set(i,j); c[i][j]=1; }
}
//检验第5行是否已经完全关闭
for(int j=1;j<7;++j){
if(b[5][j]) break;
if(j==6){
cout<<"PIZZLE#"<<t<<endl;
for(int i =1;i<6;++i){//输出结果
for(int j=1;j<7;++j){
cout<<c[i][j]<<" ";
}
cout<<endl;
}
break;
}
}
}
}
return 0;
}
再送上一种效率高的方案,是北京大学郭炜都是的《程序设计与算法》中的例题,因为开关状态只有0和1,每个开关只需1比特空间,只需要一个char型数据就可以存一行开关,很节约空间,用位运算。
#include
#include
#include
#include
using namespace std;
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 Flip(char & c, int i) {
// 将c 的第i 位为取反
c ^= ( 1 << i);
}
void OutputResult(int t,char result[]) // 输出结果
{
cout << "PUZZLE #" << t << endl;
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() {
char oriLights[5]; // 最初灯矩阵,一个比特表示一盏灯
char lights[5]; // 不停变化的灯矩阵
char result[5]; // 结果开关矩阵
char switchs; // 某一行的开关状态
int T;
cin >> T;
for( int t = 1; t <= T; ++ t) {
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(t,result);
break;
}
} // for( int n = 0; n < 64; n ++ )
}
return 0;
}