每日算法 — 使用java实现逻辑谜题:命题逻辑约束满足与回溯求解框架

引言

约束满足问题(Constraint Satisfaction Problem, CSP)是人工智能和运筹学中的核心问题类型。从数独、填字游戏到电路设计验证,CSP的形式化框架能够统一描述这些看似不同的问题。命题逻辑约束满足是最基础的CSP形式,它将问题转化为布尔变量的合取范式(CNF),通过系统化的搜索寻找满足所有约束的赋值。

本文将用Java构建一个通用的回溯求解框架,支持单元传播(Unit Propagation)和纯文字消除(Pure Literal Elimination)等优化技术,并以经典的逻辑谜题为例展示其威力。

算法原理

命题逻辑基础

合取范式(CNF)是由若干子句(clause)的合取(AND)构成的公式,每个子句是若干文字(literal)的析取(OR)。例如:

$$(x_1 \lor \neg x_2) \land (\neg x_1 \lor x_3 \lor x_4) \land (x_2 \lor \neg x_3)$$

DPLL算法(Davis-Putnam-Logemann-Loveland)是求解SAT问题的经典回溯算法:

  1. 单元传播:如果某个子句只剩一个未赋值的文字,则该文字必须为真
  2. 纯文字消除:如果某个变量在所有子句中只以正或只以负形式出现,可直接赋值
  3. 分支选择:选择一个未赋值变量,分别尝试赋值为真/假,递归求解
  4. 冲突回溯:如果某分支导致矛盾,回溯到上一决策点尝试另一分支

回溯框架设计

function DPLL(formula, assignment):
    formula = UnitPropagate(formula, assignment)
    formula = PureLiteralEliminate(formula, assignment)

    if formula has empty clause: return UNSAT
    if formula has no clauses: return SAT

    var = ChooseUnassignedVariable(formula)
    if DPLL(formula ∧ var, assignment ∪ {var=true}): return SAT
    if DPLL(formula ∧ ¬var, assignment ∪ {var=false}): return SAT
    return UNSAT

Java实现

import java.util.*;

/**
 * 命题逻辑SAT求解器:基于DPLL算法的回溯框架
 * 支持单元传播和纯文字消除优化
 */
public class DPLLSolver {

    /**
     * 文字:带符号的变量
     * 正数表示正文字,负数表示负文字
     */
    static class Literal {
        int var; // 变量编号(从1开始)
        boolean positive;

        Literal(int var, boolean positive) {
            this.var = var;
            this.positive = positive;
        }

        int toInt() {
            return positive ? var : -var;
        }

        Literal negate() {
            return new Literal(var, !positive);
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Literal)) return false;
            Literal other = (Literal) o;
            return var == other.var && positive == other.positive;
        }

        @Override
        public int hashCode() {
            return Objects.hash(var, positive);
        }

        @Override
        public String toString() {
            return positive ? "x" + var : "¬x" + var;
        }
    }

    /**
     * 子句:文字的析取
     */
    static class Clause {
        Set<Literal> literals = new HashSet<>();

        Clause(Collection<Literal> lits) {
            literals.addAll(lits);
        }

        Clause(Clause other) {
            literals.addAll(other.literals);
        }

        boolean isEmpty() {
            return literals.isEmpty();
        }

        boolean isUnit() {
            return literals.size() == 1;
        }

        Literal getUnitLiteral() {
            return literals.iterator().next();
        }

        @Override
        public String toString() {
            if (literals.isEmpty()) return "⊥";
            return "(" + String.join(" ∨ ", 
                literals.stream().map(Literal::toString).toArray(String[]::new)) + ")";
        }
    }

    /**
     * CNF公式:子句的合取
     */
    static class CNF {
        List<Clause> clauses = new ArrayList<>();
        int numVars = 0;

        CNF() {}

        CNF(CNF other) {
            for (Clause c : other.clauses) {
                clauses.add(new Clause(c));
            }
            numVars = other.numVars;
        }

        void addClause(Clause c) {
            clauses.add(c);
            for (Literal l : c.literals) {
                numVars = Math.max(numVars, l.var);
            }
        }

        boolean hasEmptyClause() {
            return clauses.stream().anyMatch(Clause::isEmpty);
        }

        boolean isEmpty() {
            return clauses.isEmpty();
        }

        @Override
        public String toString() {
            if (clauses.isEmpty()) return "⊤";
            return String.join(" ∧ ", 
                clauses.stream().map(Clause::toString).toArray(String[]::new));
        }
    }

    private int decisionCount = 0;
    private int propagationCount = 0;

    /**
     * 单元传播:强制满足单子句
     * 如果子句只剩一个未赋值文字,则该文字必须为真
     */
    public CNF unitPropagate(CNF formula, Map<Integer, Boolean> assignment) {
        boolean changed;
        CNF result = new CNF(formula);

        do {
            changed = false;
            List<Clause> toRemove = new ArrayList<>();

            for (Clause clause : result.clauses) {
                // 收集未赋值文字
                List<Literal> unassigned = new ArrayList<>();
                boolean satisfied = false;

                for (Literal lit : clause.literals) {
                    if (!assignment.containsKey(lit.var)) {
                        unassigned.add(lit);
                    } else if (assignment.get(lit.var) == lit.positive) {
                        satisfied = true;
                        break;
                    }
                }

                if (satisfied) {
                    toRemove.add(clause);
                } else if (unassigned.isEmpty()) {
                    // 所有文字都被赋值且都不满足 → 空子句(冲突)
                    return new CNF(); // 返回空公式表示冲突
                } else if (unassigned.size() == 1) {
                    // 单子句:强制赋值
                    Literal unit = unassigned.get(0);
                    assignment.put(unit.var, unit.positive);
                    propagationCount++;
                    changed = true;
                }
            }

            result.clauses.removeAll(toRemove);

            // 重新过滤已满足的子句
            if (changed) {
                result.clauses.removeIf(c -> {
                    for (Literal lit : c.literals) {
                        if (assignment.containsKey(lit.var) && 
                            assignment.get(lit.var) == lit.positive) {
                            return true;
                        }
                    }
                    return false;
                });
            }
        } while (changed);

        return result;
    }

    /**
     * 纯文字消除:如果变量只以单一极性出现,可直接赋值
     */
    public CNF pureLiteralEliminate(CNF formula, Map<Integer, Boolean> assignment) {
        Map<Integer, Boolean> polarity = new HashMap<>();

        for (Clause clause : formula.clauses) {
            for (Literal lit : clause.literals) {
                if (assignment.containsKey(lit.var)) continue;

                if (!polarity.containsKey(lit.var)) {
                    polarity.put(lit.var, lit.positive);
                } else if (polarity.get(lit.var) != lit.positive) {
                    polarity.put(lit.var, null); // 混合极性
                }
            }
        }

        CNF result = new CNF(formula);
        for (Map.Entry<Integer, Boolean> entry : polarity.entrySet()) {
            if (entry.getValue() != null) {
                assignment.put(entry.getKey(), entry.getValue());
                // 移除包含该纯文字的子句
                result.clauses.removeIf(c -> c.literals.stream()
                    .anyMatch(l -> l.var == entry.getKey() && l.positive == entry.getValue()));
            }
        }

        return result;
    }

    /**
     * DPLL核心算法
     */
    public boolean solve(CNF formula, Map<Integer, Boolean> assignment) {
        // 应用单元传播
        formula = unitPropagate(formula, assignment);
        if (formula == null || formula.clauses == null) return false; // 冲突

        // 应用纯文字消除
        formula = pureLiteralEliminate(formula, assignment);

        // 检查终止条件
        if (formula.hasEmptyClause()) return false; // UNSAT
        if (formula.isEmpty()) return true; // SAT

        // 选择分支变量(启发式:选择出现次数最多的变量)
        int branchVar = chooseVariable(formula, assignment);
        if (branchVar == -1) return true;

        decisionCount++;

        // 尝试赋值为true
        Map<Integer, Boolean> copy1 = new HashMap<>(assignment);
        copy1.put(branchVar, true);
        if (solve(new CNF(formula), copy1)) {
            assignment.putAll(copy1);
            return true;
        }

        // 尝试赋值为false
        Map<Integer, Boolean> copy2 = new HashMap<>(assignment);
        copy2.put(branchVar, false);
        if (solve(new CNF(formula), copy2)) {
            assignment.putAll(copy2);
            return true;
        }

        return false;
    }

    /**
     * 选择下一个分支变量(MOM启发式:最大出现次数)
     */
    private int chooseVariable(CNF formula, Map<Integer, Boolean> assignment) {
        Map<Integer, Integer> frequency = new HashMap<>();

        for (Clause clause : formula.clauses) {
            if (clause.literals.size() <= 2) { // 优先从短子句中选择
                for (Literal lit : clause.literals) {
                    if (!assignment.containsKey(lit.var)) {
                        frequency.merge(lit.var, 1, Integer::sum);
                    }
                }
            }
        }

        if (frequency.isEmpty()) {
            for (Clause clause : formula.clauses) {
                for (Literal lit : clause.literals) {
                    if (!assignment.containsKey(lit.var)) {
                        return lit.var;
                    }
                }
            }
            return -1;
        }

        return Collections.max(frequency.entrySet(), Map.Entry.comparingByValue()).getKey();
    }

    // ==================== 逻辑谜题建模示例 ====================

    /**
     * 构造一个简单的逻辑谜题:
     * 三人A、B、C中有一人说真话,两人说假话。
     * A说:"B在说谎"
     * B说:"C在说谎"
     * C说:"A和B都在说谎"
     * 谁是说真话的人?
     */
    public static CNF buildTruthTellerPuzzle() {
        CNF cnf = new CNF();
        // x1=A说真话, x2=B说真话, x3=C说真话

        // 恰好一个说真话:至少一个 ∧ 至多一个
        // 至少一个
        cnf.addClause(new Clause(Arrays.asList(
            new Literal(1, true), new Literal(2, true), new Literal(3, true))));

        // 至多一个:(¬x1 ∨ ¬x2) ∧ (¬x1 ∨ ¬x3) ∧ (¬x2 ∨ ¬x3)
        cnf.addClause(new Clause(Arrays.asList(new Literal(1, false), new Literal(2, false))));
        cnf.addClause(new Clause(Arrays.asList(new Literal(1, false), new Literal(3, false))));
        cnf.addClause(new Clause(Arrays.asList(new Literal(2, false), new Literal(3, false))));

        // A说"B在说谎":x1 → ¬x2,即 ¬x1 ∨ ¬x2
        cnf.addClause(new Clause(Arrays.asList(new Literal(1, false), new Literal(2, false))));

        // B说"C在说谎":x2 → ¬x3,即 ¬x2 ∨ ¬x3
        cnf.addClause(new Clause(Arrays.asList(new Literal(2, false), new Literal(3, false))));

        // C说"A和B都在说谎":x3 → (¬x1 ∧ ¬x2),即 (¬x3 ∨ ¬x1) ∧ (¬x3 ∨ ¬x2)
        cnf.addClause(new Clause(Arrays.asList(new Literal(3, false), new Literal(1, false))));
        cnf.addClause(new Clause(Arrays.asList(new Literal(3, false), new Literal(2, false))));

        return cnf;
    }

    /**
     * 构造2x2数独约束(简化版)
     */
    public static CNF buildMiniSudoku() {
        CNF cnf = new CNF();
        // x_ij_k 表示格子(i,j)填数字k
        // 简化:用1-4表示2x2数独的四个格子,每个格子一个变量
        // 实际数独约束较复杂,这里展示框架能力

        // 每个格子至少一个数字
        for (int i = 1; i <= 2; i++) {
            for (int j = 1; j <= 2; j++) {
                List<Literal> atLeastOne = new ArrayList<>();
                for (int k = 1; k <= 2; k++) {
                    int var = (i-1)*4 + (j-1)*2 + k;
                    atLeastOne.add(new Literal(var, true));
                }
                cnf.addClause(new Clause(atLeastOne));
            }
        }

        return cnf;
    }

    public static void main(String[] args) {
        // 测试1:逻辑谜题
        System.out.println("=== 逻辑谜题求解:谁在说真话? ===");
        CNF puzzle = buildTruthTellerPuzzle();
        System.out.println("CNF公式: " + puzzle);

        DPLLSolver solver = new DPLLSolver();
        Map<Integer, Boolean> assignment = new HashMap<>();
        boolean sat = solver.solve(puzzle, assignment);

        System.out.println("可满足性: " + (sat ? "SAT" : "UNSAT"));
        if (sat) {
            System.out.println("解: " + assignment);
            System.out.println("A说真话: " + assignment.get(1));
            System.out.println("B说真话: " + assignment.get(2));
            System.out.println("C说真话: " + assignment.get(3));
        }
        System.out.println("决策次数: " + solver.decisionCount);
        System.out.println("传播次数: " + solver.propagationCount);

        // 测试2:简单SAT实例
        System.out.println("\n=== 简单SAT测试 ===");
        CNF simple = new CNF();
        simple.addClause(new Clause(Arrays.asList(new Literal(1, true), new Literal(2, true))));
        simple.addClause(new Clause(Arrays.asList(new Literal(1, false), new Literal(2, true))));
        simple.addClause(new Clause(Arrays.asList(new Literal(1, false), new Literal(2, false))));

        solver = new DPLLSolver();
        assignment = new HashMap<>();
        sat = solver.solve(simple, assignment);
        System.out.println("公式: (x1 ∨ x2) ∧ (¬x1 ∨ x2) ∧ (¬x1 ∨ ¬x2)");
        System.out.println("可满足性: " + (sat ? "SAT" : "UNSAT"));
        if (sat) System.out.println("解: " + assignment);
    }
}

算法分析

时间复杂度

DPLL算法在最坏情况下需要遍历 $2^n$ 个赋值空间,时间复杂度为 $O(2^n)$。但通过单元传播和纯文字消除,实际运行中常大幅剪枝。

现代SAT求解器(如MiniSat、Glucose)在DPLL基础上增加了:
– 冲突驱动子句学习(CDCL):从冲突中学习新的子句
– 变量状态独立衰减和(VSIDS)启发式:动态调整变量优先级
– 重启策略:定期重置搜索状态

这些优化使得现代求解器可以处理数百万变量的问题。

空间复杂度

基础DPLL的空间复杂度为 $O(n + m)$,其中 $n$ 是变量数,$m$ 是子句数。CDCL版本需要额外空间存储学习子句。

完备性与正确性

DPLL算法是可靠且完备的:
– 可靠性:如果算法返回SAT,则确实存在满足赋值
– 完备性:如果公式可满足,算法一定能在有限步内找到解(或证明UNSAT)

扩展应用

数独求解

标准9×9数独可以编码为约729个变量和数千个子句的SAT问题。使用高效的SAT求解器,即使在最坏情况下也能在毫秒级求解。

电路验证

硬件设计中的等价性检验、死锁检测都可以转化为SAT问题。Intel和AMD的处理器设计流程中,SAT求解器是不可或缺的验证工具。

规划与调度

AI规划问题(如动作序列规划)和工厂作业调度都可以通过CSP/SAT框架求解,利用约束传播大幅缩减搜索空间。

总结

本文实现了一个基于DPLL算法的通用命题逻辑SAT求解器,支持单元传播和纯文字消除等核心优化。从逻辑谜题到数独、从电路验证到AI规划,约束满足框架展现了强大的问题建模能力。理解DPLL的回溯搜索机制,不仅是掌握SAT求解的基础,更是理解AI中搜索与推理本质的关键一步。在实际工程中,建议将自定义求解器作为学习工具,生产环境应使用经过高度优化的工业级求解器如MiniSat或Z3。