Python微信订餐小程序课程视频
https://edu.csdn.net/course/detail/36074
Python实战量化交易理财系统
https://edu.csdn.net/course/detail/35475
By ChesiumBy Chesium\text{By}\ \mathsf{Chesium}
DPLL 算法,全称为 Davis-Putnam-Logemann-Loveland(戴维斯-普特南-洛吉曼-洛夫兰德)算法,是一种完备的,基于回溯(backtracking)的搜索算法,用于判定命题逻辑公式(为合取范式形式)的可满足性,也就是求解 SAT(布尔可满足性问题)的一种(或者一类)算法。
SAT 问题简介
何为布尔可满足性问题?给定一条真值表达式,包含逻辑变量(又称 变量、命题变号、原子,用小写字母 a,b,…a,b,…a,b,\dots 表示)、逻辑与(AND,记为 “∧∧\wedge” )运算符、逻辑或(OR,记为 “∨∨\vee” )运算符以及非(NOT,否定,记为“¬¬\neg”)运算符,如:
(a∧¬b∧(¬(c∨d∨¬a)∨(b∧¬d)))∨(¬(¬(¬b∨a)∧c)∧d)(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)
是否存在一组对这些变量的赋值(如把所有 aaa 和 ddd 均赋值为 TrueTrue\mathrm{True} ,将所有 bbb 和 ccc 赋值为 FalseFalse\mathrm{False} ),使得整条式子最终的运算结果为 TrueTrue\mathrm{True} ?若可以,那么这个性质被称为这条逻辑公式的可满足性(satisfiability),如何快速高效地判断任意指定逻辑公式的可满足性是理论计算机科学中的一个重要的问题,也是第一个被证明为NP-完全(NP-complete,NPC)的问题。
暴力方案
对于这个问题,我们能够很容易地想到一种“暴力”的判定方法:测试这些变量赋值的每种可能的排列方式(如全部赋为 TrueTrue\mathrm{True} 、其一为 TrueTrue\mathrm{True} 其他全为 FalseFalse\mathrm{False} ……),若存在一种赋值排列使得公式的结果为 TrueTrue\mathrm{True} ,那么就可以说明这条公式是可满足的。但很显然,最坏情况下这种方法需要我们测试 2n2n2^n 种(nnn 为变量数)赋值排列,而用于检查每种赋值排列最终的运算结果也是不可忽略的。因此,随着公式规模的扩大,这种暴力算法所需的运算量会呈指数级飞快增长,这是我们不可接受的。
算法概述
但是根据现有计算复杂度理论,SAT问题是无法在多项式时间复杂度内解决的,DPLL算法也不例外。
DPLL算法是一种搜索算法,思想与DFS(Depth-first search,深度优先搜索)十分相似,或者说DPLL算法本身就属于DFS的范畴,其类似于上述我们设想的“暴力”算法:搜索所有可能的赋值排列。
具体地说,算法会在公式中选择一个变量(命题变号),将其赋值为 TrueTrue\mathrm{True} ,化简赋值后的公式,如果简化的公式是可满足的(递归地判断),那么原公式也是可满足的。否则就反过来将该变量赋值为 FalseFalse\mathrm{False} ,再执行一遍递归的判定,若也不能满足,那么原公式便是不可满足的。
这被称为 分离规则 (splitting rule),因为其将原问题分离为了两个更加简单的问题。
概念说明
DPLL算法求解的是合取范式(Conjunctive normal form,CNF),这是指形如下式的逻辑公式:
(a∨b∨¬c)∧(¬d∨x1∨¬x2∨⋯∨x7)∧(¬r∨v∨g)∧⋯∧(a∨d∨¬d)(a∨b∨¬c)∧(¬d∨x1∨¬x2∨⋯∨x7)∧(¬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)
其由多个括号括住部分的逻辑与组成,每一个括号内又是许多变量或变量的否定(逻辑非)的逻辑或组成。可以证明,所有只包含逻辑与、逻辑或、逻辑非、逻辑蕴含和括号的逻辑公式均可化为等价的合取范式。下面,我们称整个范式为“公式”,称每个括号里的部分为该公式的子句(clause),每个子句中的每个变量或其否定为文字(literal)。
可以看出,要使整条公式结果为 TrueTrue\mathrm{True} ,其所有子句都必须为 TrueTrue\mathrm{True} ,也就是说,每个子句中都至少有一个文字为 TrueTrue\mathrm{True} ,这个结论下面会用到。
DPLL 算法中的化简步骤实际上就是移除所有在赋值后值为 TrueTrue\mathrm{True} 的子句,以及所有在赋值后值为 FalseFalse\mathrm{False} 的文字。
化简步骤
这两个化简步骤是 DPLL 算法与我们“暴力”算法的主要区别,它们大大减少了搜索量,亦即加快了算法的运行速度。
第一个化简步骤:单位子句传播(Unit propagation)
我们称只含有一个(未赋值)变量的子句为单位子句(unit clause),根据上面的结论,要想让公式为 TrueTrue\mathrm{True} ,这个子句必须为 TrueTrue\mathrm{True} ,即这个变量对应的文字必须被赋值为 TrueTrue\mathrm{True} 。
比如下面的这条公式:
(a∨b∨c∨¬d)∧(¬a∨c)∧(¬c∨d)∧(a)(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)(a)(a) 为 TrueTrue\mathrm{True} 。
然后,我们要依次处理这个变量在其他子句中的出现,如果另一个子句中的一个文字与单位子句中的文字相同,如上面例子中的 (a∨b∨c∨¬d)(a∨b∨c∨¬d)(a\vee b\vee c\vee\neg d) 子句,我们知道 (a)(a)(a) 的值必须为 TrueTrue\mathrm{True} ,所以这个子句也肯定为 TrueTrue\mathrm{True} ,这意味着这个子句就不会对整个公式产生额外的约束(即 b,c,db,c,db,c,d 的取值不会影响该子句的取值),我们完全可以忽略这个子句,那就删掉它吧。
再考虑上式中第二个子句,其中出现了 (a)(a)(a) 的否定文字,我们知道它不可能为 TrueTrue\mathrm{True} 了,要让这个子句的值为 TrueTrue\mathrm{True} ,只能寄希望于 ccc 的取值了,我们完全可以把 ¬a¬a\neg a 删除(因为有没有它不影响该子句的取值)。
而第上式中第三个子句不包含 (a)(a)(a) 或其否定的出现,即 aaa 的取值不影响这个子句的取值,我们保持其不变即可。
这样,上述公式便被化简为了:
©∧(¬c∨d)∧(a)©∧(¬c∨d)∧(a)©\wedge(\neg c\vee d)\wedge(a)
这个操作就被称为单位子句传播。
概括:对于所有只包含一个文字 LL\mathrm{L} 的子句,对于公式剩余部分中的每个子句 CC\mathrm{C}:
- 若 CC\mathrm{C} 包含 LL\mathrm{L}(非否定),则删除 CC\mathrm{C}。
- 若 CC\mathrm{C} 包含 ¬L¬L\neg\mathrm{L},则删除这个 ¬L¬L\neg\mathrm{L}。
经过一次操作,我们发现公式中又出现了一个新的单位子句 ©©© ,我们可以继续对其实施一遍单位子句传播,一直到整个公式中不存在任何一个单位子句对应的变量在其他子句中出现为止。
上式可被化简为:
©∧(d)∧(a)©∧(d)∧(a)©\wedge(d)\wedge(a)
现在即使公式中每个子句都是单位子句,但是其分别对应的变量 c,d,ac,d,ac,d,a 没有在除单位子句之外的子句中出现了,单位子句传播已经没有用了,我们要实施第二个化简步骤。
第二个化简步骤:孤立文字消去(Pure literal elimination)
如果一个变量在整个公式中只出现了一次,那么我们可以将其进行恰当的赋值,使其所在的子句为 TrueTrue\mathrm{True} 。具体地说,如果其出现的那一次是以否定形式出现的,那么就将变量赋值为 FalseFalse\mathrm{False} ,这可使其对应文字为 TrueTrue\mathrm{True} ,即使其所在子句为 TrueTrue\mathrm{True} ,反正则将变量赋值为 TrueTrue\mathrm{True} ,最终也能使其所在的子句为 TrueTrue\mathrm{True} ,接下来就和上述单位子句传播中发现子句为 TrueTrue\mathrm{True} 时的处理方式相同——删掉这个子句。
一句话概括,就为:删除所有孤立