枚举算法之熄灯问题

有一个有按钮组成的矩阵 ,其中每行有6个按钮,共5行
每个按钮的位置上有一盏灯
当按下一个按钮后,该按钮以及周边位置(上边、下边、左边、右边)的灯都会改变
如果灯原来时点亮的,就会被熄灭  反之亦然

情况分析

***如果在矩阵的 角上 的按钮 改变 3 盏灯
如果在矩阵的边上  改变 4 盏灯
其余情况 这改变 5 盏灯***
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190404214605582.png)

与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果
给定矩阵中每盏灯的初始状态,求一种按按钮方案是的所有的灯都熄灭

输入
第一行时一个正整数,表示需要解决的按例数
每个案例有5行组成,每一行包括6个数字
这些数值以空格隔开,可以是0 或 1
0表示熄灭状态
1 表示点亮状态

输出:
每个案例先输出一行 :
输出字符串“PUZZLE#m” ,其中表示该案例的序号
接着按照该方案的输入格式 输出5行
1表示要把对应的按下
0表示不需要按下对应的按钮
样例输入     输入样例
2
0 1 1 0 1 0     PUZZLE #1
1 0 0 1 1 1     1 0 1 0 0 1
0 0 1 0 0 1     1 1 0 1 0 1
1 0 0 1 0 1     0 0 1 0 1 1
0 1 1 1 0 0     1 0 0 1 0 0
0 0 1 0 1 0     0 1 0 0 0 0
1 0 1 0 1 1     PUZZLE #2
0 0 1 0 1 1     1 0 0 1 1 1
1 0 1 1 0 0     1 1 0 0 0 0
0 1 0 1 0 0     0 0 0 1 0 0
1 1 0 1 0 1
1 0 1 1 0 1

解题分析

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

**第一种想法:**
枚举所有可能的按钮状态,对每个状态计算一下最后灯的情况,看看是否都熄灭
每个按钮有两种状态(按下或不按下)
总共有30个开关,那么状态数是2^30,太多,会超时

因此需要减少枚举的状态数目
===>如果存在某个局部,一旦这个局部的状态被确定,那么剩余其他状态只能是确定的一种,或者不多的n种
那么就是只需要枚举这个局部状态即可

经发现, 第一行就是这样的 局部
因为 第1行的各个开关的状态确定的情况下,这些开关作用过后将导致第一行某些灯是亮的,某些灯是 灭的
要熄灭第一行 某个亮着的灯(假设唯一第i列),那么唯一的办法就是按下第二行第i列的开关(因为第1 行的开关已经用过,
而第3 行的开关不会影响到第1行)
为使第1行的灯全部熄灭 ,第2行的合理开关状态就是唯一的

同理,依次类推,直至最后一行,最后一行的状态也是唯一的

只需要将第一行的状态确定下来,记作A ,那么剩余行的情况就是确定唯一的
直到推出最后一行的开关状态,然后看看最后一行的开关起作用后 最后一行所有灯是否熄灭

如果是,A就是一个解的状态
若果不是,第一行换个状态重新在试试

只需要枚举第一行 的状态 , 状态数是2^6 = 64;

因为最近开始学习Java,就用Java方式实现:

public class LightOutProblem {
	public static byte getBit(byte c, int i) {
		return (c >> i) & 1;
	}
	public static byte setBit(byte c, int i, int v) { // 设置c的第i位为v
		if (v == 1) 	 //置1
			c |= (1 << i);   
				//说明
				// , i =2,       	   c=00101 1 10 
					 //00000001 <<2  ==> 00000 1 00
			 //c |= (1 << i)  = 	   	   00101 1 10   
				//结果c=00101 1 10   第二位  置  1
		else  	//    置0
			c &= ~(1 << i);
		 		//  i =2,     			   c=00101 1 10 
						//00000001 <<2  ==> 00000 1 00
				// ~(1 << i)     ==>       11111 0 11
				// c &= ~(1 << i)  ==> 00101 0 10  第二位    置  0
		return c;
	}
	public static byte flip(byte c, int i) { // 将c的第i位为取反
		c ^= (1 << i);   
		// 举个例子
		//c = 00101110
		//00000001 <<2  ==> 00000100
	    //结果c=00101010   第二位从2 置为 0
		return c;
	}
	public static void outputResult(int t, byte[] result) {// 格式化输出结果
		System.out.println("PUZZLE #" + t);
		for (int i = 0; i < 5; ++i) {
			for (int j = 0; j < 6; ++j) {
				System.out.print(getBit(result[i], j));
				if (j < 5)
					System.out.print(" ");
			}
			System.out.println();
		}
	}
	public static void main(String[] args) {	
		//因考虑到数据在不超过64 因此byte就足够  并 以  1位   表示一盏灯
		byte[] oriLights; // 最初灯矩阵,一个比特表示一盏灯
		byte[] lights; // 不停变化的灯矩阵
		byte[] result; // 结果开关矩阵
		byte switchs;// 某一行的开关状态
		int T;
		Scanner scan = new Scanner(System.in);
		T = scan.nextInt();
		for (int t = 1; t <= T; ++ t) {
			oriLights = new byte[] { 0, 0, 0, 0, 0 }; //初始化所有数组
			lights = new byte[] { 0, 0, 0, 0, 0 };
			result = new byte[] { 0, 0, 0, 0, 0 };
			for (int i = 0; i < 5; i++) { // 读入最初灯状态
				for (int j = 0; j < 6; j++) {
					byte s;
					s = scan.nextByte();
					oriLights[i] = setBit(oriLights[i], j, s);
				}
			}
			for(int n = 0; n < 64; n ++) { //遍历首行开关的 64 种情况
				lights = oriLights.clone();
				switchs = (byte)n; //第i行开关状态
				for(int i =0; i < 5; i++) {
					result[i] = switchs;
					for(int j = 0; j < 6; j ++) {
						if(getBit(switchs,j) == 1) {
							if(j > 0) {
								lights[i] = flip(lights[i], j-1);//置反前一位
							}
							lights[i] = flip(lights[i], j);//置反当前位
							if(j < 5) {
								lights[i] = flip(lights[i], j+1);//置反后一位
							}
						}
					}
					if(i < 4) {
						lights[i + 1] ^= switchs; //反转下一行
					}
					switchs= lights[i];  //将上一行的结过转到下一行
				}
				if (lights[4] == 0) {//如果最后一行是0 则说明所有的灯已关闭
					outputResult(t, result);
					break;
				}
			}
		}
	}
}

总结:
1、其中使用到了对于 学生 来说非常少用的 位运算 刚开始看的时候自己也没办法理解,只好每一步每一步写出来看,明白了此题也充分理解位运算;
2、在这个问题中,充分运用到枚举的算法,加上合适的分析,减少了枚举的规模。
3.在Java中,不像C语言那样,可以传入地址,也充分体现Java基本类型中,都是值传递的特性

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值