POJ1222熄灯问题(枚举练习题)

题目描述

这道题来自 POJ1222 ,题目描述如下:

有一个由按钮组成的矩阵,其中每行有 6 个按钮,共 5 行。每个按钮的位置上有一盏灯。当按下一个按钮后,该按钮以及周围位置(上边、下边、左边、右边)的灯都会改变一次。即,如果灯原来是点亮的,就会被熄灭;如果灯原来是熄灭的,则会被点亮。在矩阵角上的按钮改变 3 盏灯的状态;在矩阵边上的按钮改变 4 盏灯的状态;其他的按钮改变 5 盏灯的状态。

熄灯规则示例图

图 1 熄灯规则示例图

在上图中,左边矩阵中用 X 标记的按钮表示被按下,右边的矩阵表示灯状态的改变。对矩阵中的每盏灯设置一个初始状态。请你按按钮,直至每一盏等都熄灭。与一盏灯毗邻的多个按钮被按下时,一个操作会抵消另一次操作的结果。在下图中,第 2 行第 3、5 列的按钮都被按下,因此第 2 行、第 4 列的灯的状态就不改变。

重复操作同一盏灯效果抵消

图 2 重复操作同一盏灯效果抵消

请你写一个程序,确定需要按下哪些按钮,恰好使得所有的灯都熄灭。根据上面的规则,我们知道:

  1. 第 2 次按下同一个按钮时,将抵消第 1 次按下时所产生的结果。因此,每个按钮最多只需要按下一次;
  2. 各个按钮被按下的顺序对最终的结果没有影响;
  3. 对第 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 逐层熄灯示例

如图 3 所示,我们可以通过按下第 2 行的第 2,3,5 个灯泡来灭掉第 1 行中的第 2,3,5 个灯泡(图中重点在于第 1 行,不考虑按下灯后对其他行的影响)。

按照这个思路,我们试着逐行进行熄灯:

逐层熄灯全过程

图 4 逐层熄灯全过程

如图 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 的问题,但是用位运算来实现我们想要的功能也是一个难题,下面让我们来一步步拆解:

  1. 如何获取一个二进制数的第 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;
}
  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);
    }
}
  1. 如何将一个二进制数的第 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();
        }
    }
}

使用以上代码可以直接 ac

图 5 使用以上代码可以直接 ac

我的 个人博客 上线了!
博客中除了同步我在 CSDN 上发布的博文,还包括自己的学习笔记总结,欢迎到访 ~

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AmbitiousJun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值