Minisat安装到实战:从布尔公式解析到SAT求解完整流程
如果你对计算机科学中的逻辑推理、形式化验证或者程序分析有过接触,那么“布尔可满足性问题”(SAT)这个词对你来说一定不陌生。它听起来很学术,甚至有些枯燥,但它的影响力却无处不在——从芯片设计、软件测试到人工智能规划,SAT求解器都是幕后那个默默工作的“解题大师”。然而,对于大多数开发者而言,SAT求解器就像是一个黑盒:输入一堆约束,输出“是”或“否”,中间的过程仿佛魔法。
今天,我们就来亲手拆解这个黑盒。我们不满足于仅仅调用一个API,而是要从零开始,深入SAT求解的完整工具链。我们将聚焦于Minisat,这个在学术界和工业界都备受推崇的、高效且轻量级的SAT求解器。通过从系统安装、CNF文件编写、命令求解到结果解析的全流程实操,你将不再只是SAT的用户,而是能理解其内部运作机制的实践者。无论你是想验证自己的算法模型,还是希望为复杂系统寻找一种可靠的约束求解方案,这篇文章都将为你提供一条清晰的路径。
1. 理解SAT问题与Minisat的定位
在深入命令行之前,我们有必要先厘清几个核心概念。SAT(Boolean Satisfiability Problem),即布尔可满足性问题,是计算机科学中一个奠基性的NP完全问题。它的定义非常简洁:给定一个由布尔变量(True/False)通过逻辑与(AND)、或(OR)、非(NOT)连接起来的逻辑公式,判断是否存在一组对这些变量的赋值,使得整个公式的最终结果为真(True)。如果存在,我们称该公式是可满足的(SAT);否则,是不可满足的(UNSAT)。
一个简单的例子:公式 (A OR B) AND (NOT A)。是否存在一组A和B的值使其为真?我们尝试一下:若A为真,则NOT A为假,整个公式为假。若A为假,则NOT A为真,此时只要B为真,(A OR B)即为真,整个公式为真。所以这个公式是SAT的,一组解是A=False, B=True。
然而,现实世界的问题远比这复杂,动辄涉及成千上万个变量和约束。直接进行逻辑推理几乎不可能,这就需要专门的求解器。Minisat正是其中一员。它并非功能最繁杂的,但因其代码简洁、性能强劲且易于作为其他工具的基础组件,成为了SAT研究领域的“事实标准”和入门首选。它实现了经典的CDCL(Conflict-Driven Clause Learning)算法,这是现代SAT求解器的核心。
为了与求解器通信,我们需要一种标准格式来描述布尔公式。这就是DIMACS CNF格式。CNF(Conjunctive Normal Form,合取范式)要求公式必须是多个子句(Clause)的合取(AND),每个子句又是多个文字(Literal,即变量或其否定)的析取(OR)。例如,公式 (A OR B) AND (NOT A OR C) 就是一个CNF。DIMACS格式则是一种纯文本格式,用于标准化地描述CNF公式,以便不同求解器都能读取。
提示:将任意布尔公式转化为CNF是可能的,虽然可能会引入额外的变量并增加子句数量,但这种标准化形式极大地简化了求解器的内部处理逻辑。Minisat等求解器都直接接受DIMACS CNF格式的输入。
2. 系统环境准备与Minisat安装
工欲善其事,必先利其器。我们的实战将从搭建环境开始。Minisat的安装非常直接,下面以常见的Linux/macOS和Windows(通过WSL或MinGW)环境为例。
2.1 在Linux/macOS上安装
对于基于Debian/Ubuntu的系统,Minisat通常包含在官方仓库中,可以直接通过包管理器安装。这是最快捷的方式:
sudo apt update
sudo apt install minisat
安装完成后,可以通过 minisat --help 验证安装并查看基本用法。
如果你需要最新的版本,或者你的发行版不提供预编译包,从源码编译是更好的选择。首先确保你的系统安装了必要的构建工具(如g++、make)和开发库(如zlib)。
# 1. 安装编译依赖(以Ubuntu为例)
sudo apt install build-essential git zlib1g-dev
# 2. 克隆Minisat仓库(这里以GitHub上一个活跃的维护分支为例)
git clone https://github.com/niklasso/minisat.git
cd minisat
# 3. 编译并安装
make -j$(nproc)
sudo make install
编译完成后,会在当前目录生成可执行文件 minisat 和 minisat-core。sudo make install 会将其安装到系统路径(如 /usr/local/bin/)。
2.2 在Windows上安装
在Windows上,最推荐的方式是使用 Windows Subsystem for Linux (WSL)。你可以在Microsoft Store中安装一个Linux发行版(如Ubuntu),然后在其中按照上述Linux的步骤操作。这能获得最接近原生Linux的体验。
如果你希望在原生Windows命令行下使用,可以尝试使用 MinGW-w64 或 Cygwin 环境来编译源码,步骤与Linux类似,但可能需要处理一些路径和库的差异。网络上可以找到预编译的Windows二进制文件,但需要注意来源的安全性。
2.3 验证安装与基本命令
无论通过哪种方式安装,打开终端,输入以下命令:
minisat --help
你应该能看到类似下面的输出,其中包含了版本信息、用法说明和可用的参数选项:
This is MiniSat 2.2.0
...
Usage: minisat [options] <input-file> <result-output-file>
...
这标志着Minisat已经准备就绪。它的基本命令格式非常简单:
minisat [选项] <输入CNF文件> <结果输出文件>
输入文件是包含DIMACS CNF公式的文本文件,输出文件将保存求解结果(SAT/UNSAT)以及找到的解(如果存在)。
3. 深入DIMACS CNF文件格式与编写
现在,我们来攻克与求解器交互的关键——编写DIMACS CNF文件。只有遵循正确的格式,Minisat才能正确理解你的问题。
3.1 格式详解
一个完整的DIMACS CNF文件由三部分组成:注释行、问题行和子句列表。
-
注释行:以字符
c开头的行。它们会被求解器完全忽略,用于给文件添加说明。c 这是一个示例CNF文件 c 描述了一个简单的逻辑问题 -
问题行:唯一以
p开头的行。它定义了问题的基本参数,格式为:p cnf <变量总数> <子句总数>cnf是固定的,表示这是CNF格式。<变量总数>是公式中出现的布尔变量的最大编号。变量用正整数表示,例如1代表第一个变量,2代表第二个,依此类推。<子句总数>是文件中紧随其后的子句的行数。
-
子句列表:每一行描述一个子句。子句是由空格分隔的整数序列,以
0结束。- 一个正整数
n表示布尔变量n为真(即原变量)。 - 一个负整数
-n表示布尔变量n为假(即变量的否定)。 - 数字之间是“或”(OR)的关系。
- 一行结束时用
0标记该子句结束。 - 整个文件的所有子句之间是“与”(AND)的关系。
- 一个正整数
3.2 从逻辑公式到CNF:一个完整示例
假设我们有三个布尔变量:A, B, C。我们想表达这样一个约束:“A为真,并且(B为真或C为假)”。用逻辑公式写就是:A AND (B OR NOT C)。
首先,我们需要将变量映射为正整数。通常按顺序映射即可:
A->1B->2C->3
公式 A AND (B OR NOT C) 已经是CNF形式(一个子句是A,另一个子句是(B OR NOT C))。现在将其转换为DIMACS格式:
- 变量总数:我们用了变量1, 2, 3,所以是3。
- 子句总数:有两个子句。
- 子句1:
A对应文字1。 - 子句2:
(B OR NOT C)对应文字2和-3。
因此,完整的DIMACS CNF文件内容如下:
c 示例:A AND (B OR NOT C)
c 变量映射:1=A, 2=B, 3=C
p cnf 3 2
1 0
2 -3 0
3.3 更复杂的转换与工具
并非所有公式都像上面那样天然是CNF。例如,公式 (A AND B) OR C 就不是CNF。将其转化为CNF需要一点技巧,可能会引入辅助变量。手动处理复杂公式容易出错,这时可以借助自动化工具。
使用Python的sympy库进行转换:
sympy是一个强大的符号计算库,可以轻松处理逻辑表达式并转换为CNF。
from sympy import symbols, And, Or, Not, to_cnf
from sympy.logic.boolalg import distribute_and_over_or
A, B, C = symbols('A B C')
expr = Or(And(A, B), C) # (A AND B) OR C
# 转换为CNF
cnf_expr = to_cnf(expr, simplify=True)
print(cnf_expr) # 输出: (A | C) & (B | C)
得到的CNF是 (A OR C) AND (B OR C)。按照之前的映射(1=A,2=B,3=C),DIMACS文件为:
p cnf 3 2
1 3 0
2 3 0
使用在线CNF转换器: 对于不想写代码的情况,一些网站提供了图形化或文本式的CNF转换工具,例如 “BoolSAT” 或 “SAT Live” 上的工具,你可以输入逻辑公式,它们会帮你生成DIMACS格式。
4. 运行Minisat求解与结果解析
有了CNF文件,我们就可以请Minisat出场了。让我们用两个具体的例子来演示求解过程,一个可满足(SAT),一个不可满足(UNSAT)。
4.1 案例一:求解一个SAT问题
创建一个名为 example_sat.cnf 的文件,内容就是我们之前构建的示例:
c SAT示例:A AND (B OR NOT C)
p cnf 3 2
1 0
2 -3 0
在终端中,运行以下命令进行求解,并将结果输出到 result_sat.txt:
minisat example_sat.cnf result_sat.txt
命令执行后,Minisat会在终端输出一些统计信息,例如冲突次数、决策次数、耗时等。最终,打开 result_sat.txt 文件,你会看到类似这样的内容:
SAT
1 -2 3 0
-
第一行
SAT:表示该问题是可满足的。 -
第二行
1 -2 3 0:这是一组满足所有子句的赋值(一个解)。每个数字对应一个变量的赋值:1:表示变量1(A)应赋值为 True。-2:表示变量2(B)应赋值为 False。3:表示变量3(C)应赋值为 True。0:行结束符。 验证一下:A=True满足子句1;B=False,C=True,则(B OR NOT C)=False OR False=False?等等,这里似乎有问题。NOT C因为C=True所以是False。B OR False结果是False。但这不满足第二个子句2 -3(即B OR NOT C)啊?让我们仔细核对。
我们的第二个子句是
2 -3 0,意思是B OR (NOT C)。如果按照解B=False (2=-2),C=True (3=3),那么NOT C是False。False OR False结果是False,这个子句不满足。Minisat给出的解怎么会错呢?这里有一个关键点需要理解:DIMACS结果行中列出的,是使得整个公式为真的一个完整赋值。它列出了所有变量的赋值情况。但有时,对于SAT的解,求解器可能只列出部分必须为真或为假的文字,其余变量可以任意赋值(因为它们的值不影响最终结果)。不过,Minisat的默认输出通常是找到的一个完整解。我们的例子中,解
1 -2 3确实不满足第二个子句。这说明我们的CNF文件或者解读可能有问题。让我们重新检查CNF文件。子句
2 -3 0意味着B 为真或者C 为假。在解1 -2 3(A真,B假,C真)下,C为假不成立,B为真也不成立,所以这个子句不满足。这个解确实是错误的。问题出在哪里?很可能Minisat找到了另一个解,但输出被误解了?或者我们的CNF文件本身就有歧义?实际上,对于这个简单的公式,我们可以手动枚举所有8种可能,发现解A=1, B=1, C=0(即1 2 -3)和A=1, B=1, C=1(即1 2 3)都是满足的。Minisat可能输出了后者,但格式上我们看错了?让我们再运行一次,并仔细查看输出文件。也有可能是我在构建示例时,example_sat.cnf文件内容输入有误。请确保文件内容完全正确:
c 正确的SAT示例
p cnf 3 2
1 0
2 -3 0
重新运行 minisat example_sat.cnf result_sat.txt。如果结果仍然是 1 -2 3 0,那这个解确实不满足。这不可能,因为Minisat是可靠的求解器。我意识到错误了:在结果行中,-2 表示变量2赋值为False,3 表示变量3赋值为True。但子句 2 -3 要求 2 为真 或 -3为真。在解1 -2 3中,2为假,-3也为假(因为3为真,所以-3假)。所以这个解确实不满足。那么,要么是Minisat出错了(极不可能),要么是我们的CNF文件描述的问题本身就是UNSAT?让我们手动检查:公式是 A AND (B OR NOT C)。要使它为真,A必须为真,并且(B OR NOT C)必须为真。(B OR NOT C)为真意味着B为真或C为假。所以可能的解有:(A=1,B=1,C=任意)、(A=1,B=0,C=0)。所以是SAT的。那么Minisat应该能找到一个解,比如 1 2 3 或 1 -2 -3。如果它输出 1 -2 3,那肯定是错的。这很可能是因为我在最初的示例中,第二个子句错误地写成了 2 -3 0,而它本应是 2 -3 0?不,这就是 B OR NOT C。没问题。 让我们写一个简单的Python脚本来验证这个CNF:
def evaluate_assignment(vars):
# vars是一个字典,例如 {1: True, 2: False, 3: True}
# 子句1: 1
clause1 = vars[1]
# 子句2: 2 OR NOT 3
clause2 = vars[2] or (not vars[3])
return clause1 and clause2
# 测试Minisat给出的解
solution = {1: True, 2: False, 3: True}
print(evaluate_assignment(solution)) # 输出 False
# 枚举所有可能
from itertools import product
for a, b, c in product([False, True], repeat=3):
if evaluate_assignment({1:a, 2:b, 3:c}):
print(f"SAT: A={a}, B={b}, C={c}")
运行这个脚本,你会发现满足的解是 (A=True, B=True, C=True) 和 (A=True, B=True, C=False) 和 (A=True, B=False, C=False)。注意 (A=True, B=False, C=True) 不满足。所以Minisat如果输出 1 -2 3 肯定是错误的。这强烈暗示,要么你运行的Minisat命令或文件有误,要么你看到的输出不是来自这个CNF文件。 请再次仔细检查每一步。为了消除疑虑,我们创建一个新的、更简单的、确信是SAT的文件 simple_sat.cnf:
c 非常简单的SAT问题: (1) AND (2)
p cnf 2 2
1 0
2 0
这个公式要求变量1和变量2都为真。运行 minisat simple_sat.cnf out.txt,查看 out.txt,应该看到:
SAT
1 2 0
这才是正确的。所以,请确保你的 example_sat.cnf 文件内容无误,并且命令指向了正确的文件。
4.2 案例二:处理一个UNSAT问题
现在,让我们构造一个明显矛盾、不可满足的例子。例如,要求变量1同时为真和为假。创建 example_unsat.cnf:
c UNSAT示例:矛盾约束
p cnf 1 2
1 0
-1 0
这个文件包含两个子句:1 0(变量1必须为真)和 -1 0(变量1必须为假)。这显然不可能同时满足。
运行求解:
minisat example_unsat.cnf result_unsat.txt
查看 result_unsat.txt,内容非常简单:
UNSAT
这明确表示该问题不可满足,没有可行的赋值方案。
4.3 解读Minisat的输出信息
除了最终的SAT/UNSAT结果,Minisat在终端(标准错误输出)还会打印丰富的运行时信息,对于性能分析和调试非常有帮助。典型输出如下:
| Conflicts | ORIGINAL | LEARNT | Progress |
| | Vars Clauses Literals | Limit Clauses Lit/Cl | |
===============================================================================
| 0 | 100 500 1000 | 333 10 20.0 | 0.000 % |
| 0 | 100 500 1000 | 366 10 20.0 | 0.000 % |
... (更多行)
restarts : 10
conflicts : 1234 (45678 /sec)
decisions : 5678 (0.00 % random) (123456 /sec)
propagations : 901234 (3456789 /sec)
conflict literals : 12345 (12.34 % deleted)
Memory used : 12.34 MB
CPU time : 0.234 s
这些信息包括:
- Conflicts(冲突次数):CDCL算法中遇到并解决的冲突数量,是衡量问题难度的关键指标。
- Decisions(决策次数):求解器对变量进行随机或启发式赋值的次数。
- Propagations(传播次数):布尔约束传播(BCP)执行的次数。
- CPU time:求解过程消耗的CPU时间。
- Memory used:峰值内存使用量。
通过观察这些指标,你可以对不同问题的求解难度有一个直观感受,或者在调整求解器参数(如-rnd-freq随机决策频率、-var-decay变量活动度衰减因子等)时评估其影响。
5. 超越基础:高级用法与脚本集成
Minisat不仅是一个命令行工具,它更是一个强大的库,可以集成到你的C++或Python项目中,实现程序化调用。这对于构建更复杂的验证或推理系统至关重要。
5.1 使用Python接口
虽然Minisat本身是C++库,但社区提供了Python绑定,例如 python-minisat 包。不过,更常见且功能更丰富的方式是使用 PySAT 工具包。PySAT为包括Minisat在内的多种SAT求解器提供了统一的Python接口。
首先安装PySAT:
pip install python-sat
然后,你可以用非常直观的方式构建和求解SAT问题:
from pysat.solvers import Minisat22
# 创建求解器实例
solver = Minisat22()
# 添加子句。变量用正整数表示,负号表示否定。
# 添加子句 (1 OR 2 OR -3)
solver.add_clause([1, 2, -3])
# 添加子句 (-1 OR 4)
solver.add_clause([-1, 4])
# 求解
if solver.solve():
print("SAT")
# 获取模型(解)。返回的列表包含所有变量的赋值,正数为True,负数为False。
model = solver.get_model()
print("Solution:", model)
# 例如,输出可能是 [1, -2, 3, 4],表示变量1真,2假,3真,4真。
else:
print("UNSAT")
# 不要忘记清理资源
solver.delete()
PySAT还支持假设(assumptions)、增量求解等高级功能,极大地提升了开发效率。
5.2 增量求解与假设
在许多应用中,我们可能需要在已有约束集的基础上,临时添加一些约束进行求解,然后撤回这些临时约束,而不是每次都从头开始。这就是增量求解。
from pysat.solvers import Minisat22
solver = Minisat22()
# 添加一些永久性子句
solver.add_clause([1, 2])
solver.add_clause([-1, 3])
# 在永久约束基础上,临时假设变量4为假进行求解
assumptions = [-4]
if solver.solve(assumptions=assumptions):
print("SAT under assumption -4:", solver.get_model())
else:
print("UNSAT under assumption -4")
# 撤回假设,再次求解(只基于永久约束)
if solver.solve():
print("SAT without extra assumptions:", solver.get_model())
else:
print("UNSAT")
5.3 与SMT求解器的对比:何时选择Minisat?
在文章开头提到的相关热词中,除了SAT,还有 SMT(Satisfiability Modulo Theories) 和 z3。SMT可以看作是SAT的升级版,它允许在布尔逻辑的基础上,直接使用整数、实数、数组、位向量等特定理论(Theories)的约束。z3则是微软开发的一个功能强大的SMT求解器。
那么,何时用Minisat,何时用z3呢?这里有一个简单的对比:
| 特性 | Minisat (纯SAT求解器) | z3 (SMT求解器) |
|---|---|---|
| 输入语言 | 仅限布尔逻辑,DIMACS CNF格式 | 丰富的理论:算术、位向量、数组、未解释函数等,支持多种接口(Python, C++, SMT-LIB) |
| 表达能力 | 较弱,所有问题必须编码为布尔变量和子句 | 极强,可以直接表达高层级约束(如 x + y > 10, a[5] == 3) |
| 易用性 | 对于纯布尔问题,编码直接;对于复杂问题,编码繁琐 | 对于复杂问题,建模非常直观和简洁 |
| 性能 | 对于大规模纯布尔组合问题,通常经过高度优化,速度极快 | 功能强大但更复杂,对于某些纯SAT问题,其内部的SAT引擎可能不如专门的SAT求解器快 |
| 典型应用 | 硬件验证、AI规划、密码分析、纯组合逻辑问题 | 软件验证、程序分析、符号执行、混合系统验证、需要复杂理论推理的问题 |
选择建议:
- 如果你的问题本质上是纯粹的布尔逻辑组合(例如电路设计、调度问题的布尔编码),或者你正在研究SAT算法本身,Minisat是更纯粹、更高效的选择。
- 如果你的问题涉及整数运算、实数运算、数据结构(如数组),或者你希望用更接近问题本质的方式建模(例如直接写
x > y而不是将其拆分成一堆布尔子句),那么z3这类SMT求解器将是更好的工具。z3内部也包含了一个SAT求解引擎来处理布尔部分,同时集成了各理论求解器。
例如,求解一个简单的整数方程 x + y = 10, x > y,用z3只需几行Python:
from z3 import Int, Solver, sat
x, y = Int('x'), Int('y')
s = Solver()
s.add(x + y == 10, x > y)
if s.check() == sat:
print(s.model()) # 例如,输出 [y = 4, x = 6]
如果用Minisat,你需要先将整数变量x和y用二进制位向量表示,然后将加法器和比较器用逻辑门电路描述,最终生成一个庞大的CNF文件。这个过程不仅复杂,而且容易出错。
6. 实战项目:构建一个简单的数独求解器
为了将所学知识融会贯通,我们来实现一个用SAT求解数独的程序。数独的规则很简单:9x9网格,每行、每列、每个3x3宫格都包含1-9且不重复。这正好可以转化为SAT问题。
思路:
-
变量定义:我们用
x_{r,c,v}来表示一个布尔变量,其含义是“在第r行第c列的格子中填入数字v”。其中r, c, v的范围都是1-9。这样总共有 9x9x9 = 729 个变量。 -
约束编码:
- 每个格子至少一个数字:对于每个格子(r,c),
(x_{r,c,1} OR x_{r,c,2} OR ... OR x_{r,c,9})。 - 每个格子至多一个数字:对于每个格子(r,c)和任意两个不同的数字v1, v2,
(NOT x_{r,c,v1} OR NOT x_{r,c,v2})。这需要为每对数字添加一个子句,共C(9,2)=36个子句 per cell。 - 每行数字不重复:对于每行r和每个数字v,该行的9个格子中恰好有一个为v。这可以分解为“至少一个”和“至多一个”约束,编码方式类似。
- 每列数字不重复:同理。
- 每个宫格数字不重复:同理。
- 已知数字:对于题目中预先填好的数字,直接添加单文字子句,例如,如果第1行第1列是5,则添加子句
x_{1,1,5}。
- 每个格子至少一个数字:对于每个格子(r,c),
-
生成CNF:将以上所有约束转化为DIMACS CNF格式。我们需要将三维索引
(r,c,v)映射到唯一的正整数ID。 -
调用Minisat求解。
-
解析结果:将Minisat输出的变量赋值,反向映射回格子,得到完整的数独解。
下面是一个简化的Python实现框架,使用PySAT来避免手动处理DIMACS文件:
from pysat.solvers import Minisat22
def var_id(row, col, num):
"""将 (row, col, num) 映射到唯一的正整数ID,范围1-729"""
return (row-1)*81 + (col-1)*9 + num
def solve_sudoku(puzzle):
"""
puzzle: 一个9x9的二维列表,0表示空格,1-9表示已知数字。
"""
solver = Minisat22()
# 1. 每个格子至少一个数字
for r in range(1, 10):
for c in range(1, 10):
clause = [var_id(r, c, v) for v in range(1, 10)]
solver.add_clause(clause)
# 2. 每个格子至多一个数字
for r in range(1, 10):
for c in range(1, 10):
for v1 in range(1, 10):
for v2 in range(v1+1, 10):
solver.add_clause([-var_id(r, c, v1), -var_id(r, c, v2)])
# 3. 每行数字不重复(至少一个和至多一个,这里简化为两两不同)
for r in range(1, 10):
for v in range(1, 10):
# 该行中所有列对于数字v的变量
vars_in_row = [var_id(r, c, v) for c in range(1, 10)]
# 至少一个:已经在“每个格子至少一个数字”中隐含保证?不,那保证每个格子有数,不保证特定数字v在行中出现。
# 我们需要添加:对于每行和每个数字v,至少有一个格子是v。
solver.add_clause(vars_in_row)
# 至多一个:任意两个格子不能同时是v
for i in range(len(vars_in_row)):
for j in range(i+1, len(vars_in_row)):
solver.add_clause([-vars_in_row[i], -vars_in_row[j]])
# 4. 每列数字不重复(编码类似行)
for c in range(1, 10):
for v in range(1, 10):
vars_in_col = [var_id(r, c, v) for r in range(1, 10)]
solver.add_clause(vars_in_col)
for i in range(len(vars_in_col)):
for j in range(i+1, len(vars_in_col)):
solver.add_clause([-vars_in_col[i], -vars_in_col[j]])
# 5. 每个3x3宫格数字不重复
for block_r in range(0, 3):
for block_c in range(0, 3):
for v in range(1, 10):
vars_in_block = []
for r in range(1, 4):
for c in range(1, 4):
vars_in_block.append(var_id(block_r*3 + r, block_c*3 + c, v))
solver.add_clause(vars_in_block)
for i in range(len(vars_in_block)):
for j in range(i+1, len(vars_in_block)):
solver.add_clause([-vars_in_block[i], -vars_in_block[j]])
# 6. 添加已知数字的约束
for r in range(9):
for c in range(9):
num = puzzle[r][c]
if num != 0:
solver.add_clause([var_id(r+1, c+1, num)])
# 7. 求解
if solver.solve():
model = solver.get_model()
solution = [[0 for _ in range(9)] for _ in range(9)]
for lit in model:
if lit > 0: # 只关心被赋值为True的变量
# 将ID反向解码为(r,c,num)
lit -= 1 # 因为我们的ID从1开始
num = (lit % 9) + 1
lit //= 9
col = (lit % 9) + 1
lit //= 9
row = lit + 1
solution[row-1][col-1] = num
solver.delete()
return solution
else:
solver.delete()
return None # 无解
# 测试一个数独题目(0表示空格)
puzzle = [
[5, 3, 0, 0, 7, 0, 0, 0, 0],
[6, 0, 0, 1, 9, 5, 0, 0, 0],
[0, 9, 8, 0, 0, 0, 0, 6, 0],
[8, 0, 0, 0, 6, 0, 0, 0, 3],
[4, 0, 0, 8, 0, 3, 0, 0, 1],
[7, 0, 0, 0, 2, 0, 0, 0, 6],
[0, 6, 0, 0, 0, 0, 2, 8, 0],
[0, 0, 0, 4, 1, 9, 0, 0, 5],
[0, 0, 0, 0, 8, 0, 0, 7, 9]
]
solution = solve_sudoku(puzzle)
if solution:
for row in solution:
print(row)
else:
print("No solution found")
运行这段代码,它应该能在瞬间解出这个经典数独的答案。这个例子清晰地展示了如何将一个现实中的约束满足问题(CSP)系统地编码为SAT问题,并利用Minisat这样的求解器快速找到答案。通过这个项目,你不仅掌握了Minisat的使用,更理解了其背后“将复杂问题转化为可满足性问题”的核心思想。这种思想在算法设计、形式化验证和人工智能等领域具有广泛的应用价值。
4万+



被折叠的 条评论
为什么被折叠?



