题目描述
这道题来自 POJ1222 ,题目描述如下:
有一个由按钮组成的矩阵,其中每行有 6 个按钮,共 5 行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上边、下边、左边、右边)的灯都会改变一次。即,如果灯原来是点亮的,就会被熄灭;如果灯原来是熄灭的,则会被点亮。在矩阵角上的按钮改变 3 盏灯的状态;在矩阵边上的按钮改变 4 盏灯的状态;其他的按钮改变 5 盏灯的状态。
在上图中,左边矩阵中用 X 标记的按钮表示被按下,右边的矩阵表示灯状态的改变。对矩阵中的每盏灯设置一个初始状态。请你按按钮,直至每一盏等都熄灭。与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果。在下图中,第 2 行第 3、5 列的按钮都被按下,因此第 2 行、第 4 列的灯的状态就不改变。
请你写一个程序,确定需要按下哪些按钮,恰好使得所有的灯都熄灭。根据上面的规则,我们知道:
- 第 2 次按下同一个按钮时,将抵消第 1 次按下时所产生的结果。因此,每个按钮最多只需要按下一次;
- 各个按钮被按下的顺序对最终的结果没有影响;
- 对第 1 行中每盏点亮的灯,按下第 2 行对应的按钮,就可以熄灭第 1 行的全部灯。如此重复下去,可以熄灭第 1、2、3、4 行的全部灯。同样,按下第 1、2、3、4、5 列的按钮,可以熄灭前 5 列的灯。
输入
5 行组成,每一行包括 6 个数字( 0 或 1 )。相邻两个数字之间用单个空格隔开。0 表示灯的初始状态是熄灭的,1 表示灯的初始状态是点亮的。
输出
5 行组成,每一行包括 6 个数字( 0 或 1 )。相邻两个数字之间用单个空格隔开。其中的 1 表示需要把对应的按钮按下,0 则表示不需要按对应的按钮。
样例输入
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
样例输出
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
题目分析
题目的意思大致如下:
-
给定一个固定行和固定列的矩阵( 5 × 6 ),矩阵中的元素只包含 0 和 1,分别代表
熄灯
和亮灯
。 -
要求输出一个同样规格的矩阵,矩阵中的元素也是由 0 和 1 构成,分别代表
不按开关
和按开关
。 -
一盏灯最多只需要按 1 次就够。
-
重复操作同一盏灯效果会抵消,即一盏灯的状态只会在
亮
和灭
之间来回切换。
因为按下一盏灯的时候,会影响当前灯及其周围的灯,故我们可以采取 根据层数从上往下逐行按灯
的方法来熄灯。具体一点,我们可以通过按第 2 行的灯来熄灭第 1 行的所有灯,再按第 3 行的灯来熄灭第 2 行的所有灯…
如图 3 所示,我们可以通过按下第 2 行的第 2,3,5 个灯泡来灭掉第 1 行中的第 2,3,5 个灯泡(图中重点在于第 1 行,不考虑按下灯后对其他行的影响)。
按照这个思路,我们试着逐行进行熄灯:
如图 4 所示,我们按照逐层熄灯的思想,成功地灭掉了前 4 层的灯,只剩第 5 层(最后 1 层),这一层我们是无论如何都灭不掉的(会导致其他的已经别灭掉的灯重新亮回来)。
再仔细想想,第 1 层的灯我们是不是自始至终都没有去碰过?
所以,为了按照图 4 的流程执行结束的时候,第 5 层的灯也能够恰巧被熄灭,我们可以试图找到一种方案来按第 1 层的灯。从以上的分析来看,用不同方案去按第 1 层的灯会对应到一种不同的第 5 层灯的最终结果,那么我们只需要 枚举 第 1 层中所有的可能的按法,就必定能够找出一种方案使得第 5 层灯最终的状态是全部熄灭(题目输入样例满足有可解方案的前提下)。
那么,下一个问题来了,该如何枚举?
普通的枚举是通过 嵌套 for 循环
的方式列举每个元素的所有可能取值,比如我要枚举 直角坐标系中所有的满足 0 <= x,y <= 20 的整数坐标点
,代码可以这么写:
for (int x = 0; x <= 20; x++) {
for (int y = 0; y <= 20; y++) {
// ...
}
}
但是对于这道题来说,第 1 层一共有 6 个元素,难道我们要写一个 6 层嵌套的 for 循环???
这显然是不合理的,再细想一下,每一个元素只有 2 种可能的取值,我们其实可以使用 位运算
来代替嵌套 for 循环。
第 1 层的 可按方案
一共有 2 ^ 6 = 64
这么多种,那么我们可以通过使用 1 层 for 循环遍历 int
类型的数 0 ~ 63
来代表每一种不同的情况,对于每一个数,我们只取它的二进制位的后 6 位即可。
虽说位运算能解决多重嵌套 for 的问题,但是用位运算来实现我们想要的功能也是一个难题,下面让我们来一步步拆解:
- 如何获取一个二进制数的第 i 位(从右往左,从 0 开始计算)的数值?
我们只需要将第 i 位一直 右移
,移动到第 1 位,然后再将得到的数值与 1 做 与 &
运算即可。
跟 1 做 “与” 运算有两个作用,一个是将除了第 1 位之外的所有二进制位抹零,另一个是保留第 1 位原有的值(
1 & 0 == 0, 1 & 1 == 1
)
代码如下:
/**
* 获取整形数二进制的某一位
* @param num 要进行运算的整形数
* @param i 获取第几位(从右往左,从 0 开始数)
* @return 第 i 位的信息,0 | 1
*/
public static int getBit(int num, int i) {
return (num >> i) & 1;
}
- 如何设置一个二进制数的第 i 位为指定值(0 或 1)?
参考获取值的思想,我们可以反着来。
比如我要将某个数 x 的第 3 位的值设置成 1,只需要将 1 左移
3 位,再与 x 做 或 |
运算即可。
1 左移 3 位之后,即得到
001000
,将这个数与 x 做 “或” 运算,数 x 的第 3 位的值会被修改成 1,而 x 的其他位置因为是跟 0 “或”,所以会保持原有数值不变
再比如我要将某个数 x 的第 3 位的值设置成 0,还是需要将 1 左移
3 位,然后 取反 ~
,再与 x 做 与 &
运算即可。
代码如下:
/**
* 设置整形数二进制的某一位
* @param num 要进行运算的整形数
* @param i 操作第几位(从右往左,从 0 开始数)
* @param val 要设置的数值,0 | 1
* @return 设置完成之后的值
*/
public static int setBit(int num, int i, int val) {
if (val == 1) {
return num | (1 << i);
} else {
return num & ~(1 << i);
}
}
- 如何将一个二进制数的第 i 位进行翻转(0 变 1,1 变 0)?
对于这道题来说,亮灯 / 熄灯这个过程就对应到二进制位的翻转操作。
还是一样,我们将 1 左移
到 i 这个位置上,然后跟原数进行 异或 ^
操作即可。
0 跟任何数 “异或” 会保留原数的值,而 1 跟 0,1 进行异或的话会让原数进行翻转
代码如下:
/**
* 将整形数二进制中的某一位进行翻转,0 翻成 1,1 翻成 0
* @param num 要操作的整形数
* @param i 要翻转第几位(从右往左,从 0 开始数)
* @return 翻转之后的结果
*/
public static int flipBit(int num, int i) {
return num ^ (1 << i);
}
好了,解决这 3 个位运算操作之后,我们就直接遍历第 1 层的 64 种情况,然后再按照逐层熄灯的思路去模拟就好了。
比如我当前遍历到 0 ~ 63
中的数字 17
,对应的二进制位为 010001
,那么我就按下第 1 层灯泡的第 0,4 个灯泡(从 0 开始数),然后根据按完之后的情形,按照逐层熄灯的思路按顺序按第 2 ~ 5 层,同时将每一层按了哪个灯记录下来,当每一层都按完之后,判断是否所有灯都熄灭了,是的话就将按灯记录输出出去就 ok 了。
还有一点要提一下的就是,我们操作二进制位的时候,是 从右往左
来看的,而这个灯泡矩阵是 从左往右
看的(从右往左来看进行编码应该也没什么太大的问题,只不过我是从我代码的角度来说),所以在 coding 的时候需要做一下索引变换,即第 i
个灯泡,对应第 cols - i - 1
(在本题中,cols 恒等于 6)位二进制位。
代码(Java)
input()
和printBitMatrix()
两个方法分别用于处理题目的 I / O- 全局变量
originLights
存放的是程序最初输入的矩阵,只用来拷贝,不做修改 flipLights()
是一个比较业务性的整体封装的方法,用于翻转指定灯和周围灯,顺便做边界判定。
import java.util.Scanner;
/**
* 熄灯问题 - POJ1222
* 问题描述见官网:http://bailian.openjudge.cn/practice/2811/
*/
public class Program2 {
/**
* 题目输入的数组
*/
public static int[] originLights;
public static void main(String[] args) {
// 读取输入
originLights = input();
int cols = 6;
// 使用位运算,枚举第一行中 64 种情况
// printBitMetrix(originLights, 6); // 验证输入的矩阵是否正确
out:
for (int i = 0; i < 64; i++) {
// 1 构造一个与 originLights 相同的临时矩阵
int[] tempMetrix = new int[5];
for (int j = 0; j < 5; j++) {
tempMetrix[j] = originLights[j];
}
// 2 准备一个存放答案的矩阵
int[] ans = new int[5];
// 3 设置矩阵的第一行
ans[0] = i;
for (int j = 0; j < cols; j++) {
// 如果当前位是 1,那就翻转灯泡
if (getBit(i, cols - j - 1) == 1) {
flipLights(tempMetrix, 0, cols - j - 1, cols);
}
}
// 4 设置矩阵的第 2 - 5 行
for (int j = 1; j < tempMetrix.length; j++) {
// 看上一行哪个灯泡还是亮着的就翻当前行的对应列
for (int k = 0; k < cols; k++) {
if (getBit(tempMetrix[j - 1], cols - k - 1) == 1) {
flipLights(tempMetrix, j, cols - k - 1, cols);
ans[j] = setBit(ans[j], cols - k - 1, 1);
}
}
}
// 5 判断如果每一行都被熄灭了,就说明找到可行解
for (int j = 0; j < tempMetrix.length; j++) {
if (tempMetrix[j] != 0) {
continue out;
}
}
printBitMetrix(ans, cols);
return;
}
}
/**
* 翻转灯泡,被翻转的灯泡的周围(上下左右,边界内)也会被翻转
* @param metrix 要操作的矩阵
* @param i 要翻转哪一行的灯泡
* @param j 要翻转哪一列的灯泡
* @param cols 一共有多少列
*/
public static void flipLights(int[] metrix, int i, int j, int cols) {
int rows = metrix.length;
// 当前
if (0 <= i && i < rows && 0 <= j && j < cols) {
metrix[i] = flipBit(metrix[i], j);
}
// 上
if (0 <= i - 1 && i - 1 < rows && 0 <= j && j < cols) {
metrix[i - 1] = flipBit(metrix[i - 1], j);
}
// 左
if (0 <= i && i < rows && 0 <= j - 1 && j - 1 < cols) {
metrix[i] = flipBit(metrix[i], j - 1);
}
// 下
if (0 <= i + 1 && i + 1 < rows && 0 <= j && j < cols) {
metrix[i + 1] = flipBit(metrix[i + 1], j);
}
// 右
if (0 <= i && i < rows && 0 <= j + 1 && j + 1 < cols) {
metrix[i] = flipBit(metrix[i], j + 1);
}
}
/**
* 将整形数二进制中的某一位进行翻转,0 翻成 1,1 翻成 0
* @param num 要操作的整形数
* @param i 要翻转第几位(从右往左,从 0 开始数)
* @return 翻转之后的结果
*/
public static int flipBit(int num, int i) {
return num ^ (1 << i);
}
/**
* 设置整形数二进制的某一位
* @param num 要进行运算的整形数
* @param i 操作第几位(从右往左,从 0 开始数)
* @param val 要设置的数值,0 | 1
* @return 设置完成之后的值
*/
public static int setBit(int num, int i, int val) {
if (val == 1) {
return num | (1 << i);
} else {
return num & ~(1 << i);
}
}
/**
* 获取整形数二进制的某一位
* @param num 要进行运算的整形数
* @param i 获取第几位(从右往左,从 0 开始数)
* @return 第 i 位的信息,0 | 1
*/
public static int getBit(int num, int i) {
return (num >> i) & 1;
}
/**
* 从控制台中读取输入
* @return 读取完成的矩阵
*/
public static int[] input() {
Scanner scanner = new Scanner(System.in);
int[] nums = new int[5];
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 6; j++) {
// 将矩阵中的每一行的信息存放在 int 类型的二进制位上
nums[i] = setBit(nums[i], 5 - j, scanner.nextInt());
}
}
scanner.close();
return nums;
}
/**
* 打印位矩阵,即列信息存放在 int 类型数的二进制位上
*/
public static void printBitMetrix(int[] bitMetrix, int cols) {
int m = bitMetrix.length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < cols; j++) {
System.out.print(getBit(bitMetrix[i], cols - j - 1));
if (j != cols - 1) {
System.out.print(" ");
}
}
System.out.println();
}
}
}
我的 个人博客 上线了!
博客中除了同步我在 CSDN 上发布的博文,还包括自己的学习笔记总结,欢迎到访 ~