作为枚举的最后一个例子,我们来讲解一个稍微复杂点的例子,不要害怕,慢慢看下去,其实并不难。

1、题目描述

例题:熄灯问题

  • 有一个由按钮组成的矩阵 , 其中每行有 6个按钮 , 共5行
  • 每个按钮的位置上有一盏灯
  • 当按下一个按钮后 , 该按钮以及周围位置 (上边 , 下边 , 左边, 右边)的灯都会改变状态
    4-熄灯问题_算法基础修炼指南
  • 如果灯原来是点亮的 , 就会被熄灭
  • 如果灯原来是熄灭的 , 则会被点亮
    • 在矩阵角上的按钮改变3盏灯的状态
    • 在矩阵边上的按钮改变4盏灯的状态
    • 其他的按钮改变5盏灯的状态
      4-熄灯问题_算法基础_02
  • 与一盏灯毗邻的多个按钮被按下时 ,一个操作会抵消另一个操作的结果
  • 给定矩阵中每盏灯的初始状态,求一种按按钮方案,使得所有的灯都熄灭

输入 :

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

输出 :

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

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、总结

  1. 局部进行枚举
  2. 用二进制数进行枚举
  3. 位运算的使用