每日算法 — 使用java实现Slitherlink:约束传播与回溯搜索求解

引言

Slitherlink(数回/Loop the Loop)是日本 Nikoli 出版社发明的经典逻辑谜题:在由点构成的网格中,部分格子标有数字 0~3,表示围绕该格子的封闭回路经过的边数。玩家需要画出一条连续的闭合回路,满足所有数字约束。

本文将用 Java 实现 Slitherlink 求解器,核心算法基于约束传播(Constraint Propagation)与回溯搜索(Backtracking Search),展示如何从局部约束逐步推导出全局解。

问题建模

棋盘表示

对于 m×n 的数字网格:
– 水平边:m+1 行,每行 n 条
– 垂直边:m 行,每行 n+1 条
– 总边数:(m+1)×n + m×(n+1)

用三个二维数组表示:

hEdges[m+1][n]  // 水平边:0=无, 1=有, -1=未知
vEdges[m][n+1]  // 垂直边:0=无, 1=有, -1=未知
numbers[m][n]   // 格子数字:-1=空, 0~3=约束

约束条件

每个数字格子周围的4条边之和必须等于该数字:
– 上 hEdges[i][j]
– 下 hEdges[i+1][j]
– 左 vEdges[i][j]
– 右 vEdges[i][j+1]

此外,回路必须是单一的闭合环,不能有分叉或孤立线段。

核心算法:约束传播 + 回溯搜索

约束传播规则

从数字约束可以直接推断边的状态:

数字 已确定边 剩余未知边 推断
0 0 4 所有边 = 0
3 0 4 所有边 = 1
3 1 3 剩余3条边 = 1
0 1 3 矛盾(回溯)
2 1 3 剩余中选2条 = 1
1 0 4 无法直接推断

进阶规则(顶点约束):
– 每个顶点的度只能是 0 或 2(回路无分叉)
– 若某顶点已有2条边确定,其余边必须为0
– 若某顶点已有3条边确定为0,第4条必须为… 不,回路不要求经过每个顶点

等等,修正:顶点的度可以是 0(回路不经过)或 2(回路经过)。

回溯搜索

当约束传播无法继续时,选择一个未知边进行假设(设为0或1),然后递归求解。若导致矛盾则回溯。

完整 Java 实现

import java.util.*;

/**
 * Slitherlink(数回)求解器
 * 基于约束传播与回溯搜索的逻辑谜题求解
 */
public class SlitherlinkSolver {

    // 边状态
    static final int UNKNOWN = -1;
    static final int EMPTY = 0;
    static final int LINE = 1;

    int rows, cols;
    int[][] numbers;      // 数字约束
    int[][] hEdges;       // 水平边 [rows+1][cols]
    int[][] vEdges;       // 垂直边 [rows][cols+1]

    public SlitherlinkSolver(int[][] numbers) {
        this.rows = numbers.length;
        this.cols = numbers[0].length;
        this.numbers = new int[rows][cols];
        for (int i = 0; i < rows; i++) {
            this.numbers[i] = numbers[i].clone();
        }
        this.hEdges = new int[rows + 1][cols];
        this.vEdges = new int[rows][cols + 1];

        // 初始化为未知
        for (int[] row : hEdges) Arrays.fill(row, UNKNOWN);
        for (int[] row : vEdges) Arrays.fill(row, UNKNOWN);
    }

    /**
     * 主求解方法:约束传播 + 回溯
     */
    public boolean solve() {
        // 先进行约束传播
        boolean changed;
        do {
            changed = propagateConstraints();
        } while (changed);

        // 检查是否已解决
        if (isComplete() && isValidLoop()) {
            return true;
        }

        // 检查是否有矛盾
        if (hasContradiction()) {
            return false;
        }

        // 选择一个未知边进行分支
        Edge branchEdge = selectBranchEdge();
        if (branchEdge == null) {
            return isComplete() && isValidLoop();
        }

        // 保存状态
        int[][] savedHEdges = copyArray(hEdges);
        int[][] savedVEdges = copyArray(vEdges);

        // 尝试设为 LINE
        setEdge(branchEdge, LINE);
        if (solve()) return true;

        // 恢复状态
        hEdges = savedHEdges;
        vEdges = savedVEdges;
        savedHEdges = copyArray(hEdges);
        savedVEdges = copyArray(vEdges);

        // 尝试设为 EMPTY
        setEdge(branchEdge, EMPTY);
        if (solve()) return true;

        // 恢复状态
        hEdges = savedHEdges;
        vEdges = savedVEdges;

        return false;
    }

    /**
     * 约束传播:利用数字和顶点约束推断边的状态
     */
    boolean propagateConstraints() {
        boolean changed = false;

        // 规则1:数字约束传播
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (numbers[r][c] < 0) continue;

                int target = numbers[r][c];
                int[] edges = getCellEdges(r, c);

                int lineCount = 0, emptyCount = 0, unknownCount = 0;
                List<Edge> unknownEdges = new ArrayList<>();

                for (Edge e : edges) {
                    int val = getEdgeValue(e);
                    if (val == LINE) lineCount++;
                    else if (val == EMPTY) emptyCount++;
                    else {
                        unknownCount++;
                        unknownEdges.add(e);
                    }
                }

                // 如果已有太多LINE,矛盾
                if (lineCount > target) return false; // 触发回溯

                // 如果已有太多EMPTY,无法满足target
                if (emptyCount > 4 - target) return false;

                // 如果剩余未知边必须全为LINE
                if (lineCount + unknownCount == target) {
                    for (Edge e : unknownEdges) {
                        if (getEdgeValue(e) == UNKNOWN) {
                            setEdge(e, LINE);
                            changed = true;
                        }
                    }
                }

                // 如果剩余未知边必须全为EMPTY
                if (lineCount == target) {
                    for (Edge e : unknownEdges) {
                        if (getEdgeValue(e) == UNKNOWN) {
                            setEdge(e, EMPTY);
                            changed = true;
                        }
                    }
                }
            }
        }

        // 规则2:顶点度数约束
        // 每个顶点度数只能是0或2
        for (int r = 0; r <= rows; r++) {
            for (int c = 0; c <= cols; c++) {
                List<Edge> incident = getIncidentEdges(r, c);
                int lineCount = 0, emptyCount = 0;
                List<Edge> unknownEdges = new ArrayList<>();

                for (Edge e : incident) {
                    int val = getEdgeValue(e);
                    if (val == LINE) lineCount++;
                    else if (val == EMPTY) emptyCount++;
                    else unknownEdges.add(e);
                }

                // 若已有2条LINE,其余必须为EMPTY
                if (lineCount == 2) {
                    for (Edge e : unknownEdges) {
                        if (getEdgeValue(e) == UNKNOWN) {
                            setEdge(e, EMPTY);
                            changed = true;
                        }
                    }
                }

                // 若已有3条EMPTY,且还有未知边
                // 如果只剩1条未知边:若lineCount==0则设为LINE(度=2)
                // 若lineCount==1则矛盾(度不能为1)
                if (emptyCount == incident.size() - 1 && unknownEdges.size() == 1) {
                    if (lineCount == 0) {
                        setEdge(unknownEdges.get(0), LINE);
                        changed = true;
                    } else if (lineCount == 1) {
                        return false; // 度=1,矛盾
                    }
                }

                // 若lineCount > 2,矛盾
                if (lineCount > 2) return false;
            }
        }

        // 规则3:避免小环(除非已完整)
        if (!isComplete()) {
            // 检查是否有不完整的小环
            // 简化版:暂不实现复杂环检测,留到最终验证
        }

        return changed;
    }

    /**
     * 获取格子周围的4条边
     */
    Edge[] getCellEdges(int r, int c) {
        return new Edge[] {
            new Edge(true, r, c),      // 上
            new Edge(true, r + 1, c),  // 下
            new Edge(false, r, c),     // 左
            new Edge(false, r, c + 1)  // 右
        };
    }

    /**
     * 获取顶点关联的边(最多4条)
     */
    List<Edge> getIncidentEdges(int r, int c) {
        List<Edge> edges = new ArrayList<>();
        // 上水平边
        if (r > 0 && c < cols) edges.add(new Edge(true, r, c));
        // 下水平边
        if (r < rows && c < cols) edges.add(new Edge(true, r + 1, c)); // 修正索引
        // 实际上水平边是 hEdges[r][c] 表示第r行第c列上方的水平边
        // 重新设计:hEdges[i][j] 连接 (i,j) 和 (i,j+1)

        edges.clear();
        // 上方水平边(如果存在)
        if (r > 0 && c < cols) edges.add(new Edge(true, r - 1, c)); // hEdges[r-1][c] 在顶点(r,c)上方?
        // 不对,重新梳理索引

        // hEdges[i][j]: 连接点(i,j)和(i,j+1)的水平边,i=0..rows, j=0..cols-1
        // vEdges[i][j]: 连接点(i,j)和(i+1,j)的垂直边,i=0..rows-1, j=0..cols

        // 顶点(r,c)的关联边:
        // 左水平边: hEdges[r][c-1] (如果 c > 0)
        // 右水平边: hEdges[r][c] (如果 c < cols)
        // 上垂直边: vEdges[r-1][c] (如果 r > 0)
        // 下垂直边: vEdges[r][c] (如果 r < rows)

        if (c > 0) edges.add(new Edge(true, r, c - 1));
        if (c < cols) edges.add(new Edge(true, r, c));
        if (r > 0) edges.add(new Edge(false, r - 1, c));
        if (r < rows) edges.add(new Edge(false, r, c));

        return edges;
    }

    int getEdgeValue(Edge e) {
        return e.isHorizontal ? hEdges[e.r][e.c] : vEdges[e.r][e.c];
    }

    void setEdge(Edge e, int value) {
        if (e.isHorizontal) hEdges[e.r][e.c] = value;
        else vEdges[e.r][e.c] = value;
    }

    /**
     * 选择分支边:优先选择约束最强的未知边
     */
    Edge selectBranchEdge() {
        // 策略:选择涉及最多数字约束的未知边
        for (int r = 0; r <= rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (hEdges[r][c] == UNKNOWN) {
                    return new Edge(true, r, c);
                }
            }
        }
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c <= cols; c++) {
                if (vEdges[r][c] == UNKNOWN) {
                    return new Edge(false, r, c);
                }
            }
        }
        return null;
    }

    /**
     * 检查是否所有边都已确定
     */
    boolean isComplete() {
        for (int[] row : hEdges) {
            for (int v : row) if (v == UNKNOWN) return false;
        }
        for (int[] row : vEdges) {
            for (int v : row) if (v == UNKNOWN) return false;
        }
        return true;
    }

    /**
     * 检查是否有矛盾
     */
    boolean hasContradiction() {
        // 数字约束矛盾
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (numbers[r][c] < 0) continue;
                int target = numbers[r][c];
                Edge[] edges = getCellEdges(r, c);
                int lineCount = 0, unknownCount = 0;
                for (Edge e : edges) {
                    int v = getEdgeValue(e);
                    if (v == LINE) lineCount++;
                    else if (v == UNKNOWN) unknownCount++;
                }
                if (lineCount > target) return true;
                if (lineCount + unknownCount < target) return true;
            }
        }

        // 顶点度数矛盾
        for (int r = 0; r <= rows; r++) {
            for (int c = 0; c <= cols; c++) {
                List<Edge> incident = getIncidentEdges(r, c);
                int lineCount = 0;
                for (Edge e : incident) {
                    if (getEdgeValue(e) == LINE) lineCount++;
                }
                if (lineCount > 2) return true;
            }
        }

        return false;
    }

    /**
     * 验证回路是否合法:单一的闭合环
     */
    boolean isValidLoop() {
        // 1. 所有数字约束满足
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (numbers[r][c] < 0) continue;
                Edge[] edges = getCellEdges(r, c);
                int lineCount = 0;
                for (Edge e : edges) {
                    if (getEdgeValue(e) == LINE) lineCount++;
                }
                if (lineCount != numbers[r][c]) return false;
            }
        }

        // 2. 所有顶点度数为0或2
        for (int r = 0; r <= rows; r++) {
            for (int c = 0; c <= cols; c++) {
                List<Edge> incident = getIncidentEdges(r, c);
                int lineCount = 0;
                for (Edge e : incident) {
                    if (getEdgeValue(e) == LINE) lineCount++;
                }
                if (lineCount != 0 && lineCount != 2) return false;
            }
        }

        // 3. 检查是单一回路(不是多个小环)
        // 找到一条LINE边,沿回路遍历,统计经过的边数
        int totalLineEdges = 0;
        Edge startEdge = null;
        for (int r = 0; r <= rows; r++) {
            for (int c = 0; c < cols; c++) {
                if (hEdges[r][c] == LINE) {
                    totalLineEdges++;
                    if (startEdge == null) startEdge = new Edge(true, r, c);
                }
            }
        }
        for (int r = 0; r < rows; r++) {
            for (int c = 0; c <= cols; c++) {
                if (vEdges[r][c] == LINE) {
                    totalLineEdges++;
                    if (startEdge == null) startEdge = new Edge(false, r, c);
                }
            }
        }

        if (totalLineEdges == 0) return false; // 空回路

        // BFS/DFS沿回路遍历
        Set<String> visited = new HashSet<>();
        int traversed = traverseLoop(startEdge, visited);

        return traversed == totalLineEdges;
    }

    /**
     * 沿回路遍历,返回经过的边数
     */
    int traverseLoop(Edge start, Set<String> visited) {
        int count = 0;
        Edge current = start;
        Edge prev = null;

        while (current != null) {
            String key = current.toString();
            if (visited.contains(key)) break;
            visited.add(key);
            count++;

            // 找到当前边的两个端点
            int[][] endpoints = getEndpoints(current);
            Edge next = null;
            for (int[] ep : endpoints) {
                List<Edge> incident = getIncidentEdges(ep[0], ep[1]);
                for (Edge e : incident) {
                    if (getEdgeValue(e) == LINE && !e.equals(current) && !e.equals(prev)) {
                        next = e;
                        break;
                    }
                }
                if (next != null) break;
            }

            prev = current;
            current = next;
        }

        return count;
    }

    int[][] getEndpoints(Edge e) {
        if (e.isHorizontal) {
            return new int[][]{{e.r, e.c}, {e.r, e.c + 1}};
        } else {
            return new int[][]{{e.r, e.c}, {e.r + 1, e.c}};
        }
    }

    int[][] copyArray(int[][] arr) {
        int[][] copy = new int[arr.length][];
        for (int i = 0; i < arr.length; i++) {
            copy[i] = arr[i].clone();
        }
        return copy;
    }

    /**
     * 打印解答
     */
    void printSolution() {
        System.out.println("=== Slitherlink 解答 ===");
        for (int r = 0; r <= rows; r++) {
            // 打印水平边和点
            for (int c = 0; c <= cols; c++) {
                System.out.print("·");
                if (c < cols) {
                    System.out.print(hEdges[r][c] == LINE ? "───" : "   ");
                }
            }
            System.out.println();

            // 打印垂直边和数字
            if (r < rows) {
                for (int c = 0; c <= cols; c++) {
                    System.out.print(vEdges[r][c] == LINE ? "│" : " ");
                    if (c < cols) {
                        if (numbers[r][c] >= 0) {
                            System.out.print(" " + numbers[r][c] + " ");
                        } else {
                            System.out.print("   ");
                        }
                    }
                }
                System.out.println();
            }
        }
    }

    /**
     * 边类
     */
    static class Edge {
        final boolean isHorizontal;
        final int r, c;

        Edge(boolean isHorizontal, int r, int c) {
            this.isHorizontal = isHorizontal;
            this.r = r;
            this.c = c;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Edge)) return false;
            Edge edge = (Edge) o;
            return isHorizontal == edge.isHorizontal && r == edge.r && c == edge.c;
        }

        @Override
        public int hashCode() {
            return Objects.hash(isHorizontal, r, c);
        }

        @Override
        public String toString() {
            return (isHorizontal ? "H" : "V") + "(" + r + "," + c + ")";
        }
    }

    public static void main(String[] args) {
        // 5x5 示例谜题 (-1表示无数字)
        int[][] puzzle = {
            { 2, 3,-1, 2, 1},
            { 2,-1, 2,-1, 3},
            {-1, 2, 1, 3,-1},
            { 1,-1, 3,-1, 2},
            { 3, 2,-1, 2, 1}
        };

        SlitherlinkSolver solver = new SlitherlinkSolver(puzzle);
        System.out.println("求解中...");

        long start = System.currentTimeMillis();
        boolean solved = solver.solve();
        long end = System.currentTimeMillis();

        if (solved) {
            System.out.println("求解成功!耗时 " + (end - start) + "ms");
            solver.printSolution();
        } else {
            System.out.println("无解!");
        }

        // 更小的测试
        int[][] smallPuzzle = {
            {3, 2},
            {2, 3}
        };

        System.out.println("\n=== 2x2 小谜题 ===");
        SlitherlinkSolver solver2 = new SlitherlinkSolver(smallPuzzle);
        if (solver2.solve()) {
            solver2.printSolution();
        } else {
            System.out.println("无解");
        }
    }
}

算法解析

约束传播的核心思想

Slitherlink 的约束具有局部传播性:一个数字格子的约束会影响其四条边,边的确定又会通过顶点约束影响相邻格子的边。这种连锁反应可以大幅缩小搜索空间。

为何约束传播 + 回溯如此高效

对于 5×5 的 Slitherlink:
– 无约束传播:2^60 种可能(约 10^18)
– 约束传播后:通常只剩下极少数未知边需要分支
– 回溯深度:通常 < 10

顶点度数约束的重要性

顶点度数只能是 0 或 2 的规则是排除非法解的关键:
– 防止分叉(度 > 2)
– 防止死路(度 = 1)
– 结合数字约束,可以快速确定大量边的状态

复杂度分析

指标 复杂度 说明
约束传播 O(R×C) 每轮扫描所有格子和顶点
最坏情况回溯 O(2^E) E=边数,实际远小于此
5×5 谜题 毫秒级 约束传播后搜索空间极小
10×10 谜题 秒级 需要更多分支

扩展思考

  1. 高级约束规则:角点规则、边规则等可以进一步减少分支。
  2. 对称性剪枝:利用棋盘对称性避免重复搜索。
  3. 人机交互求解:将求解器改造为提示系统,逐步揭示推理过程。

总结

Slitherlink 求解器完美演绎了约束满足问题(CSP)的经典解法:约束传播快速缩小搜索空间,回溯搜索处理剩余不确定性。这一组合策略是数独、扫雷、填字游戏等逻辑谜题求解的通用框架。