一、游戏介绍与算法目标
鲁班锁,又称孔明锁,是中国古代传统的土木建筑固定结合器,也是经典的益智玩具。它由多根木条通过凹凸结构相互咬合,无需钉子或胶水即可稳固连接。最著名的”六柱鲁班锁”由6根木条组成,每根木条上有特定的缺口和凸台,装配顺序和方向都有严格要求。
本文用Java实现鲁班锁的装配求解器,核心算法包括:
- 三维空间碰撞检测:判断木条在空间中的相交关系
- 约束满足问题(CSP)建模:将装配问题转化为变量-约束系统
- 回溯搜索与剪枝:在装配顺序空间中搜索可行解
- 方向枚举优化:利用对称性减少无效搜索分支
二、问题建模
2.1 木条几何表示
每根木条可表示为三维空间中的长方体,带有若干缺口(凹陷)和凸台(凸起)。为简化计算,将木条离散化为单位立方体组成的网格:
木条 = { (x, y, z) | x∈[0, L-1], y∈[0, W-1], z∈[0, H-1], grid[x][y][z] = 1 }
其中 grid[x][y][z] = 1 表示该位置有实体材料,= 0 表示缺口。
2.2 装配约束
两根木条 A 和 B 可以共存的充要条件是:
– 空间不重叠:A ∩ B = ∅(实体部分不能相交)
– 相互支撑:每根木条至少有一部分被其他木条卡住(防止散落)
2.3 方向与位置
每根木条有24种可能的方向(三维旋转群),每种方向有多个可能的平移位置。搜索空间巨大,需要高效剪枝。
三、完整Java实现
import java.util.*;
/**
* 鲁班锁三维装配求解器
* 使用回溯搜索与约束满足求解装配顺序
*/
public class LubanLockSolver {
/** 三维向量 */
public static class Vec3 {
int x, y, z;
public Vec3(int x, int y, int z) { this.x = x; this.y = y; this.z = z; }
public Vec3 add(Vec3 o) { return new Vec3(x + o.x, y + o.y, z + o.z); }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Vec3)) return false;
Vec3 v = (Vec3) o;
return x == v.x && y == v.y && z == v.z;
}
@Override public int hashCode() { return Objects.hash(x, y, z); }
@Override public String toString() { return "(" + x + "," + y + "," + z + ")"; }
}
/** 木条:由一组单位立方体位置组成 */
public static class Piece {
String name;
List<Vec3> voxels; // 原始局部坐标下的实体块
int id;
public Piece(String name, int id) {
this.name = name;
this.id = id;
this.voxels = new ArrayList<>();
}
public void addVoxel(int x, int y, int z) {
voxels.add(new Vec3(x, y, z));
}
/** 获取该木条在指定变换后的世界坐标 */
public List<Vec3> getWorldVoxels(Transform t) {
List<Vec3> result = new ArrayList<>();
for (Vec3 v : voxels) {
Vec3 rotated = t.rotation.apply(v);
result.add(rotated.add(t.translation));
}
return result;
}
}
/** 三维旋转:使用置换矩阵表示 */
public static class Rotation {
// 旋转矩阵的3列,分别表示原x,y,z轴映射到的新方向
Vec3 axisX, axisY, axisZ;
public Rotation(Vec3 axisX, Vec3 axisY, Vec3 axisZ) {
this.axisX = axisX;
this.axisY = axisY;
this.axisZ = axisZ;
}
public Vec3 apply(Vec3 v) {
return new Vec3(
v.x * axisX.x + v.y * axisY.x + v.z * axisZ.x,
v.x * axisX.y + v.y * axisY.y + v.z * axisZ.y,
v.x * axisX.z + v.y * axisY.z + v.z * axisZ.z
);
}
}
/** 变换:旋转+平移 */
public static class Transform {
Rotation rotation;
Vec3 translation;
public Transform(Rotation rotation, Vec3 translation) {
this.rotation = rotation;
this.translation = translation;
}
@Override public String toString() {
return "T" + translation + " R[x" + rotation.axisX + ",y" + rotation.axisY + ",z" + rotation.axisZ + "]";
}
}
/** 装配状态 */
public static class AssemblyState {
Transform[] transforms; // 每根木条的变换
boolean[] placed; // 是否已放置
Set<Vec3> occupied; // 已占用的空间坐标
public AssemblyState(int n) {
this.transforms = new Transform[n];
this.placed = new boolean[n];
this.occupied = new HashSet<>();
}
public AssemblyState copy() {
AssemblyState s = new AssemblyState(transforms.length);
System.arraycopy(transforms, 0, s.transforms, 0, transforms.length);
System.arraycopy(placed, 0, s.placed, 0, placed.length);
s.occupied = new HashSet<>(occupied);
return s;
}
}
// 所有24种旋转
private static final List<Rotation> ROTATIONS = generateRotations();
private static List<Rotation> generateRotations() {
List<Rotation> rots = new ArrayList<>();
// 6个面作为z轴方向
Vec3[] zAxes = {
new Vec3(0,0,1), new Vec3(0,0,-1),
new Vec3(0,1,0), new Vec3(0,-1,0),
new Vec3(1,0,0), new Vec3(-1,0,0)
};
// 对于每个z轴,4个x轴方向
for (Vec3 z : zAxes) {
Vec3[] candidates = {
new Vec3(1,0,0), new Vec3(-1,0,0),
new Vec3(0,1,0), new Vec3(0,-1,0),
new Vec3(0,0,1), new Vec3(0,0,-1)
};
for (Vec3 x : candidates) {
// x与z正交
if (x.x * z.x + x.y * z.y + x.z * z.z != 0) continue;
// y = z × x(叉积)
Vec3 y = new Vec3(
z.y * x.z - z.z * x.y,
z.z * x.x - z.x * x.z,
z.x * x.y - z.y * x.x
);
rots.add(new Rotation(x, y, z));
}
}
// 去重
Set<String> seen = new HashSet<>();
List<Rotation> unique = new ArrayList<>();
for (Rotation r : rots) {
String key = r.axisX + "," + r.axisY + "," + r.axisZ;
if (seen.add(key)) unique.add(r);
}
return unique;
}
private final List<Piece> pieces;
private final int containerSize; // 装配容器边长
public LubanLockSolver(List<Piece> pieces, int containerSize) {
this.pieces = pieces;
this.containerSize = containerSize;
}
/**
* 检查木条在指定变换下是否与已放置木条冲突
*/
public boolean isValidPlacement(Piece piece, Transform transform, Set<Vec3> occupied) {
List<Vec3> worldVoxels = piece.getWorldVoxels(transform);
for (Vec3 v : worldVoxels) {
// 超出容器边界
if (v.x < 0 || v.x >= containerSize ||
v.y < 0 || v.y >= containerSize ||
v.z < 0 || v.z >= containerSize) {
return false;
}
// 与已占用空间冲突
if (occupied.contains(v)) {
return false;
}
}
return true;
}
/**
* 检查木条是否被其他木条"卡住"(至少有一个方向不能移动)
*/
public boolean isSupported(Piece piece, Transform transform, Set<Vec3> occupied) {
List<Vec3> worldVoxels = piece.getWorldVoxels(transform);
Set<Vec3> selfSet = new HashSet<>(worldVoxels);
// 检查6个方向是否有阻挡
Vec3[] directions = {
new Vec3(1,0,0), new Vec3(-1,0,0),
new Vec3(0,1,0), new Vec3(0,-1,0),
new Vec3(0,0,1), new Vec3(0,0,-1)
};
int blockedDirs = 0;
for (Vec3 dir : directions) {
boolean canMove = true;
for (Vec3 v : worldVoxels) {
Vec3 next = v.add(dir);
// 如果移动方向上的位置在容器外或被其他木条占据,则不能移动
if (next.x < 0 || next.x >= containerSize ||
next.y < 0 || next.y >= containerSize ||
next.z < 0 || next.z >= containerSize) {
continue; // 边界阻挡
}
if (occupied.contains(next) && !selfSet.contains(next)) {
continue; // 其他木条阻挡
}
// 该方向上此格可以移动
canMove = false;
break;
}
if (!canMove) blockedDirs++;
}
// 至少有一个方向被完全阻挡
return blockedDirs >= 1;
}
/**
* 回溯搜索装配方案
* @return 每根木条的变换方案,null表示无解
*/
public List<Transform> solve() {
AssemblyState state = new AssemblyState(pieces.size());
List<Integer> order = new ArrayList<>();
if (backtrack(state, order)) {
return Arrays.asList(state.transforms);
}
return null;
}
private boolean backtrack(AssemblyState state, List<Integer> order) {
if (order.size() == pieces.size()) {
return true; // 所有木条已放置
}
// 选择下一个要放置的木条(MRV启发式:可选位置最少的优先)
int nextPiece = selectNextPiece(state);
if (nextPiece == -1) return false;
Piece piece = pieces.get(nextPiece);
List<Transform> candidates = generateCandidates(piece, state);
// 按支撑度排序:优先选择被更多已放置木条支撑的方案
candidates.sort((a, b) -> {
int scoreA = countSupport(piece, a, state);
int scoreB = countSupport(piece, b, state);
return Integer.compare(scoreB, scoreA);
});
for (Transform t : candidates) {
if (!isValidPlacement(piece, t, state.occupied)) continue;
// 放置木条
state.placed[nextPiece] = true;
state.transforms[nextPiece] = t;
List<Vec3> voxels = piece.getWorldVoxels(t);
state.occupied.addAll(voxels);
order.add(nextPiece);
// 递归
if (backtrack(state, order)) return true;
// 回溯
order.remove(order.size() - 1);
state.occupied.removeAll(voxels);
state.placed[nextPiece] = false;
state.transforms[nextPiece] = null;
}
return false;
}
/** MRV启发式:选择候选位置最少的未放置木条 */
private int selectNextPiece(AssemblyState state) {
int best = -1;
int minCandidates = Integer.MAX_VALUE;
for (int i = 0; i < pieces.size(); i++) {
if (state.placed[i]) continue;
int count = generateCandidates(pieces.get(i), state).size();
if (count < minCandidates) {
minCandidates = count;
best = i;
}
}
return best;
}
/** 生成某木条的所有候选变换 */
private List<Transform> generateCandidates(Piece piece, AssemblyState state) {
List<Transform> candidates = new ArrayList<>();
for (Rotation rot : ROTATIONS) {
// 计算旋转后的包围盒
int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE, minZ = Integer.MAX_VALUE;
int maxX = Integer.MIN_VALUE, maxY = Integer.MIN_VALUE, maxZ = Integer.MIN_VALUE;
for (Vec3 v : piece.voxels) {
Vec3 r = rot.apply(v);
minX = Math.min(minX, r.x); maxX = Math.max(maxX, r.x);
minY = Math.min(minY, r.y); maxY = Math.max(maxY, r.y);
minZ = Math.min(minZ, r.z); maxZ = Math.max(maxZ, r.z);
}
int sizeX = maxX - minX + 1;
int sizeY = maxY - minY + 1;
int sizeZ = maxZ - minZ + 1;
// 枚举平移位置使木条在容器内
for (int tx = 0; tx <= containerSize - sizeX; tx++) {
for (int ty = 0; ty <= containerSize - sizeY; ty++) {
for (int tz = 0; tz <= containerSize - sizeZ; tz++) {
Vec3 trans = new Vec3(tx - minX, ty - minY, tz - minZ);
candidates.add(new Transform(rot, trans));
}
}
}
}
return candidates;
}
/** 计算某放置方案与已放置木条的接触面数 */
private int countSupport(Piece piece, Transform t, AssemblyState state) {
List<Vec3> voxels = piece.getWorldVoxels(t);
Set<Vec3> voxelSet = new HashSet<>(voxels);
int count = 0;
Vec3[] dirs = {
new Vec3(1,0,0), new Vec3(-1,0,0),
new Vec3(0,1,0), new Vec3(0,-1,0),
new Vec3(0,0,1), new Vec3(0,0,-1)
};
for (Vec3 v : voxels) {
for (Vec3 d : dirs) {
Vec3 neighbor = v.add(d);
if (state.occupied.contains(neighbor) && !voxelSet.contains(neighbor)) {
count++;
}
}
}
return count;
}
/** 创建简化版三柱鲁班锁 */
public static List<Piece> createSimple3PieceLock() {
List<Piece> pieces = new ArrayList<>();
// 木条1:横向,中间有缺口让木条2穿过
Piece p1 = new Piece("横条", 0);
for (int x = 0; x < 5; x++) {
for (int y = 0; y < 1; y++) {
for (int z = 0; z < 1; z++) {
if (x == 2) continue; // 中间缺口
p1.addVoxel(x, y, z);
}
}
}
pieces.add(p1);
// 木条2:纵向,中间有缺口让木条1穿过
Piece p2 = new Piece("纵条", 1);
for (int x = 0; x < 1; x++) {
for (int y = 0; y < 5; y++) {
for (int z = 0; z < 1; z++) {
if (y == 2) continue; // 中间缺口
p2.addVoxel(x, y, z);
}
}
}
pieces.add(p2);
// 木条3:竖向,中间有十字缺口
Piece p3 = new Piece("竖条", 2);
for (int x = 0; x < 1; x++) {
for (int y = 0; y < 1; y++) {
for (int z = 0; z < 5; z++) {
if (z == 2) continue; // 中间缺口
p3.addVoxel(x, y, z);
}
}
}
pieces.add(p3);
return pieces;
}
/** 测试主程序 */
public static void main(String[] args) {
List<Piece> pieces = createSimple3PieceLock();
LubanLockSolver solver = new LubanLockSolver(pieces, 5);
System.out.println("=== 鲁班锁装配求解器 ===");
System.out.println("木条数量: " + pieces.size());
System.out.println("旋转种类: " + ROTATIONS.size());
List<Transform> solution = solver.solve();
if (solution != null) {
System.out.println("\n找到装配方案!");
for (int i = 0; i < solution.size(); i++) {
System.out.println(pieces.get(i).name + ": " + solution.get(i));
}
} else {
System.out.println("未找到装配方案");
}
}
}
四、算法复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 候选生成 | O( | R |
| 碰撞检测 | O(V) | O(1) |
| 单次回溯 | O(P · C · V) | O(P · V) |
| 完整搜索 | O((P!) · C^P) | O(P · V) |
MRV启发式与接触面排序剪枝可将实际搜索空间降低数个数量级。
五、关键设计要点
- 体素离散化:将连续几何转化为单位立方体集合,碰撞检测简化为集合交运算
- 24种旋转枚举:三维物体的旋转群恰有24个元素,覆盖所有正交方向
- MRV启发式:优先放置约束最多的木条,减少分支因子
- 接触面排序:优先选择与其他木条有更多接触面的放置方案,提高结构稳定性
- 支撑检查:可选的物理稳定性验证,确保每根木条至少有一个平移自由度被阻挡
六、扩展方向
- 六柱锁建模:完整实现传统六柱鲁班锁的6根木条(3组镜像对称对)
- 拆解顺序求解:反向从完整状态搜索拆解步骤,与装配顺序互为逆序
- STL文件解析:读取真实3D模型的体素化数据,求解实际鲁班锁
- 稳定性评估:引入刚体静力学,计算各木条的受力平衡状态