每日算法 — 使用java实现电路谜题:二进制枚举与高斯消元最优解

问题引入

电路谜题是一类经典的逻辑益智游戏:有 n 盏灯排成一排,每盏灯对应一个开关,但开关与灯之间并非一一对应——按下一个开关可能同时翻转多盏灯的状态。目标是找到按动哪些开关,能让所有灯从初始状态变为全部点亮,且按动次数最少。本文用Java实现两种核心解法:小规模下的二进制枚举,以及大规模下的模2高斯消元。

算法原理

问题建模

将灯的状态用 0(灭)和 1(亮)表示,按开关的操作等价于异或运算。设开关 i 对灯 j 的影响为 a[i][j](1 表示翻转,0 表示无影响),则问题转化为求解异或线性方程组:

x[0]·a[0][j] ⊕ x[1]·a[1][j] ⊕ ... ⊕ x[n-1]·a[n-1][j] = b[j]

其中 x[i] 表示是否按下开关 i,b[j] 表示灯 j 的目标状态(与初始状态异或后的需求)。

方法一:二进制枚举

当 n ≤ 20 时,可用位掩码枚举所有 2^n 种开关组合,验证是否满足所有灯的状态要求。枚举时可用位运算加速状态计算。

方法二:模2高斯消元

将方程组视为在 GF(2)(二元域)上的线性方程组,使用高斯消元求解。与普通高斯消元的区别在于:加减法均变为异或运算,无需乘除法。

消元完成后,若存在自由变量,则可通过枚举自由变量的所有组合,找出使按下开关数最少的最优解。

Java完整实现

import java.util.Arrays;

/**
 * 电路谜题求解器:二进制枚举 + 模2高斯消元
 * 目标:找到最少开关组合使所有灯点亮
 */
public class CircuitPuzzle {

    /**
     * 方法一:二进制枚举(适用于 n <= 20 的小规模问题)
     * @param n 灯/开关数量
     * @param effect effect[i] 是一个位掩码,表示开关i影响哪些灯
     * @param init 初始灯状态(位掩码,1表示灭需要被翻转)
     * @return 最少按动次数,无解返回-1
     */
    public int solveByEnumeration(int n, int[] effect, int init) {
        int target = (1 << n) - 1; // 所有灯都亮(所有位为1)
        int minPresses = Integer.MAX_VALUE;
        int bestMask = -1;

        // 枚举所有 2^n 种开关组合
        for (int mask = 0; mask < (1 << n); mask++) {
            int state = init;
            int presses = Integer.bitCount(mask);

            // 应用选中的开关
            for (int i = 0; i < n; i++) {
                if ((mask >> i & 1) == 1) {
                    state ^= effect[i];
                }
            }

            // 检查是否所有灯都亮
            if (state == target && presses < minPresses) {
                minPresses = presses;
                bestMask = mask;
            }
        }

        return minPresses == Integer.MAX_VALUE ? -1 : minPresses;
    }

    /**
     * 方法二:模2高斯消元(适用于 n 较大的情况)
     * @param a 增广矩阵,a[i][j]表示开关j对灯i的影响,最后一列是目标状态
     * @return 最少按动次数,无解返回-1
     */
    public int solveByGauss(int[][] a) {
        int n = a.length;    // 方程数(灯数)
        int m = a[0].length - 1; // 变量数(开关数)

        int row = 0;
        int[] where = new int[m];
        Arrays.fill(where, -1);

        // 高斯消元主过程
        for (int col = 0; col < m && row < n; col++) {
            // 找当前列中第一个值为1的行
            int sel = row;
            for (int i = row; i < n; i++) {
                if (a[i][col] == 1) {
                    sel = i;
                    break;
                }
            }
            if (a[sel][col] == 0) continue; // 该列无主元

            // 交换当前行与选中的行
            int[] temp = a[sel];
            a[sel] = a[row];
            a[row] = temp;

            where[col] = row;

            // 消去其他行中该列的1(模2下的高斯消元)
            for (int i = 0; i < n; i++) {
                if (i != row && a[i][col] == 1) {
                    for (int j = col; j <= m; j++) {
                        a[i][j] ^= a[row][j];
                    }
                }
            }
            row++;
        }

        // 检查是否有矛盾方程 0 = 1
        for (int i = row; i < n; i++) {
            if (a[i][m] == 1) return -1; // 无解
        }

        // 统计自由变量数量
        int freeVars = 0;
        for (int i = 0; i < m; i++) {
            if (where[i] == -1) freeVars++;
        }

        // 枚举自由变量的所有组合,寻找最少按动次数
        int minPresses = Integer.MAX_VALUE;
        int totalCombinations = 1 << freeVars;

        for (int mask = 0; mask < totalCombinations; mask++) {
            int[] ans = new int[m];
            int f = 0;

            // 设置自由变量的值
            for (int i = 0; i < m; i++) {
                if (where[i] == -1) {
                    ans[i] = (mask >> f) & 1;
                    f++;
                }
            }

            // 回代求解主元变量
            for (int i = m - 1; i >= 0; i--) {
                if (where[i] != -1) {
                    int sum = a[where[i]][m]; // 右侧常数项
                    for (int j = i + 1; j < m; j++) {
                        sum ^= (a[where[i]][j] & ans[j]);
                    }
                    ans[i] = sum;
                }
            }

            int presses = 0;
            for (int v : ans) presses += v;
            minPresses = Math.min(minPresses, presses);
        }

        return minPresses;
    }

    /**
     * 辅助方法:打印增广矩阵
     */
    public void printMatrix(int[][] a) {
        for (int[] row : a) {
            System.out.println(Arrays.toString(row));
        }
    }

    /**
     * 构造一个示例电路谜题
     * 3盏灯,3个开关,按下开关i会翻转灯i和灯(i+1)%3
     */
    public static int[][] createExample() {
        int n = 3;
        int[][] a = new int[n][n + 1];
        // 开关0影响灯0和灯1
        a[0][0] = 1; a[0][1] = 1; a[0][2] = 0;
        // 开关1影响灯1和灯2
        a[1][0] = 0; a[1][1] = 1; a[1][2] = 1;
        // 开关2影响灯0和灯2
        a[2][0] = 1; a[2][1] = 0; a[2][2] = 1;
        // 目标:所有灯从灭变亮(初始全0,目标全1)
        a[0][3] = 1; a[1][3] = 1; a[2][3] = 1;
        return a;
    }

    public static void main(String[] args) {
        CircuitPuzzle solver = new CircuitPuzzle();

        System.out.println("=== 电路谜题求解演示 ===\n");

        // 二进制枚举示例
        System.out.println("【二进制枚举法】");
        int n = 3;
        int[] effect = {0b011, 0b110, 0b101}; // 开关影响关系
        int init = 0b000; // 初始全灭
        int result = solver.solveByEnumeration(n, effect, init);
        System.out.println("最少按动次数: " + result);

        // 高斯消元示例
        System.out.println("\n【模2高斯消元法】");
        int[][] matrix = createExample();
        System.out.println("增广矩阵:");
        solver.printMatrix(matrix);
        int result2 = solver.solveByGauss(matrix);
        System.out.println("最少按动次数: " + result2);

        // 大规模测试
        System.out.println("\n【大规模测试:10盏灯环形电路】");
        int bigN = 10;
        int[][] bigMatrix = new int[bigN][bigN + 1];
        for (int i = 0; i < bigN; i++) {
            bigMatrix[i][i] = 1;
            bigMatrix[i][(i + 1) % bigN] = 1;
            bigMatrix[i][bigN] = 1; // 目标全亮
        }
        int result3 = solver.solveByGauss(bigMatrix);
        System.out.println("10盏灯环形电路最少按动次数: " + result3);
    }
}

复杂度分析

方法 时间复杂度 空间复杂度 适用规模
二进制枚举 O(2^n × n) O(1) n ≤ 20
高斯消元 O(n³ + 2^f × n²) O(n²) n ≤ 500,f为自由变量数

高斯消元后若自由变量 f 较多(f > 20),枚举自由变量仍可能超时,此时可用启发式搜索或折半枚举优化。

扩展思考

  1. 多解判重:可通过维护一个解向量集合,输出所有满足最少按动次数的开关组合。
  2. 加权最小解:若每个开关有不同的”按动代价”,问题转化为模2域上的加权最小覆盖,可用迭代加深或 Dancing Links 求解。
  3. 三维扩展:将灯排成二维网格,每个开关影响周围十字形区域,本质仍是线性方程组,规模增大时高斯消元更具优势。

电路谜题是理解线性代数在离散优化中应用的绝佳案例,也是许多经典谜题(如熄灯游戏、魔方状态求解)的数学基础。