每日算法 — 使用java实现鲁班锁:三维约束满足与回溯装配算法

一、游戏介绍与算法目标

鲁班锁,又称孔明锁,是中国古代传统的土木建筑固定结合器,也是经典的益智玩具。它由多根木条通过凹凸结构相互咬合,无需钉子或胶水即可稳固连接。最著名的”六柱鲁班锁”由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启发式与接触面排序剪枝可将实际搜索空间降低数个数量级。

五、关键设计要点

  1. 体素离散化:将连续几何转化为单位立方体集合,碰撞检测简化为集合交运算
  2. 24种旋转枚举:三维物体的旋转群恰有24个元素,覆盖所有正交方向
  3. MRV启发式:优先放置约束最多的木条,减少分支因子
  4. 接触面排序:优先选择与其他木条有更多接触面的放置方案,提高结构稳定性
  5. 支撑检查:可选的物理稳定性验证,确保每根木条至少有一个平移自由度被阻挡

六、扩展方向

  • 六柱锁建模:完整实现传统六柱鲁班锁的6根木条(3组镜像对称对)
  • 拆解顺序求解:反向从完整状态搜索拆解步骤,与装配顺序互为逆序
  • STL文件解析:读取真实3D模型的体素化数据,求解实际鲁班锁
  • 稳定性评估:引入刚体静力学,计算各木条的受力平衡状态