每日算法 — 使用java实现Futoshiki:约束传播与拓扑排序求解

引言

Futoshiki(不等号数独)是一种起源于日本的逻辑谜题。与标准数独不同,Futoshiki 在 n×n 的棋盘中加入了不等号约束(> 和 <),要求相邻格子之间满足大小关系。本文将用 Java 实现一个完整的 Futoshiki 求解器,核心采用约束传播(Constraint Propagation)与拓扑排序(Topological Sort)来高效求解。

核心算法思路

1. 问题建模

Futoshiki 的约束包括:
– 每行、每列必须恰好包含 1~n 各一次(拉丁方约束)
– 相邻格子间的不等号约束(如 A > B 表示 A 的值必须大于 B)

2. 约束传播

为每个空格维护候选数集合。通过以下规则排除:
– 行/列排除:同行/列已出现的数字不可再用
– 不等号传播:若 A > B,则 A 的候选数必须大于 B 的候选数最小值;B 的候选数必须小于 A 的候选数最大值

3. 拓扑排序检测环路

若不等号约束形成环路(如 A > B > C > A),则谜题无解。可通过构建有向图并检测环路来提前排除矛盾状态。

Java 完整实现

import java.util.*;

/**
 * Futoshiki(不等号数独)求解器
 * 使用约束传播 + 拓扑排序环路检测 + 回溯搜索
 */
public class FutoshikiSolver {
    private int n;
    private int[][] grid;          // 0表示空格
    private boolean[][][] candidates; // candidates[r][c][v]
    // 不等号约束: inequalities[r][c] 存储从(r,c)出发的约束
    // 每个约束为 int[]{nr, nc, type}, type=1 表示 grid[r][c] > grid[nr][nc], type=-1 表示 <
    private List<int[]>[][] inequalities;

    public FutoshikiSolver(int n) {
        this.n = n;
        this.grid = new int[n][n];
        this.candidates = new boolean[n][n][n + 1];
        this.inequalities = new ArrayList[n][n];
        for (int r = 0; r < n; r++) {
            for (int c = 0; c < n; c++) {
                grid[r][c] = 0;
                for (int v = 1; v <= n; v++) candidates[r][c][v] = true;
                inequalities[r][c] = new ArrayList<>();
            }
        }
    }

    public void setCell(int r, int c, int v) {
        if (v >= 1 && v <= n) {
            grid[r][c] = v;
            for (int i = 0; i < n; i++) {
                candidates[r][i][v] = false;
                candidates[i][c][v] = false;
            }
        }
    }

    /**
     * 添加不等号约束
     * @param type 1表示(r1,c1)>(r2,c2), -1表示(r1,c1)<(r2,c2)
     */
    public void addInequality(int r1, int c1, int r2, int c2, int type) {
        inequalities[r1][c1].add(new int[]{r2, c2, type});
        inequalities[r2][c2].add(new int[]{r1, c1, -type});
    }

    /**
     * 根据不等号约束排除候选数
     */
    private boolean propagateInequalities() {
        boolean changed = false;
        for (int r = 0; r < n; r++) {
            for (int c = 0; c < n; c++) {
                if (grid[r][c] != 0) continue;
                for (int[] ineq : inequalities[r][c]) {
                    int nr = ineq[0], nc = ineq[1], type = ineq[2];
                    // type=1: grid[r][c] > grid[nr][nc]
                    // type=-1: grid[r][c] < grid[nr][nc]
                    int otherMin = getMinCandidate(nr, nc);
                    int otherMax = getMaxCandidate(nr, nc);
                    if (type == 1) {
                        // 当前格必须大于对方,排除 <= otherMax 的候选值
                        for (int v = 1; v <= otherMax; v++) {
                            if (candidates[r][c][v]) {
                                candidates[r][c][v] = false;
                                changed = true;
                            }
                        }
                    } else {
                        // 当前格必须小于对方,排除 >= otherMin 的候选值
                        for (int v = otherMin; v <= n; v++) {
                            if (candidates[r][c][v]) {
                                candidates[r][c][v] = false;
                                changed = true;
                            }
                        }
                    }
                }
            }
        }
        return changed;
    }

    private int getMinCandidate(int r, int c) {
        if (grid[r][c] > 0) return grid[r][c];
        for (int v = 1; v <= n; v++) {
            if (candidates[r][c][v]) return v;
        }
        return n + 1;
    }

    private int getMaxCandidate(int r, int c) {
        if (grid[r][c] > 0) return grid[r][c];
        for (int v = n; v >= 1; v--) {
            if (candidates[r][c][v]) return v;
        }
        return 0;
    }

    /**
     * 检测不等号约束图中是否存在环路
     * 若存在环路则无解
     */
    private boolean hasCycle() {
        // 为每个未填格子建立节点,进行拓扑排序
        // 简化:仅对涉及未填格子的约束建图
        Map<String, Integer> nodeId = new HashMap<>();
        List<String> nodes = new ArrayList<>();
        for (int r = 0; r < n; r++) {
            for (int c = 0; c < n; c++) {
                if (grid[r][c] == 0) {
                    String key = r + "," + c;
                    nodeId.put(key, nodes.size());
                    nodes.add(key);
                }
            }
        }
        int m = nodes.size();
        if (m == 0) return false;

        List<Integer>[] graph = new ArrayList[m];
        int[] inDegree = new int[m];
        for (int i = 0; i < m; i++) graph[i] = new ArrayList<>();

        for (int r = 0; r < n; r++) {
            for (int c = 0; c < n; c++) {
                for (int[] ineq : inequalities[r][c]) {
                    int nr = ineq[0], nc = ineq[1], type = ineq[2];
                    if (grid[r][c] == 0 && grid[nr][nc] == 0) {
                        String from = r + "," + c;
                        String to = nr + "," + nc;
                        // type=1: from > to, 即 to -> from 的有向边(to要先确定上限)
                        // 实际上我们建"必须小于"的边
                        if (type == 1) {
                            // from > to, 所以 to 的候选值影响 from 的下界
                            // 简化为:to 必须在 from 之前考虑
                            int u = nodeId.get(to);
                            int v = nodeId.get(from);
                            graph[u].add(v);
                            inDegree[v]++;
                        } else {
                            int u = nodeId.get(from);
                            int v = nodeId.get(to);
                            graph[u].add(v);
                            inDegree[v]++;
                        }
                    }
                }
            }
        }

        Queue<Integer> q = new LinkedList<>();
        for (int i = 0; i < m; i++) {
            if (inDegree[i] == 0) q.add(i);
        }
        int count = 0;
        while (!q.isEmpty()) {
            int u = q.poll();
            count++;
            for (int v : graph[u]) {
                inDegree[v]--;
                if (inDegree[v] == 0) q.add(v);
            }
        }
        return count != m; // 若无法遍历所有节点,说明有环
    }

    private boolean applyNakedSingles() {
        boolean changed = false;
        for (int r = 0; r < n; r++) {
            for (int c = 0; c < n; c++) {
                if (grid[r][c] != 0) continue;
                int count = 0, val = -1;
                for (int v = 1; v <= n; v++) {
                    if (candidates[r][c][v]) {
                        count++;
                        val = v;
                    }
                }
                if (count == 1) {
                    fillCell(r, c, val);
                    changed = true;
                } else if (count == 0) {
                    return false; // 矛盾
                }
            }
        }
        return changed;
    }

    private void fillCell(int r, int c, int v) {
        grid[r][c] = v;
        for (int i = 0; i < n; i++) {
            candidates[r][i][v] = false;
            candidates[i][c][v] = false;
        }
    }

    private boolean propagate() {
        while (true) {
            boolean changed = false;
            if (applyNakedSingles()) changed = true;
            if (!isValid()) return false;
            if (propagateInequalities()) changed = true;
            if (!isValid()) return false;
            if (hasCycle()) return false;
            if (!changed) break;
        }
        return true;
    }

    private boolean isValid() {
        for (int r = 0; r < n; r++) {
            for (int c = 0; c < n; c++) {
                if (grid[r][c] == 0) {
                    boolean has = false;
                    for (int v = 1; v <= n; v++) {
                        if (candidates[r][c][v]) {
                            has = true;
                            break;
                        }
                    }
                    if (!has) return false;
                }
            }
        }
        return true;
    }

    private boolean isComplete() {
        for (int r = 0; r < n; r++) {
            for (int c = 0; c < n; c++) {
                if (grid[r][c] == 0) return false;
            }
        }
        return true;
    }

    private boolean solve() {
        if (isComplete()) return true;

        int[] best = null;
        int minCount = n + 1;
        for (int r = 0; r < n; r++) {
            for (int c = 0; c < n; c++) {
                if (grid[r][c] != 0) continue;
                int count = 0;
                for (int v = 1; v <= n; v++) {
                    if (candidates[r][c][v]) count++;
                }
                if (count < minCount) {
                    minCount = count;
                    best = new int[]{r, c};
                }
            }
        }
        if (best == null || minCount == 0) return false;

        int r = best[0], c = best[1];
        int[][] gridBackup = new int[n][n];
        boolean[][][] candBackup = new boolean[n][n][n + 1];
        for (int i = 0; i < n; i++) {
            System.arraycopy(grid[i], 0, gridBackup[i], 0, n);
            for (int j = 0; j < n; j++) {
                System.arraycopy(candidates[i][j], 0, candBackup[i][j], n + 1);
            }
        }

        for (int v = 1; v <= n; v++) {
            if (!candidates[r][c][v]) continue;
            fillCell(r, c, v);
            if (propagate() && solve()) return true;
            // 恢复
            for (int i = 0; i < n; i++) {
                System.arraycopy(gridBackup[i], 0, grid[i], 0, n);
                for (int j = 0; j < n; j++) {
                    System.arraycopy(candBackup[i][j], 0, candidates[i][j], n + 1);
                }
            }
        }
        return false;
    }

    public boolean solvePuzzle() {
        if (!propagate()) return false;
        return solve();
    }

    public void printGrid() {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                System.out.printf("%2d ", grid[i][j]);
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        FutoshikiSolver solver = new FutoshikiSolver(5);
        // 预填部分数字
        solver.setCell(0, 0, 1);
        solver.setCell(4, 4, 5);
        // 添加不等号约束: (r1,c1) > (r2,c2)
        solver.addInequality(0, 0, 0, 1, 1);   // (0,0) > (0,1)
        solver.addInequality(1, 2, 1, 1, 1);   // (1,2) > (1,1)
        solver.addInequality(2, 3, 2, 2, 1);   // (2,3) > (2,2)
        solver.addInequality(3, 1, 3, 0, -1);  // (3,1) < (3,0)

        System.out.println("初始棋盘:");
        solver.printGrid();

        long start = System.currentTimeMillis();
        if (solver.solvePuzzle()) {
            System.out.println("\n求解结果:");
            solver.printGrid();
            System.out.println("\n耗时:" + (System.currentTimeMillis() - start) + " ms");
        } else {
            System.out.println("无解!");
        }
    }
}

算法复杂度分析

  • 约束传播:每次循环 O(n² × k),k 为不等号数量。
  • 拓扑排序检测:O(n² + k)。
  • 回溯搜索:最坏 O(n^(n²)),但约束传播大幅剪枝。
  • 空间复杂度:O(n³ + k)。

扩展思考

  1. 不等号链推导:A > B > C 可进一步推出 A > C,增强传播能力。
  2. X-Wing / Swordfish:数独中的高级技巧同样适用于 Futoshiki 的行/列约束。
  3. 随机生成:先生成完整拉丁方,再随机添加不等号约束并挖空,确保唯一解。

小结

本文通过 Java 实现了 Futoshiki 求解器,核心采用约束传播、拓扑排序环路检测与回溯搜索。不等号约束的加入使得问题比标准数独更具挑战性,代码中展示了如何将图论算法(拓扑排序)融入逻辑谜题求解。