每日算法 — 使用java实现九连环:递归分治与二进制状态递推求解

引言

九连环是中国传统智力玩具,由九个环套在剑形框上组成。解环规则为:想要摘下或装上第 k 个环,必须保证第 k−1 个环在框上,且第 1 到 k−2 个环都已取下。本文用Java实现九连环的求解器,讲解递归分治如何将问题拆解为子问题,以及二进制状态编码如何将连环状态压缩为整数进行高效递推。

问题建模

用长度为9的布尔数组表示每个环的状态:true 表示环在框上,false 表示已取下。目标是从初始状态 [true,true,...,true] 变为 [false,false,...,false]。

九连环的核心约束可归纳为两条操作规则:
– 规则1:第1个环可以自由取下或装上(不受限制)。
– 规则2:第 k 个环(k > 1)只有当第 k−1 个环在框上、且前面所有环都不在框上时,才能取下或装上。

核心算法一:递归分治求解

设 f(n) 为取下前 n 个环所需的最少步数。通过分析可得递推关系:

f(1) = 1
f(n) = f(n-1) + 1 + f(n-1) = 2*f(n-1) + 1  (当需要操作第n个环时)

实际上,九连环的标准递推为 f(n) = 2*f(n-2) + f(n-1) + 1,因为操作第 n 环需要先处理好前 n−2 个环。以下是带状态追踪的递归实现:

import java.util.*;

public class NineLinkedRings {
    private int n; // 环的数量
    private boolean[] rings; // 环的状态
    private List<String> steps; // 记录操作步骤
    private long stepCount; // 总步数计数

    public NineLinkedRings(int n) {
        this.n = n;
        this.rings = new boolean[n]; // 默认false,表示都在框上(初始为true)
        Arrays.fill(rings, true);
        this.steps = new ArrayList<>();
        this.stepCount = 0;
    }

    /**
     * 递归求解:将前 k 个环全部取下
     * @param k 目标环数
     */
    public void solveDown(int k) {
        if (k == 0) return;
        if (k == 1) {
            // 第1个环可以直接取下
            operate(0, false);
            return;
        }
        // 要取下第k个环,需要先取下前k-2个环
        solveDown(k - 2);
        // 然后取下第k个环(此时第k-1个在框上,前k-2个已取下)
        operate(k - 1, false);
        // 再把前k-2个环装回去(为了后续操作第k-1个环)
        solveUp(k - 2);
        // 递归取下前k-1个环
        solveDown(k - 1);
    }

    /**
     * 递归求解:将前 k 个环全部装上
     * @param k 目标环数
     */
    public void solveUp(int k) {
        if (k == 0) return;
        if (k == 1) {
            operate(0, true);
            return;
        }
        // 先装上前k-1个环
        solveUp(k - 1);
        // 把前k-2个环取下
        solveDown(k - 2);
        // 装上第k个环
        operate(k - 1, true);
        // 再把前k-2个环装回去
        solveUp(k - 2);
    }

    /**
     * 执行单步操作
     * @param index 环的索引(0-based)
     * @param putOn true表示装上,false表示取下
     */
    private void operate(int index, boolean putOn) {
        // 验证操作合法性
        if (!canOperate(index, putOn)) {
            throw new IllegalStateException(
                String.format("无法%s第%d个环", putOn ? "装上" : "取下", index + 1)
            );
        }
        rings[index] = putOn;
        stepCount++;
        steps.add(String.format("步%d: %s第%d环", stepCount, 
                                putOn ? "装上" : "取下", index + 1));
    }

    /**
     * 判断是否可以操作第 index 个环
     */
    private boolean canOperate(int index, boolean putOn) {
        if (rings[index] == putOn) return false; // 已经是目标状态
        if (index == 0) return true; // 第1个环始终可操作
        // 第index个环可操作的条件:第index-1个环在框上,且前index-1个环都不在
        if (!rings[index - 1]) return false;
        for (int i = 0; i < index - 1; i++) {
            if (rings[i]) return false;
        }
        return true;
    }

    public void printSteps() {
        for (String s : steps) {
            System.out.println(s);
        }
        System.out.println("总步数: " + stepCount);
    }

    public static void main(String[] args) {
        NineLinkedRings game = new NineLinkedRings(9);
        game.solveDown(9);
        game.printSteps();
    }
}

递归求解的时间复杂度为 O(2^n),因为九连环的总步数本身就是 2^n - 1(奇数环)或 (2^(n+1) - 2) / 3(标准九连环公式)。

核心算法二:二进制状态编码与递推

九连环的状态可以用一个 n 位二进制数表示:第 i 位为1表示第 i 环在框上。这样状态转移可以通过位运算快速判断。

    /**
     * 使用二进制编码检查操作合法性
     * @param state 当前状态,第i位为1表示第i环在框上
     * @param index 要操作的环(0-based)
     * @param putOn true表示装上
     */
    public static boolean canOperateBinary(int state, int index, boolean putOn) {
        boolean current = ((state >> index) & 1) == 1;
        if (current == putOn) return false;
        if (index == 0) return true;
        // 检查第index-1位是否为1
        if (((state >> (index - 1)) & 1) == 0) return false;
        // 检查前index-1位是否全为0
        int mask = (1 << (index - 1)) - 1;
        return (state & mask) == 0;
    }

    /**
     * 计算从全满状态到全空状态的最少步数(标准公式)
     * 九连环步数公式:
     * - 奇数个环:steps = (2^(n+1) - 1) / 3
     * - 偶数个环:steps = (2^(n+1) - 2) / 3
     */
    public static long calculateMinSteps(int n) {
        long pow2 = 1L << (n + 1); // 2^(n+1)
        if (n % 2 == 1) {
            return (pow2 - 1) / 3;
        } else {
            return (pow2 - 2) / 3;
        }
    }

BFS验证最短路径

为了验证递归解的最优性,可以用BFS在状态空间中进行搜索。九连环有 2^9 = 512 种状态,BFS完全可以处理。

    /**
     * BFS求解开九连环的最短路径
     * @return 从全满(511)到全空(0)的最短步数
     */
    public static int bfsShortestPath(int n) {
        int totalStates = 1 << n;
        boolean[] visited = new boolean[totalStates];
        int[] dist = new int[totalStates];
        int[] parent = new int[totalStates];
        Arrays.fill(parent, -1);

        Queue<Integer> queue = new LinkedList<>();
        int start = totalStates - 1; // 全满状态
        int goal = 0; // 全空状态

        queue.offer(start);
        visited[start] = true;

        while (!queue.isEmpty()) {
            int state = queue.poll();
            if (state == goal) break;

            // 尝试操作每一个环
            for (int i = 0; i < n; i++) {
                boolean isOn = ((state >> i) & 1) == 1;
                if (canOperateBinary(state, i, !isOn)) {
                    int nextState = state ^ (1 << i);
                    if (!visited[nextState]) {
                        visited[nextState] = true;
                        dist[nextState] = dist[state] + 1;
                        parent[nextState] = state;
                        queue.offer(nextState);
                    }
                }
            }
        }
        return dist[goal];
    }

复杂度分析

方法 时间复杂度 空间复杂度 适用场景
递归分治 O(2^n) O(n) 栈空间 生成具体操作步骤
数学公式 O(1) O(1) 快速计算总步数
BFS状态搜索 O(2^n × n) O(2^n) 验证最优性、小规模n

对于标准九连环(n=9),BFS只需搜索512个状态,递归产生341步操作序列,三者结果完全一致。

总结

九连环的求解展示了递归分治的经典应用:操作第 n 个环的问题被分解为操作前 n−2 个环的子问题,再通过子问题的组合得到原问题的解。二进制状态编码则让我们可以用位运算在常数时间内判断合法性,将状态空间压缩到极致。这种”大问题拆小、小问题解决后再组合”的思想,正是分治算法的精髓所在。