By C h e s i u m \text{By}\ \mathsf{Chesium} By Chesium
DPLL 算法,全称为 Davis-Putnam-Logemann-Loveland(戴维斯-普特南-洛吉曼-洛夫兰德)算法,是一种完备的,基于回溯(backtracking)的搜索算法,用于判定命题逻辑公式(为合取范式形式)的可满足性,也就是求解 SAT(布尔可满足性问题)的一种(或者一类)算法。
SAT 问题简介
何为布尔可满足性问题?给定一条真值表达式,包含逻辑变量(又称 变量、命题变号、原子,用小写字母 a , b , … a,b,\dots a,b,… 表示)、逻辑与(AND,记为 “ ∧ \wedge ∧” )运算符、逻辑或(OR,记为 “ ∨ \vee ∨” )运算符以及非(NOT,否定,记为“ ¬ \neg ¬”)运算符,如:
( a ∧ ¬ b ∧ ( ¬ ( c ∨ d ∨ ¬ a ) ∨ ( b ∧ ¬ d ) ) ) ∨ ( ¬ ( ¬ ( ¬ b ∨ a ) ∧ c ) ∧ d ) (a\wedge\neg b\wedge(\neg(c\vee d\vee\neg a)\vee(b\wedge\neg d)))\vee(\neg(\neg(\neg b\vee a)\wedge c)\wedge d) (a∧¬b∧(¬(c∨d∨¬a)∨(b∧¬d)))∨(¬(¬(¬b∨a)∧c)∧d)
是否存在一组对这些变量的赋值(如把所有 a a a 和 d d d 均赋值为 T r u e \mathrm{True} True ,将所有 b b b 和 c c c 赋值为 F a l s e \mathrm{False} False ),使得整条式子最终的运算结果为 T r u e \mathrm{True} True ?若可以,那么这个性质被称为这条逻辑公式的可满足性(satisfiability),如何快速高效地判断任意指定逻辑公式的可满足性是理论计算机科学中的一个重要的问题,也是第一个被证明为NP-完全(NP-complete,NPC)的问题。
暴力方案
对于这个问题,我们能够很容易地想到一种“暴力”的判定方法:测试这些变量赋值的每种可能的排列方式(如全部赋为 T r u e \mathrm{True} True 、其一为 T r u e \mathrm{True} True 其他全为 F a l s e \mathrm{False} False ……),若存在一种赋值排列使得公式的结果为 T r u e \mathrm{True} True ,那么就可以说明这条公式是可满足的。但很显然,最坏情况下这种方法需要我们测试 2 n 2^n 2n 种( n n n 为变量数)赋值排列,而用于检查每种赋值排列最终的运算结果也是不可忽略的。因此,随着公式规模的扩大,这种暴力算法所需的运算量会呈指数级飞快增长,这是我们不可接受的。
算法概述
但是根据现有计算复杂度理论,SAT问题是无法在多项式时间复杂度内解决的,DPLL算法也不例外。
DPLL算法是一种搜索算法,思想与DFS(Depth-first search,深度优先搜索)十分相似,或者说DPLL算法本身就属于DFS的范畴,其类似于上述我们设想的“暴力”算法:搜索所有可能的赋值排列。
具体地说,算法会在公式中选择一个变量(命题变号),将其赋值为 T r u e \mathrm{True} True ,化简赋值后的公式,如果简化的公式是可满足的(递归地判断),那么原公式也是可满足的。否则就反过来将该变量赋值为 F a l s e \mathrm{False} False ,再执行一遍递归的判定,若也不能满足,那么原公式便是不可满足的。
这被称为 分离规则 (splitting rule),因为其将原问题分离为了两个更加简单的问题。
概念说明
DPLL算法求解的是合取范式(Conjunctive normal form,CNF),这是指形如下式的逻辑公式:
( a ∨ b ∨ ¬ c ) ∧ ( ¬ d ∨ x 1 ∨ ¬ x 2 ∨ ⋯ ∨ x 7 ) ∧ ( ¬ r ∨ v ∨ g ) ∧ ⋯ ∧ ( a ∨ d ∨ ¬ d ) (a\vee b\vee\neg c)\wedge (\neg d\vee x_1\vee\neg x_2\vee\dots\vee x_7)\wedge (\neg r\vee v\vee g)\wedge\dots\wedge (a\vee d\vee\neg d) (a∨b∨¬c)∧(¬d∨x1∨¬x2∨⋯∨x7)∧(¬r∨v∨g)∧⋯∧(a∨d∨¬d)
其由多个括号括住部分的逻辑与组成,每一个括号内又是许多变量或变量的否定(逻辑非)的逻辑或组成。可以证明,所有只包含逻辑与、逻辑或、逻辑非、逻辑蕴含和括号的逻辑公式均可化为等价的合取范式。下面,我们称整个范式为“公式”,称每个括号里的部分为该公式的子句(clause),每个子句中的每个变量或其否定为文字(literal)。
可以看出,要使整条公式结果为 T r u e \mathrm{True} True ,其所有子句都必须为 T r u e \mathrm{True} True ,也就是说,每个子句中都至少有一个文字为 T r u e \mathrm{True} True ,这个结论下面会用到。
DPLL 算法中的化简步骤实际上就是移除所有在赋值后值为 T r u e \mathrm{True} True 的子句,以及所有在赋值后值为 F a l s e \mathrm{False} False 的文字。
化简步骤
这两个化简步骤是 DPLL 算法与我们“暴力”算法的主要区别,它们大大减少了搜索量,亦即加快了算法的运行速度。
第一个化简步骤:单位子句传播(Unit propagation)
我们称只含有一个(未赋值)变量的子句为单位子句(unit clause),根据上面的结论,要想让公式为 T r u e \mathrm{True} True ,这个子句必须为 T r u e \mathrm{True} True ,即这个变量对应的文字必须被赋值为 T r u e \mathrm{True} True 。
比如下面的这条公式:
( a ∨ b ∨ c ∨ ¬ d ) ∧ ( ¬ a ∨ c ) ∧ ( ¬ c ∨ d ) ∧ ( a ) (a\vee b\vee c\vee\neg d)\wedge(\neg a\vee c)\wedge(\neg c\vee d)\wedge(a) (a∨b∨c∨¬d)∧(¬a∨c)∧(¬c∨d)∧(a)
其中最后一个子句就为单位子句,亦即我们要使文字 ( a ) (a) (a) 为 T r u e \mathrm{True} True 。
然后,我们要依次处理这个变量在其他子句中的出现,如果另一个子句中的一个文字与单位子句中的文字相同,如上面例子中的 (