作为枚举的最后一个例子,我们来讲解一个稍微复杂点的例子,不要害怕,慢慢看下去,其实并不难。
1、题目描述
例题:熄灯问题
- 有一个由按钮组成的矩阵 , 其中每行有 6个按钮 , 共5行
- 每个按钮的位置上有一盏灯
- 当按下一个按钮后 , 该按钮以及周围位置 (上边 , 下边 , 左边, 右边)的灯都会改变状态
- 如果灯原来是点亮的 , 就会被熄灭
- 如果灯原来是熄灭的 , 则会被点亮
- 在矩阵角上的按钮改变3盏灯的状态
- 在矩阵边上的按钮改变4盏灯的状态
-
其他的按钮改变5盏灯的状态
- 与一盏灯毗邻的多个按钮被按下时 ,一个操作会抵消另一个操作的结果
- 给定矩阵中每盏灯的初始状态,求一种按按钮方案,使得所有的灯都熄灭
输入 :
- 第一行是一个正整数N,表示需要解决的案例数
- 每个案例由5行组成,每一行包括6个数字
- 这些数字以空格隔开,可以是0或1
- 0表示灯的初始状态是熄灭的
- 1表示灯的初始状态是点亮的
输出 :
- 对每个案例,首先输出一行
输出字符串 “PUZZLE #m”,其中 m 是该案例的序 号 - 接着按照该案例的输入格式出5行
- 1表示需要把对应的按钮按下
- 0表示不需要按对应的按钮
- 每个数字以一个空格隔开
2、解题分析
首先对于一个按钮,只有两种状态,按下与不按,按两下和不按的结果还是一样的。每个按钮按下的顺序对最终的结果没有影响。
对于这道题,使用枚举,最初的想法就是枚举所有可能的开关状态,逐一尝试,每一种状态最终产生的结果,是否可以熄灭所有的灯,每个按钮有两个状态,按下或者不按下,这里一共有30个开关,那么状态数就是230,状态实在太多了,这么计算花费的时间太长。
基本思路:
所以一定要减少枚举的次数,如何才能减少枚举的次数呢,这里的做法是将问题分解,对整体的某个局部进行枚举,如果这个局部状态确定以后,剩下的状态是确定的,也就是只有一种或者为数不多的几种,那么我们只需要枚举这个局部的状态就可以了。
在本题中是否存在这样的局部呢,是存在的。第一行就是一个局部。
- 因为第1行的各开关状态确定情况下,这些开关作用过后,将导致第1行某些灯是亮的,某些灯是灭的
- 要熄灭第1行某个亮着的灯(假设位于第 i 列),那么唯一的办法就是按下第2行第 i 列的开关(因为第1行的开关已经用过了,而第3行及其后的开关不会影响到第1行)
- 为了使第1行的灯全部熄灭,第2行的合理开关状态就是唯一的
第2行的开关起作用后 ,
- 为了熄灭第2行的灯,第3行的合理开关状态就也是唯一
- 以此类推,最后一行的开关状态也是唯一的
- 只要第1行的状态定下来,记作A,那么剩余行的情况就是确定唯一的了
- 推算出最后一行的开关状态,然后看看最一行的开关起作用后,最后一行的所有灯是否都熄灭:
- 如果是,那么A就是一个解的状态
- 如果不是,那么A不是解的状态,第1行换个状态重新试
只需枚举第1行的状态,状态数是 26 = 64
3、程序实现
一共有5行灯,每行6个,就需要用30个数据存储这30个灯和开关的状态。我们发现灯的状态和开关状态只有1和0两个状态,就可以用二进制的数的每一位来表示一盏灯的状态。这里使用char数据类型,每一个char数据类型占1个字节,8位,足够表示一行6个灯的状态。
第一行有6个开关,每个开关都有开关两种状态,要是用6重循环来计算也可以,但是比较麻烦,这里我们也使用二进制数的每一位来表示开关的状态,从0-63,每个数对应的二进制数每一位表示一个开关状态,从0到63就遍历的第一行所有的开关方案。
#include<iostream>
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 = c |(1 << i);
else
c = c & ~(1 << i);
}
void Flip(char &c, int i) //将c的第i位取反
{
c = 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 orilight[5]; //最初的灯矩阵,一个比特代表一个灯
char light[5]; //变化中的灯矩阵
char result[5]; //结果开关状态的矩阵
char switchs; //某一行开关状态
int T;
cin >> T;
for (int t = 1; t <= T; t++)
{
memset(orilight,0,sizeof(orilight));
for (int i = 0; i < 5; i++)//读入最初灯的状态
{
for (int j = 0; j < 6; j++)
{
int s;
cin >> s;
SetBit(orilight[i], j,s);
}
}
for (int n = 0; n < 64; n++) //遍历首行开关的64种状态
{
memcpy(light, orilight, sizeof(orilight));
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(light[i], j - 1); //开关左侧灯的状态改变
Flip(light[i], j); //开关位置灯状态改变
if (j < 5)
Flip(light[i], j + 1); //开关右侧灯状态改变
}
}
if (i < 4)
light[i + 1] = light[i + 1] ^ switchs;//i+1行灯的状态改变
switchs = light[i]; //i+1行开关状态就等于i行灯的状态
}
if (light[4]==0) //最后一行灯全部熄灭了,方案可行
{
OutputResult(t, result);
break; //后面的开关方案不用尝试了
}
}
}
return 0;
}
4、总结
- 局部进行枚举
- 用二进制数进行枚举
- 位运算的使用