引言
九连环是中国传统智力玩具,由九个环套在剑形框上组成。解环规则为:想要摘下或装上第 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 个环的子问题,再通过子问题的组合得到原问题的解。二进制状态编码则让我们可以用位运算在常数时间内判断合法性,将状态空间压缩到极致。这种”大问题拆小、小问题解决后再组合”的思想,正是分治算法的精髓所在。