引言
孔明锁(又称鲁班锁)是中国传统木工智力玩具,由若干木条通过凹凸结构相互咬合而成。拆解和组装孔明锁需要理解各部件之间的空间约束关系。本文用Java实现孔明锁的核心求解逻辑,讲解约束传播算法如何逐层缩减部件的可行位置,以及三维几何碰撞检测如何判断木条之间是否构成有效互锁结构。
问题建模
将孔明锁抽象为三维空间中的长方体木条集合。每根木条有以下属性:
– 尺寸:长、宽、高(以基本单元格为单位)
– 位置:(x, y, z) 三维坐标
– 方向:沿X/Y/Z轴之一放置
– 缺口:木条上被挖去的部分,用于与其他木条咬合
经典的六根孔明锁中,每根木条尺寸相同(如 3×1×1),中间带有特定的十字形或L形缺口。六根木条两两正交,沿三个坐标轴各有两根。
核心算法一:三维AABB碰撞检测
判断两根木条是否发生碰撞(非缺口处的实体部分重叠),是孔明锁组装验证的基础。
import java.util.*;
public class KongmingLockSolver {
/**
* 三维空间中的长方体木条
*/
static class WoodenBar {
int id;
int x, y, z; // 左下角坐标
int w, h, d; // 沿X/Y/Z轴的长度
Set<Gap> gaps; // 缺口集合
WoodenBar(int id, int x, int y, int z, int w, int h, int d) {
this.id = id;
this.x = x; this.y = y; this.z = z;
this.w = w; this.h = h; this.d = d;
this.gaps = new HashSet<>();
}
/**
* 添加缺口:缺口是木条上的空心区域
*/
void addGap(int gx, int gy, int gz, int gw, int gh, int gd) {
gaps.add(new Gap(gx, gy, gz, gw, gh, gd));
}
/**
* 获取木条的实体区域(总区域减去缺口)
*/
List<AABB> getSolidParts() {
List<AABB> solids = new ArrayList<>();
solids.add(new AABB(x, y, z, x + w, y + h, z + d));
// 用缺口裁剪实体区域(简化处理:直接记录缺口位置)
// 实际实现中可用三维差集算法
return solids;
}
AABB getBoundingBox() {
return new AABB(x, y, z, x + w, y + h, z + d);
}
}
/**
* 轴对齐包围盒(Axis-Aligned Bounding Box)
*/
static class AABB {
int minX, minY, minZ;
int maxX, maxY, maxZ;
AABB(int x1, int y1, int z1, int x2, int y2, int z2) {
this.minX = x1; this.minY = y1; this.minZ = z1;
this.maxX = x2; this.maxY = y2; this.maxZ = z2;
}
/**
* 判断两个AABB是否相交(碰撞检测核心)
*/
boolean intersects(AABB other) {
return this.minX < other.maxX && this.maxX > other.minX &&
this.minY < other.maxY && this.maxY > other.minY &&
this.minZ < other.maxZ && this.maxZ > other.minZ;
}
/**
* 计算相交区域
*/
AABB intersection(AABB other) {
int ix1 = Math.max(this.minX, other.minX);
int iy1 = Math.max(this.minY, other.minY);
int iz1 = Math.max(this.minZ, other.minZ);
int ix2 = Math.min(this.maxX, other.maxX);
int iy2 = Math.min(this.maxY, other.maxY);
int iz2 = Math.min(this.maxZ, other.maxZ);
if (ix1 >= ix2 || iy1 >= iy2 || iz1 >= iz2) {
return null; // 不相交
}
return new AABB(ix1, iy1, iz1, ix2, iy2, iz2);
}
int volume() {
return (maxX - minX) * (maxY - minY) * (maxZ - minZ);
}
}
static class Gap {
int x, y, z, w, h, d;
Gap(int x, int y, int z, int w, int h, int d) {
this.x = x; this.y = y; this.z = z;
this.w = w; this.h = h; this.d = d;
}
}
核心算法二:约束传播与可行域缩减
孔明锁的组装本质上是一个约束满足问题(CSP)。每根木条的放置受到其他木条的限制,可以通过约束传播逐步缩减可行域。
/**
* 约束传播求解器
* 维护每根木条的可能位置集合,通过约束不断缩减
*/
static class ConstraintSolver {
List<WoodenBar> bars;
Map<Integer, List<Position>> domains; // 每根木条的可行位置域
ConstraintSolver(List<WoodenBar> bars) {
this.bars = bars;
this.domains = new HashMap<>();
initializeDomains();
}
/**
* 初始化每根木条的所有可能位置
* 对于标准六根锁,每根木条沿一个轴放置,位置受限
*/
void initializeDomains() {
for (WoodenBar bar : bars) {
List<Position> possible = new ArrayList<>();
// 在 5×5×5 的组装空间内枚举可能位置
for (int x = -2; x <= 2; x++) {
for (int y = -2; y <= 2; y++) {
for (int z = -2; z <= 2; z++) {
if (isValidInitialPosition(bar, x, y, z)) {
possible.add(new Position(x, y, z));
}
}
}
}
domains.put(bar.id, possible);
}
}
boolean isValidInitialPosition(WoodenBar bar, int x, int y, int z) {
// 中心约束:木条中心必须接近原点
int cx = x + bar.w / 2;
int cy = y + bar.h / 2;
int cz = z + bar.d / 2;
return Math.abs(cx) <= 1 && Math.abs(cy) <= 1 && Math.abs(cz) <= 1;
}
/**
* 约束传播:根据已固定的木条,移除其他木条不可行的位置
*/
boolean propagateConstraints() {
boolean changed = true;
int iterations = 0;
final int MAX_ITER = 1000;
while (changed && iterations < MAX_ITER) {
changed = false;
iterations++;
for (WoodenBar bar : bars) {
List<Position> domain = domains.get(bar.id);
Iterator<Position> it = domain.iterator();
while (it.hasNext()) {
Position pos = it.next();
bar.x = pos.x; bar.y = pos.y; bar.z = pos.z;
// 检查与所有已确定或部分确定的木条的约束
if (!satisfiesConstraints(bar)) {
it.remove();
changed = true;
}
}
if (domain.isEmpty()) {
return false; // 无可行解
}
}
}
return true;
}
/**
* 检查木条 bar 的当前位置是否满足与其他所有木条的约束
*/
boolean satisfiesConstraints(WoodenBar bar) {
for (WoodenBar other : bars) {
if (other.id == bar.id) continue;
AABB box1 = bar.getBoundingBox();
AABB box2 = other.getBoundingBox();
// 若AABB不相交,则无约束
if (!box1.intersects(box2)) continue;
// 若相交,检查是否为合法的互锁(通过缺口咬合)
AABB overlap = box1.intersection(box2);
if (overlap == null) continue;
// 简化判断:重叠区域必须至少被一方的缺口覆盖
if (!isCoveredByGap(bar, overlap) && !isCoveredByGap(other, overlap)) {
return false; // 实体部分非法重叠
}
}
return true;
}
/**
* 检查给定区域是否完全被某木条的缺口覆盖
*/
boolean isCoveredByGap(WoodenBar bar, AABB region) {
for (Gap gap : bar.gaps) {
AABB gapBox = new AABB(
bar.x + gap.x, bar.y + gap.y, bar.z + gap.z,
bar.x + gap.x + gap.w, bar.y + gap.y + gap.h, bar.z + gap.z + gap.d
);
if (gapBox.intersects(region)) {
// 简化:只要有缺口覆盖部分重叠区域即认为合法
return true;
}
}
return false;
}
}
static class Position {
int x, y, z;
Position(int x, int y, int z) { this.x = x; this.y = y; this.z = z; }
}
核心算法三:拆卸序列搜索
孔明锁的拆解顺序是唯一的(或有限几种)。通过分析每根木条当前是否”自由”(沿某一方向移动不再受其他木条阻挡),可以找到正确的拆卸序列。
/**
* 寻找孔明锁的拆卸序列
* @return 按拆卸顺序排列的木条ID列表
*/
public List<Integer> findDisassemblySequence(List<WoodenBar> assembledBars) {
List<Integer> sequence = new ArrayList<>();
Set<Integer> removed = new HashSet<>();
List<WoodenBar> remaining = new ArrayList<>(assembledBars);
while (!remaining.isEmpty()) {
boolean found = false;
for (WoodenBar bar : remaining) {
if (removed.contains(bar.id)) continue;
// 检查木条是否至少有一个自由移动方向
int[][] directions = {{1,0,0}, {-1,0,0}, {0,1,0}, {0,-1,0}, {0,0,1}, {0,0,-1}};
for (int[] dir : directions) {
if (canSlideOut(bar, remaining, removed, dir[0], dir[1], dir[2])) {
sequence.add(bar.id);
removed.add(bar.id);
remaining.remove(bar);
found = true;
break;
}
}
if (found) break;
}
if (!found) {
throw new IllegalStateException("无法找到合法的拆卸顺序,锁结构可能有误");
}
}
return sequence;
}
/**
* 检查木条是否可以沿指定方向滑出
*/
boolean canSlideOut(WoodenBar bar, List<WoodenBar> allBars,
Set<Integer> removed, int dx, int dy, int dz) {
// 模拟沿方向无限移动,检查是否与其他未拆除的木条发生碰撞
for (int step = 1; step <= 10; step++) {
AABB moved = new AABB(
bar.x + dx * step, bar.y + dy * step, bar.z + dz * step,
bar.x + bar.w + dx * step, bar.y + bar.h + dy * step, bar.z + bar.d + dz * step
);
for (WoodenBar other : allBars) {
if (other.id == bar.id || removed.contains(other.id)) continue;
AABB otherBox = other.getBoundingBox();
// 忽略缺口处的碰撞(简化:只要实体部分不撞即可)
if (moved.intersects(otherBox)) {
// 检查相交区域是否被缺口覆盖
AABB overlap = moved.intersection(otherBox);
if (overlap != null && overlap.volume() > 0) {
// 需要更精细的缺口覆盖判断
if (!isCoveredByGap(bar, overlap) && !isCoveredByGap(other, overlap)) {
return false; // 实体碰撞,无法滑出
}
}
}
}
}
return true;
}
boolean isCoveredByGap(WoodenBar bar, AABB region) {
for (Gap gap : bar.gaps) {
AABB gapBox = new AABB(
bar.x + gap.x, bar.y + gap.y, bar.z + gap.z,
bar.x + gap.x + gap.w, bar.y + gap.y + gap.h, bar.z + gap.z + gap.d
);
if (gapBox.intersects(region)) return true;
}
return false;
}
复杂度分析
| 算法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| AABB碰撞检测 | O(1) | O(1) |
| 约束传播 | O(N² × D²) | O(N × D) |
| 拆卸序列搜索 | O(N³) | O(N) |
其中 N 为木条数量(通常6根),D 为单根木条的位置域大小。由于 N 很小,所有算法在毫秒级即可完成。
总结
孔明锁的求解过程完美诠释了约束满足问题的求解范式:先用AABB碰撞检测建立空间约束关系,再通过约束传播缩减每根木条的可行域,最后用贪心策略寻找拆卸序列。这种”几何建模 → 约束传播 → 序列搜索”的三层架构,不仅适用于传统智力玩具,也可以推广到机械装配验证、蛋白质折叠模拟等三维空间推理任务中。