引言
剧本杀是一种依赖逻辑推理和信息交换的社交游戏,玩家需要通过线索分析、证言比对和逻辑推演找出真相。本文将剧本杀推理过程抽象为约束满足问题(CSP),用Java实现辅助推理系统,核心算法包括命题逻辑约束传播、贝叶斯概率推理更新,以及基于DPLL的回溯求解框架。
核心算法原理
1. 命题逻辑建模
将剧本杀中的每个事实建模为布尔变量:
– ( A_i ):嫌疑人 ( i ) 是凶手
– ( W_j ):线索 ( j ) 为真
– ( T_k ):时间线 ( k ) 成立
约束表示为子句集合,如 “只有一人是凶手”:
[ (A_1 \lor A_2 \lor \dots \lor A_n) \land \bigwedge_{i \neq j} (\neg A_i \lor \neg A_j) ]
2. 约束传播:单元传播
若子句中只有一个未赋值文字,则该文字必须为真。反复应用此规则可缩小搜索空间。
3. 贝叶斯概率推理
给定新证据 ( E ),更新各嫌疑人的后验概率:
[ P(A_i | E) = \frac{P(E | A_i) \cdot P(A_i)}{P(E)} ]
4. DPLL回溯求解
结合单元传播、纯文字消去和回溯搜索,判定约束系统的可满足性。
Java完整实现
import java.util.*;
/**
* 剧本杀逻辑推理引擎
* 核心算法:命题逻辑约束传播 + 贝叶斯概率更新 + DPLL回溯求解
*/
public class MurderMysterySolver {
// 命题变量:嫌疑人是否为凶手
private final int suspectCount;
// 约束子句列表,每个子句是文字列表(正数表示正文字,负数表示负文字)
private final List<List<Integer>> clauses;
// 当前赋值:null=未赋值, true/false
private Boolean[] assignment;
// 先验概率
private final double[] priorProb;
// 后验概率
private double[] posteriorProb;
public MurderMysterySolver(int suspectCount) {
this.suspectCount = suspectCount;
this.clauses = new ArrayList<>();
this.priorProb = new double[suspectCount];
this.posteriorProb = new double[suspectCount];
// 初始均匀先验
for (int i = 0; i < suspectCount; i++) {
priorProb[i] = 1.0 / suspectCount;
posteriorProb[i] = priorProb[i];
}
// 添加"只有一人是凶手"约束
// 至少一人:(A1 || A2 || ... || An)
List<Integer> atLeastOne = new ArrayList<>();
for (int i = 1; i <= suspectCount; i++) atLeastOne.add(i);
clauses.add(atLeastOne);
// 至多一人:对所有 i≠j,(!Ai || !Aj)
for (int i = 1; i <= suspectCount; i++) {
for (int j = i + 1; j <= suspectCount; j++) {
List<Integer> atMostOne = new ArrayList<>();
atMostOne.add(-i);
atMostOne.add(-j);
clauses.add(atMostOne);
}
}
}
/**
* 添加新线索约束
* @param clause 子句,如 [1, -2] 表示 "嫌疑人1是凶手 或 嫌疑人2不是凶手"
*/
public void addClue(List<Integer> clause) {
clauses.add(new ArrayList<>(clause));
}
/**
* 单元传播:反复寻找单位子句并赋值
* @param assign 当前赋值数组
* @param clauses 子句集合
* @return 若出现空子句则返回false(矛盾),否则返回true
*/
private boolean unitPropagate(Boolean[] assign, List<List<Integer>> clauses) {
boolean changed = true;
while (changed) {
changed = false;
for (List<Integer> clause : clauses) {
int unassignedCount = 0;
int lastUnassigned = 0;
boolean satisfied = false;
for (int lit : clause) {
int var = Math.abs(lit);
if (assign[var - 1] == null) {
unassignedCount++;
lastUnassigned = lit;
} else {
boolean value = assign[var - 1];
if ((lit > 0 && value) || (lit < 0 && !value)) {
satisfied = true;
break;
}
}
}
if (satisfied) continue;
if (unassignedCount == 0) return false; // 空子句,矛盾
if (unassignedCount == 1) {
// 单元子句,强制赋值
int var = Math.abs(lastUnassigned);
assign[var - 1] = lastUnassigned > 0;
changed = true;
}
}
}
return true;
}
/**
* DPLL回溯求解
* @param assign 当前赋值
* @param clauses 剩余子句
* @return 是否存在满足赋值
*/
private boolean dpll(Boolean[] assign, List<List<Integer>> clauses) {
Boolean[] newAssign = assign.clone();
if (!unitPropagate(newAssign, clauses)) return false;
// 检查是否所有子句满足
boolean allSatisfied = true;
for (List<Integer> clause : clauses) {
boolean sat = false;
for (int lit : clause) {
int var = Math.abs(lit);
if (newAssign[var - 1] != null) {
if ((lit > 0 && newAssign[var - 1]) || (lit < 0 && !newAssign[var - 1])) {
sat = true;
break;
}
} else {
allSatisfied = false;
}
}
if (!sat && allSatisfied) return false;
}
if (allSatisfied) {
this.assignment = newAssign;
return true;
}
// 选择第一个未赋值变量
int branchVar = -1;
for (int i = 0; i < suspectCount; i++) {
if (newAssign[i] == null) { branchVar = i; break; }
}
if (branchVar == -1) return true;
// 分支:先尝试true
newAssign[branchVar] = true;
if (dpll(newAssign, clauses)) return true;
// 回溯,尝试false
newAssign[branchVar] = false;
return dpll(newAssign, clauses);
}
/**
* 求解当前约束系统
*/
public boolean solve() {
Boolean[] initAssign = new Boolean[suspectCount];
return dpll(initAssign, clauses);
}
/**
* 贝叶斯概率更新
* @param evidence 证据:evidence[i] = 0/1/null 表示第i个线索为假/真/未知
* @param likelihoods 似然矩阵:likelihoods[i][j] = P(证据j | 嫌疑人i是凶手)
*/
public void updateProbabilities(int[] evidence, double[][] likelihoods) {
int clueCount = evidence.length;
double[] numerator = new double[suspectCount];
double denominator = 0;
for (int i = 0; i < suspectCount; i++) {
double likelihoodProduct = 1.0;
for (int j = 0; j < clueCount; j++) {
if (evidence[j] == 1) {
likelihoodProduct *= likelihoods[i][j];
} else if (evidence[j] == 0) {
likelihoodProduct *= (1 - likelihoods[i][j]);
}
}
numerator[i] = likelihoodProduct * priorProb[i];
denominator += numerator[i];
}
for (int i = 0; i < suspectCount; i++) {
posteriorProb[i] = denominator > 0 ? numerator[i] / denominator : priorProb[i];
}
}
/**
* 获取当前最可能的凶手
*/
public int getMostLikelySuspect() {
int best = 0;
for (int i = 1; i < suspectCount; i++) {
if (posteriorProb[i] > posteriorProb[best]) best = i;
}
return best;
}
/**
* 打印推理结果
*/
public void printInference() {
System.out.println("=== 剧本杀逻辑推理引擎 ===\n");
System.out.println("约束子句:");
for (List<Integer> clause : clauses) {
System.out.println(" " + clause);
}
System.out.println("\nDPLL求解结果:");
if (solve()) {
System.out.println(" 约束系统可满足");
for (int i = 0; i < suspectCount; i++) {
System.out.printf(" 嫌疑人 %d: %s%n", i + 1, assignment[i] != null ? assignment[i] : "未知");
}
} else {
System.out.println(" 约束系统矛盾(存在虚假线索)");
}
System.out.println("\n贝叶斯概率推理:");
for (int i = 0; i < suspectCount; i++) {
System.out.printf(" 嫌疑人 %d 概率: %.4f%n", i + 1, posteriorProb[i]);
}
System.out.printf("\n最可能凶手: 嫌疑人 %d (概率 %.4f)%n",
getMostLikelySuspect() + 1, posteriorProb[getMostLikelySuspect()]);
}
public static void main(String[] args) {
// 5人剧本杀
MurderMysterySolver solver = new MurderMysterySolver(5);
// 添加线索约束
// 线索1:如果嫌疑人1不是凶手,则嫌疑人3有不在场证明
solver.addClue(Arrays.asList(1, -3));
// 线索2:嫌疑人2和嫌疑人4至少一人有动机
solver.addClue(Arrays.asList(2, 4));
// 线索3:如果嫌疑人5是凶手,则指纹证据为真(由其他约束处理)
solver.addClue(Arrays.asList(-5, 5)); // 恒真,占位
// 证据:3条线索 [真, 假, 真]
int[] evidence = {1, 0, 1};
// 似然矩阵:likelihoods[i][j] = P(证据j | 嫌疑人i是凶手)
double[][] likelihoods = {
{0.9, 0.2, 0.7}, // 嫌疑人1
{0.3, 0.8, 0.4}, // 嫌疑人2
{0.2, 0.9, 0.3}, // 嫌疑人3
{0.6, 0.5, 0.8}, // 嫌疑人4
{0.4, 0.3, 0.9} // 嫌疑人5
};
solver.updateProbabilities(evidence, likelihoods);
solver.printInference();
}
}
复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 单元传播 | ( O(C \cdot L) ),( C )为子句数,( L )为平均长度 | ( O(V) ),( V )为变量数 |
| DPLL回溯 | 最坏 ( O(2^V) ),实际远小于 | ( O(V + C) ) |
| 贝叶斯更新 | ( O(S \cdot E) ),( S )为嫌疑人数量,( E )为证据数 | ( O(S) ) |
总结
本文构建了剧本杀辅助推理引擎,核心创新包括:
1. 命题逻辑建模:将游戏事实转化为布尔约束系统
2. DPLL高效求解:单元传播大幅剪枝,快速判定线索一致性
3. 贝叶斯概率更新:综合多条证据量化各嫌疑人嫌疑度
4. 约束传播:自动推导隐藏信息,辅助玩家逻辑推演
该系统可扩展应用于侦探小说推理、法律证据分析等更广泛的逻辑推理场景。