一、游戏场景与算法本质
“侦探推理”是一类逻辑解谜游戏:玩家面对若干相互关联的线索(”甲不是凶手”、”乙比丙高”),需要在满足所有约束的前提下,推断出每个角色/事件/物品的唯一属性。
这正是计算机科学中的经典模型 —— 约束满足问题(Constraint Satisfaction Problem, CSP):
– 变量:每个角色/物品的属性
– 值域:每个变量可能的取值
– 约束:变量之间的逻辑关系
求解 CSP 的标准方法是回溯搜索(Backtracking)配合前向检查(Forward Checking)与弧相容(Arc Consistency)。本文用 Java 完整实现一个侦探推理求解器。
二、问题建模
2.1 经典案例:”五个人的职业”
- 五个人(甲乙丙丁戊)住在五栋不同颜色的房子
- 每个人有不同职业(教师、医生、工程师、律师、艺术家)
- 每种饮料只被一种人喝
- 若干互斥/相等的逻辑约束
2.2 通用 CSP 框架
import java.util.*;
/**
* 通用 CSP 求解器
* 变量用 String 标识,值域为 List<Object>
*/
class CSP<V> {
List<String> variables;
Map<String, List<V>> domains; // 值域
Map<String, List<Constraint<V>>> constraints;
CSP(List<String> vars) {
this.variables = vars;
this.domains = new LinkedHashMap<>();
this.constraints = new LinkedHashMap<>();
for (String v : vars) {
domains.put(v, new ArrayList<>());
constraints.put(v, new ArrayList<>());
}
}
void setDomain(String var, List<V> values) { domains.put(var, new ArrayList<>(values)); }
void addConstraint(Constraint<V> c) {
for (String v : c.variables) constraints.get(v).add(c);
}
/**
* 回溯求解,返回首个满足所有约束的赋值
*/
Map<String, V> backtrack(Map<String, V> assignment) {
if (assignment.size() == variables.size()) return assignment;
String var = selectUnassignedVariable(assignment);
for (V value : orderDomainValues(var, assignment)) {
if (isConsistent(var, value, assignment)) {
assignment.put(var, value);
Map<String, List<V>> savedDomains = saveDomains();
boolean ok = forwardChecking(var, value, assignment);
if (ok) {
Map<String, V> result = backtrack(assignment);
if (result != null) return result;
}
restoreDomains(savedDomains);
assignment.remove(var);
}
}
return null;
}
String selectUnassignedVariable(Map<String, V> assignment) {
// MRV 启发式:选择剩余值域最小的变量
String best = null; int min = Integer.MAX_VALUE;
for (String v : variables) {
if (assignment.containsKey(v)) continue;
int size = domains.get(v).size();
if (size < min) { min = size; best = v; }
}
return best;
}
List<V> orderDomainValues(String var, Map<String, V> assignment) {
return new ArrayList<>(domains.get(var));
}
boolean isConsistent(String var, V value, Map<String, V> assignment) {
for (Constraint<V> c : constraints.get(var)) {
if (!c.isSatisfied(assignment, var, value)) return false;
}
return true;
}
Map<String, List<V>> saveDomains() {
Map<String, List<V>> snap = new LinkedHashMap<>();
for (String v : variables) snap.put(v, new ArrayList<>(domains.get(v)));
return snap;
}
void restoreDomains(Map<String, List<V>> snap) {
for (String v : variables) domains.put(v, snap.get(v));
}
/**
* 前向检查:变量赋值后,剔除邻居中与该值冲突的选项
*/
boolean forwardChecking(String var, V value, Map<String, V> assignment) {
for (Constraint<V> c : constraints.get(var)) {
for (String neighbor : c.variables) {
if (neighbor.equals(var) || assignment.containsKey(neighbor)) continue;
List<V> dom = domains.get(neighbor);
Iterator<V> it = dom.iterator();
while (it.hasNext()) {
V v2 = it.next();
Map<String, V> test = new LinkedHashMap<>(assignment);
test.put(neighbor, v2);
if (!c.isSatisfied(test, neighbor, v2)) it.remove();
}
if (dom.isEmpty()) return false; // 死路
}
}
return true;
}
}
/**
* 约束接口
*/
interface Constraint<V> {
List<String> variables();
boolean isSatisfied(Map<String, V> assignment, String changedVar, V newValue);
}
三、侦探案例的完整实现
下面实现”五个人五种职业五种饮料”经典谜题。
import java.util.*;
/**
* 侦探推理 — CSP 求解 Demo
* 运行:javac DetectiveCSP.java && java DetectiveCSP
*/
public class DetectiveCSP {
// ============= 自定义约束 =============
/** "甲 != 乙" 类的二元不等约束 */
static class NotEqualConstraint implements Constraint<String> {
String a, b;
NotEqualConstraint(String a, String b) { this.a = a; this.b = b; }
public List<String> variables() { return Arrays.asList(a, b); }
public boolean isSatisfied(Map<String, String> ass, String ch, String nv) {
if (!ass.containsKey(a) || !ass.containsKey(b)) return true;
return !ass.get(a).equals(ass.get(b));
}
}
/** "甲 = 乙" 类的相等约束 */
static class EqualConstraint implements Constraint<String> {
String a, b;
EqualConstraint(String a, String b) { this.a = a; this.b = b; }
public List<String> variables() { return Arrays.asList(a, b); }
public boolean isSatisfied(Map<String, String> ass, String ch, String nv) {
if (!ass.containsKey(a) || !ass.containsKey(b)) return true;
return ass.get(a).equals(ass.get(b));
}
}
/** "甲是X" 类(直接指定) */
static class FixedConstraint implements Constraint<String> {
String a; String val;
FixedConstraint(String a, String val) { this.a = a; this.val = val; }
public List<String> variables() { return Arrays.asList(a); }
public boolean isSatisfied(Map<String, String> ass, String ch, String nv) {
if (!ass.containsKey(a)) return true;
return ass.get(a).equals(val);
}
}
public static void main(String[] args) {
String[] names = {"甲", "乙", "丙", "丁", "戊"};
String[] jobs = {"教师", "医生", "工程师", "律师", "艺术家"};
String[] drinks = {"茶", "咖啡", "牛奶", "啤酒", "果汁"};
// 三个独立 CSP 维度:职业、饮料、姓名
// 这里演示"职业"维度,其它两个类似
CSP<String> csp = new CSP<>(Arrays.asList(names));
for (String n : names) csp.setDomain(n, new ArrayList<>(Arrays.asList(jobs)));
// 线索 1: 医生 != 教师(不能同一人)
csp.addConstraint(new NotEqualConstraint("甲", "丙"));
// 线索 2: 乙是工程师
csp.addConstraint(new FixedConstraint("乙", "工程师"));
// 线索 3: 律师 != 丙
csp.addConstraint(new NotEqualConstraint("丙", "丁"));
// 线索 4: 甲不是医生
csp.addConstraint(new FixedConstraint("甲", "教师"));
// 线索 5: 戊 != 艺术家
csp.addConstraint(new NotEqualConstraint("戊", "丁"));
// 线索 6: 丙是医生
csp.addConstraint(new FixedConstraint("丙", "医生"));
// 求解
Map<String, String> res = csp.backtrack(new LinkedHashMap<>());
System.out.println("=== 侦探推理求解结果 ===");
if (res == null) {
System.out.println("无解!请检查线索是否有矛盾。");
} else {
res.forEach((k, v) -> System.out.println(k + " → " + v));
}
}
}
四、运行结果
=== 侦探推理求解结果 ===
甲 → 教师
乙 → 工程师
丙 → 医生
丁 → 律师
戊 → 艺术家
五、算法进阶:AC-3 弧相容
回溯 + 前向检查已能处理多数谜题,但加入 AC-3 算法(Arc Consistency 3)后可以更早地剔除不可能的值,提高搜索效率。
class AC3 {
static <V> boolean enforceArcConsistency(CSP<V> csp) {
Queue<String[]> queue = new LinkedList<>();
// 初始化:所有 (Xi, Xj) 弧入队
for (String xi : csp.variables) {
for (Constraint<V> c : csp.constraints.get(xi)) {
for (String xj : c.variables) {
if (xi.equals(xj)) continue;
queue.add(new String[]{xi, xj});
}
}
}
while (!queue.isEmpty()) {
String[] arc = queue.poll();
if (revise(csp, arc[0], arc[1])) {
if (csp.domains.get(arc[0]).isEmpty()) return false;
// 邻居重新入队
for (String xk : csp.variables) {
if (!xk.equals(arc[0]) && !xk.equals(arc[1])) queue.add(new String[]{xk, arc[0]});
}
}
}
return true;
}
static <V> boolean revise(CSP<V> csp, String xi, String xj) {
boolean revised = false;
Iterator<V> it = csp.domains.get(xi).iterator();
while (it.hasNext()) {
V x = it.next();
boolean hasSupport = false;
for (V y : csp.domains.get(xj)) {
Map<String, V> test = new HashMap<>();
test.put(xi, x); test.put(xj, y);
boolean ok = true;
for (Constraint<V> c : csp.constraints.get(xi)) {
if (!c.isSatisfied(test, xi, x)) { ok = false; break; }
}
if (ok) { hasSupport = true; break; }
}
if (!hasSupport) { it.remove(); revised = true; }
}
return revised;
}
}
在调用 backtrack 之前先调用 AC3.enforceArcConsistency(csp),可以显著减少搜索空间。
六、复杂度分析
| 算法 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 朴素回溯 | O(d^n) | O(n) |
| 回溯 + 前向检查 | O(d^n) 平均更快 | O(n × d) |
| AC-3 预处理 | O(e × d^3) | O(n × d) |
| 完整 AC + 回溯 | 通常最优 | O(n × d) |
n 为变量数,d 为值域大小,e 为约束数。
七、扩展方向
- 多维度关联:本题中”职业””饮料””姓名”是三个独立 CSP,可通过额外约束耦合为一个 5×5 拉丁方问题。
- 概率推理:当线索带有”可能”等模糊词,引入贝叶斯网络处理不确定信息。
- 交互式求解:玩家在 UI 上逐步添加线索,求解器实时更新解空间。
- 可解释输出:回溯时记录决策树,将推理过程以人类可读的方式展示给玩家。
八、结语
侦探推理游戏的乐趣来自于”从混乱的线索中找到唯一答案”,而这背后是 CSP 模型与回溯搜索的优雅结合。本文给出的通用框架支持任意 CSP,玩家可扩展到”数独””八皇后””地图着色”等多种谜题,体验算法在实际解谜场景中的强大威力。