CNF 合取范式

CNF 是合取范式的简称,是可满足问题中比较重要的概念。在实际应用中,我们一般将约束写成 CNF 范式的格式,然后通过求解器 Solver 对其进行求解。因此 CNF 可以理解为一种问题约束的表现形式。 本文对 CNF 的基本概念,存储形式,及其应用做一点简要的介绍。

1. CNF 的基本概念

布尔变量 (Boolean variable) 指的是那种只能取 真 (True) 或 假 (False) 的变量。布尔变量是布尔逻辑的基础。布尔变量之间是可以运用 布尔运算符 (Boolean operators) 来进行相互操作的,这些布尔运算符包括 或 ( ∨ \vee ),且 ( ∧ \wedge ),非 ( ¬ \neg ¬)。

布尔表达式 (Boolean expression) 指的是通过布尔运算符将布尔变量连接起来的表达式。如针对布尔变量 a , b a,b a,b 的布尔表达式可以是 a ∨ b , a ∧ b , ¬ a a \vee b, a \wedge b, \neg a ab,ab,¬a。我们通过对这些简单的表达式不断进行或且非操作就可以变成很复杂的表达式,如 ( ¬ a ∨ b ) ∧ ( a ∨ ¬ b ) (\neg a \vee b) \wedge (a \vee \neg b) (¬ab)(a¬b)。但无论布尔变量如何取值,布尔表达式永远只有真 (True) 和假 (False) 两种结果。

可满足问题 (Satisifiability problem, SAT) 指的是给布尔表达式中的布尔变量赋值,使得由这些布尔变量组合起来的布尔表达式结果为真。这些布尔变量的赋值组合成为 可满足问题的解 (Solution)。可满足问题的一个形象的比喻是:布尔变量相当于电灯的一系列开关,每个开关可以选择 “开” (True) 或 “不开” (False) 。这些开关的组合再通过黑盒子 (不二表达式) 来控制电灯 “亮” (True) 或 “不亮” (False)。可满足问题要解决的就是调整这这些开关,使得最终电灯亮起来。
在这里插入图片描述

注意,并不是所有的可满足问题都是有解的。对于某些布尔表达式,我们无论取什么值都无法使这些表达式为真,这样的式子我们称之为 不可满足式永假式矛盾式 (Contradictory formula)

析取 (Disjunctive) 本身是命题逻辑中的概念1,简单来讲其含义大致可看做是或操作 ( ∨ \vee )。那些仅由或运算符连接而成的布尔表达式称之为 析取子句 (Disjunctive clause)。如 ( a ∨ b ∨ ¬ c ) (a \vee b \vee \neg c) (ab¬c)。与之相反,合取 (Conjunctive) 指代的其实就是与操作 ( ∧ \wedge )。那些仅有与运算符连接而成的布尔表达式称之为 合取子句 (Conjunctive clause)。如 ( a ∧ b ) (a \wedge b) (ab) ( a ∧ b ∧ ¬ c ) (a \wedge b \wedge \neg c) (ab¬c)

合取范式 (Conjunctive Normal Form)2 是命题公式的一个标准型,它由一系列 析取子句 用 合取操作 连接而来。如 ( a ) ∧ ( a ∨ ¬ c ) ∧ ( b ∨ c ) (a) \wedge (a \vee \neg c) \wedge (b \vee c) (a)(a¬c)(bc)。与之相反,析取范式 (Disjunctive Normal Form) 是命题公式的另一个标准型,它由一系列 合取子句 用 析取操作 连接而来。如 ( a ) ∨ ( a ∧ ¬ c ) ∨ ( b ∧ c ) (a) \vee (a \wedge \neg c) \vee (b \wedge c) (a)(a¬c)(bc)

2. CNF 的存储格式

本文主要讨论合取范式 (CNF)。为了方便起见,合取范式文件存储在 .dimacs 为后缀名的文件中3。书写合取范式文件需要满足如下规则:

a ) 文件主要由 注释块子句块 两部分组成,其中注释块一般用作变量的声明,每个变量占一行;子句块主要书写问题的条件 (即 cnf 范式),每个子句占一行。文件用标记行 p cnf {num_var} {num_clauses} 来区分 注释块 和 子句块 两部分,即该行之前是注释,该行之后是子句。这里的 {num_var} 表示变量的个数,{num_clauses} 表示子句的个数。

// 表示有 10 个变量,20 个子句。
p cnf 10 20

b ) 在注释快中,每行声明一个变量,且以小写字母 c 开头。注释的形式为 c {var_id} {var_name},其中 var_id 表示变量的序号,如 1,2,…,注意此序号必须从 1 开始且连续。var_name 表示变量的名称。这样做是为了后续子句书写的便利,即用序号表示变量。

// 声明变量 a,其序号为 1;声明变量 b,其序号为 2
c 1 a
c 2 b

c ) 在子句块中,每行表示一个析取子句 (仅包含或操作),且以数字 0 结尾。如 1 -2 0 表示 ( a ∨ ¬ b ) (a \vee \neg b) (a¬b) 这个析取子句。2 -1 0 表示 ( b ∨ ¬ a ) (b \vee \neg a) (b¬a) 这个析取子句。数字前面的符号表示非 ( ¬ \neg ¬) 的含义。子句块中所包含的所有行用与操作连接起来就是我们的 cnf 了。因此在 SAT 问题中,如果要使得最终的合取范式 (CNF) 结果为真,则必须要求每一行的析取子句结果为真。

// 子句含义为 (a OR !b)
1 -2 0
// 子句含义为 (b OR !a)
2 -1 0

综上所述,我们可以根据 a,b 构建一个简单的 cnf,并存储在 .dimacs 文件中,文件内容为,

c 1 a
c 2 b
p cnf 2 2
1 -2 0
2 -1 0

3. CNF 的应用

在可满足问题中,合取范式 CNF 主要是用来表达问题的约束,由于CNF 是由一系列子句用 与运算符 ( ∧ \wedge ) 连接而成,因此必须让每个子句都为真,最终的结果才能为真。先给出一个 CNF 在特征模型中的运用。

特征模型 (Feature model) 是软件产品线工程 (Software Product Line Engineering) 中的重要概念,它规定了软件产品在运行或发布时各个组件 (配置项) 之间应当满足的关系。通常该特征模型是以树状的结果来展现,因此 树的层次结构节点之间的蕴含关系 共同组成了特征模型的约束,我们使用 cnf 来表示这些约束。下图展现了一个软件的特征模型,
在这里插入图片描述
在这个特征模型中,每个结点代表一个特征,我们暂时不考虑特征间的蕴涵约束,仅仅考虑层次关系所产生的约束。如 BTWTreesINComponer 的父节点,当 INComponer 取 0 时 BTWTrees 不能取 1,则二者约束可描述为 BTWTrees → \rightarrow INComponer,转化成 cnf 格式为 BTWTrees ∨ ¬ \vee \neg ¬INComponer。我总结了特征模型中的 5 种基本约束 ,每种约束与只对应的逻辑命题公式如下所示,
pic-2
根据上图的命题公式,我们可以将上述 5 种结构写为对应的合取范式的形式,如下表所示,

结构名称命题公式合取范式 CNF
Optional node (B 是可选特征) B → A B \rightarrow A BA ( ¬ B ∨ A ) (\neg B \vee A) (¬BA)
Mandatory node (B 是必选特征) B → A ,   A → B B \rightarrow A, \ A \rightarrow B BA, AB ( ¬ B ∨ A ) ∧ ( ¬ A ∨ B ) (\neg B \vee A) \wedge (\neg A \vee B) (¬BA)(¬AB)
OR group (B,C,D 至少选择一个) A ↔ ( B ∨ C ∨ D ) A \leftrightarrow (B \vee C \vee D) A(BCD) ( ¬ A ∨ B ∨ C ∨ D ) ∧ ( ¬ B ∨ A ) ∧ ( ¬ C ∨ A ) (\neg A \vee B \vee C \vee D) \wedge (\neg B \vee A) \wedge (\neg C \vee A) (¬ABCD)(¬BA)(¬CA)
∧ ( ¬ D ∨ A ) \wedge (\neg D \vee A) (¬DA)
AND group (选 A 必选 B) A ↔ ( B ∨ C ∨ D ) A \leftrightarrow (B \vee C \vee D) A(BCD)
A → B A \rightarrow B AB
( ¬ A ∨ B ∨ C ∨ D ) ∧ ( ¬ B ∨ A ) ∧ ( ¬ C ∨ A ) (\neg A \vee B \vee C \vee D) \wedge (\neg B \vee A) \wedge (\neg C \vee A) (¬ABCD)(¬BA)(¬CA)
∧ ( ¬ D ∨ A ) ∧ ( ¬ A ∨ B ) \wedge (\neg D \vee A) \wedge (\neg A \vee B) (¬DA)(¬AB)
Alternative group (B,C,D 只能选择一个) A ↔ ( B ∨ C ∨ D ) A \leftrightarrow (B \vee C \vee D) A(BCD)
B → ( ¬ C ∧ ¬ D ) B \rightarrow (\neg C \wedge \neg D) B(¬C¬D)
C → ( ¬ B ∧ ¬ D ) C \rightarrow (\neg B \wedge \neg D) C(¬B¬D)
D → ( ¬ B ∧ ¬ C ) D \rightarrow (\neg B \wedge \neg C) D(¬B¬C)
( ¬ A ∨ B ∨ C ∨ D ) ∧ ( ¬ B ∨ A ) ∧ ( ¬ C ∨ A ) (\neg A \vee B \vee C \vee D) \wedge (\neg B \vee A) \wedge (\neg C \vee A) (¬ABCD)(¬BA)(¬CA)
∧ ( ¬ D ∨ A ) ∧ ( ¬ B ∨ ¬ C ∨ ¬ D ) \wedge (\neg D \vee A) \wedge (\neg B \vee \neg C \vee \neg D) (¬DA)(¬B¬C¬D)

上述的特征模型使用 FeatureIDE4 插件画出来的,由德国乌尔姆大学的团队开发并维护。该插件不但能编辑特征模型,还能根据约束来求解出合法的特征组合,是非常好用的一款工具。

4. Python 的简单实现

在 Python 编程语言中,官方提供了一个非常通用的可满足问题的求解库,即 pysat5
在这里插入图片描述
样例 1: 下面的代码展示了求解可满足问题的一般性方法。 其中可满足问题以子句集 clauses 的形式描述,即 clauses 集合中的每个子句都必须为真。首先定义求解器对象 s1 ,然后使用 add_clause() 函数往求解器对象中加入约束。 最终求解器对象调用 enum_modes() 来返回所有可能解。

from pysat.solvers import Solver

def solve_sat_from_clauses(clauses):
	'''
	use g3 solver to solve the clauses list
	>>>> Parameters:
	:clauses: clauses that should be satisified
	>>>> Return:
	:solutions: all possible solutions to the sat problem
	'''
	solutions = []
	with Solver(name='g3') as s1: # define a g3 solver
		for clause in clauses:
			s1.add_clause(clause) # add clauses
		if s1.solve():
			for solution in s1.enum_models(): # enumerate all possible solutions
				solutions.append(solution)
				
	return solutions

子句集 clauses 中的每个子句 clause 均是析取子句,即元素用或运算符相连接。假设变量 a a a 对应 1,变量 b b b 对应 2,则 ( a ∨ ¬ b ) (a \vee \neg b) (a¬b) 写为 [1,-2] ¬ a \neg a ¬a 写为 [-1]。求解器返回的解 solution 也是用正负号表示取值的真假,假设最终解的结果是 [-1,2],则表示 a a a=False, b b b=True。

样例 2: 下面的代码展示了从 ,dimacs 文件中读入 CNF 格式约束并求解的过程。与样例 1 不同的是,在函数中需要引入 pysat.formula.CNF,并实例化 CNF 对象来提取文件中的约束。

from pysat.solvers import Solver
from pysat.formula import CNF

def solve_sat_from_cnf(cnf_file):
	'''
	use g3 solver to solve the sat problem from cnf file
	>>>> Parameters:
	:cnf_file: cnf file that contains clauses
	>>>> Return:
	:solutions: all possible solutions to the sat problem
	'''
	solutions = []
	constraints= CNF(from_file=cnf_file) # collect clauses from cnf file
	with Solver(name='g3') as s1: # define a g3 solver
		s1.append_formula(constraints) # add clauses
		if s1.solve():
			for solution in s1.enum_models(): # enumerate all possible solutions
				solutions.append(solution)
				
	return solutions		

样例 3: 下面的代码展示了求解可满足问题时,添加额外假设的方法。比如我们在求解 a ∨ ¬ b a \vee \neg b a¬b 约束时添加了假设 a a a=False 的条件,我们可以在 solve() 函数中添加属性 assumptions=[-1]

from pysat.solver import Solver

if __name__ == "__main__":
	with Solver(name='g3') as s1:
		s1.add_clause([1,-2])
		if s1.solve(assumptions=[-1]): # add assumptions
			for solution in s1.enum_models():
				print(solutions)

  1. 倪先生要做数学家. “命题逻辑的学习笔记 (知乎)”. Link ↩︎

  2. 梦里一声何处鸿. “析取范式与合取范式 (CSDN 博客)”. Link ↩︎

  3. John Burkardt. “CNF 格式简介 (佛罗里达州立大学)”. Link. ↩︎

  4. Thomas Thüm. “FeatureIDE 简介 (乌尔姆大学)”. Link ↩︎

  5. pysathq. “A toolkit for SAT-based prototyping in Python (Github)”. Link ↩︎

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值