CDCL:从冲突到学习子句
前置知识:SAT 与 DPLL中的部分赋值、单位传播和归结。
冲突为何值得保留
冲突驱动子句学习(Conflict-Driven Clause Learning,CDCL)在搜索遇到冲突时,分析哪些赋值共同导致了矛盾,并学习一个禁止该组合的子句。学习子句应是当前公式的逻辑后果;在不涉及额外公式变换的基本 CDCL 中,由原始子句和已证实的学习子句归结得到新子句,不会删除原公式的任何模型 [1, pp. 140–143]。
用 表示原始公式, 表示新子句,则
学习的目的不是改变答案,而是让原本隐含的约束更早参与传播。
赋值轨迹、决策层与原因
求解器用赋值轨迹(trail)保存按发生顺序排列的赋值。每个赋值至少需要记录:变量的值、决策层,以及传播它的原因子句;决定取值的赋值标记为决策,未赋值变量没有有效决策层或原因。
根层记为 。每做一次分支决策,就进入新的决策层;在该决策之后、下次决策之前产生的传播归于当前层。只有单位传播结束且没有冲突,才开始下一次决策。冲突发生时,传播可以立即停止 [1, pp. 135–140]。
原因子句允许反向解释赋值。若 在 时传播 ,就记录 是 的原因。概念上的蕴含图用 、 表示这个依赖;两个前提必须共同成立,单条边不表示独立的逻辑蕴含。
一个贯穿传播、学习与回跳的例子
考虑八个变量 和六个子句:
下面固定一种合法的决策和传播顺序,便于手算:
- 第 层决定 ,由 传播 。
- 第 层决定 , 被满足,没有进一步传播。
- 第 层决定 ,由 传播 。
- 处理 时,先由 传播 ,再由 传播 。
- 的两个文字都为假,发生冲突。
用下标表示决策层,并在括号内写传播原因,轨迹是
这个顺序不是唯一的。例如先处理 引起的传播,也可能得到 ,继而在 上发现冲突。冲突子句和推导过程依赖实际传播顺序,因此分析时必须与记录下来的轨迹一致。
先把冲突写成归结推导
从被违反的子句 出发,按轨迹逆序消去当前层中较晚的传播变量:
因此可以学习
每一步都是可靠的归结。直观上, 会分别强制 ,与 冲突,所以 和 不能同时为真。这个结论不依赖第 层对 的决定。
为什么在这里停止:1-UIP
在当前决策层的蕴含图中,从该层决策节点到冲突节点的每条路径都经过的赋值节点,称为唯一蕴含点(unique implication point,UIP)。当前层的决策节点本身也是一个 UIP;最靠近冲突的 UIP 称为 first UIP,通常写作 1-UIP [1, p. 143]。
本例中,从 到冲突的两条分支分别经过 和 ,都通过 ,所以 是 1-UIP, 是更早的 UIP。较低层的 是此时固定的上下文。
用逆序归结实现时,一直消去当前层中较晚的传播赋值,直到所得子句只含一个当前层文字。本例中 含 两个当前层文字, 含 两个,而 只含 ,因此停止。这是一个可执行的停止条件,不需要显式构建整张蕴含图 [2, sec. 4.4]。
对同一次冲突,沿这条当前层归结链继续追溯到更早的 UIP,可能增加较低层文字,并使回跳层保持不变或升高;这一结论有固定推导方式的前提。它不等于说 1-UIP 子句是所有逻辑后果中最短的子句,也不能替代额外的学习子句最小化 [1, pp. 143–145]。
回跳到哪一层
设学习子句为 ,其中 是唯一的当前层文字。标准断言式回跳选择其他文字的最大决策层:
如果没有其他文字,即学到单位子句,则取 。撤销所有高于 层的赋值后,其他文字仍然为假,而 变成未赋值,因此新子句成为单位子句并传播 ;这称为断言性质 [1, pp. 143–144]。
本例中 位于第 层,所以从第 层回跳到第 层,跳过了无关的第 层。撤销 后, 仍保留, 立即传播 ,随后 传播 。这里传播的是 ,不能把这个过程简化成“翻转最近一次决策”。
继续决定 并补全 ,得到一个模型:
逐一代入可见六个原始子句均为真。例子同时说明:当前分支发生冲突,不代表整个公式 UNSAT。
1-UIP 分析:逆序扫描与标记
前面的归结过程可以直接沿 trail 实现,不必先画出蕴含图。下面沿用本文的基本 CDCL 设定:根层赋值由公式本身推出,每个非根层只有一次决策,并且先传播到不动点才做下一次决策。过程仅处理当前层 的冲突,根层冲突由外层循环直接判为 UNSAT [2, sec. 4.4]。
trail 中保存的是当前为真的文字,索引从 开始。 返回文字的变量, 返回变量的决策层, 返回传播文字 的原因子句。若 是决策文字,就没有原因子句。
扫描过程维护四项状态:
- 保存已经标记、尚未消去的变量,用来避免在依赖汇合处重复计数。
- 收集最终子句中的较低层文字,保持它们在原因子句中的符号。
- 记录已经发现、尚未处理的当前层变量数。
- 从 trail 末端向前移动,选择最新的已标记赋值。
仅表示本轮要扫描的子句文字:最初是冲突子句,之后是去掉被传播文字的原因子句。它不是每一步完整的归结结果;已经发现的其余依赖保存在 和 中。下面是算法伪代码,读取求解器的 trail、决策层和原因记录,不是可独立运行的程序。
Algorithm 1 First-UIP Conflict Analysis
1:function AnalyzeFirstUIP()
2:
3:, ,
4:,
5:repeat
6:for all do
7:
8:if and then
9:
10:if then
11:
12:else
13:
14:while do
15:
16:,
17:
18:
19:if then
20:
21:until
22:,
23:
24:return
每轮扫描完 时, 等于当前依赖中尚未处理的当前层变量数。若取出最新的赋值 后 ,就用 的原因展开它的前提,相当于消去当前归结式中的 。若取出 后 ,说明展开它之前只剩这一个当前层变量,因此保留 并停止;此时不能再继续消去它。这就是 1-UIP 停止条件 [2, sec. 4.4; 1, pp. 143–144]。
在取出 并将 减一之后,若 ,那么 必有传播原因:该层决策在 trail 中早于所有本层传播,若已经退到决策文字,它后面的本层依赖就已处理完,此时必有 。因此算法不会对一个需要继续展开的决策文字读取原因。根层变量被跳过,是因为它们的真值已由公式推出;这相当于使用根层事实做单位归结,不是任意删除子句文字。
将算法用于前面的 冲突,可以逐轮核对:
- 扫描 ,标记 ,得到 。逆序取出 后 ,下一轮扫描 。
- 新标记 后 。继续逆序取出 ,令 ,下一轮扫描 。
- 已标记,不重复增加 ; 在较低层,将 放入 。取出 后 ,停止并返回 、、。
这里的符号方向很重要:trail 中记录 为真,学习子句中保留的是 。 在每次调用时重新初始化;工程实现通常复用标记数组,并显式清理本次使用过的标记。标记和计数减少了重复检查,但不改变前面逐步归结得到的结论。
学习子句最小化:什么时候可以删除文字
1-UIP 规定在当前层的归结何时停止,并不保证所得子句已经没有冗余文字。较低层赋值之间仍可能存在可利用的原因关系。删除一个文字会加强子句,因此必须证明加强后的子句仍是公式的逻辑后果;局部最小化和递归最小化就是两种利用原因关系的检查方法 [1, pp. 144–146]。
局部最小化:一次归结就能删去一个文字
另外考虑一个独立于前面 的小公式:
第 层决定 ,由 传播 ;第 层决定 ,先由 传播 ,再由 传播 ,在 上发现冲突。按照这条轨迹分析:
中只有 属于当前层,因此已经满足 1-UIP 的停止条件。但 的原因 仍可继续利用:
这次归结去掉 ,加入的 原本就在 中,所以净效果是删除一个文字。它没有改变结论的可靠性,也保留了断言文字 ;回跳到第 层后, 使 立即传播 。
更一般地,若 , 的原因是 ,并且 的全部文字已包含在 中,那么归结得到 ,即可删除 。这是一项足够条件,需要检查原因中的其余文字,而不是只检查 是否来自传播 [1, p. 145]。
递归最小化:沿多步原因链证明冗余
把上述 中的 换成两个子句,其余子句不变:
现在第 层的传播链是 ,第 层的冲突分析仍得到 。只查看 的直接原因 时,会遇到不在 中的 ,因而上述局部检查不能直接删掉 。继续追溯 的原因,则有
递归检查可以把其他保留文字所对应的赋值作为已知边界,沿候选文字的原因向前追溯。若所有依赖最终都落到这个边界或已经证实的根层事实,就能证明候选文字冗余;若遇到边界之外、没有原因可继续展开的决策赋值,这条检查路径就不能完成删除证明。检查失败只表示该方法尚未证明可删,不意味着不存在其他证明 [1, pp. 145–146]。
反例:更短不等于仍然正确
回到最初的四子句公式 。已经证明 ,但不能再任意删除其中一个文字:
- 若删去 ,得到单位子句 。赋值 满足全部四个原始子句,却使 为假。
- 若删去 ,得到单位子句 。赋值 同样满足 ,却使 为假。
因此,这两个进一步缩短的子句都会排除原公式的合法模型。最小化算法应保留 1-UIP 的断言文字,对其他文字做有依据的删减,并按最终子句重新计算回跳层。这里的“最小化”通常是依据特定规则消除冗余,不承诺求得所有逻辑后果中长度最短的子句。
CDCL 的基本循环
下面用伪代码表示 CDCL 的基本循环。AnalyzeFirstUIP 使用前文的逆序扫描过程,返回初步的 1-UIP 子句,本循环未启用进一步的学习子句最小化。其余辅助过程共同操作当前求解器的状态,具体实现仍未展开,伪代码不能独立运行。返回满足赋值表示 SAT,返回 表示无解。 是传播返回的冲突子句; 表示没有冲突,与空子句不同。冲突分析显式给出学习子句 、回跳层 和断言文字 。
Algorithm 2 CDCL
Input: CNF formula
Output: A model of , or Unsat
1:Initialize()
2:while true do
3: Propagate()
4:if then
5:if then
6:return Unsat
7: AnalyzeFirstUIP()
8:Backtrack()
9:AddClause()
10:Enqueue()
11:else if AllAssigned() then
12:return Assignment()
13:else
14:Decide(ChooseLiteral())
Initialize 初始化公式、赋值轨迹和根层传播队列;Propagate 没有冲突时返回 ,有冲突时返回冲突子句,空子句也属于冲突。Enqueue 的第二个参数记录原因子句;Decide 合并了新建决策层与记录无原因的决策赋值。
该伪代码强调“传播—冲突分析—回跳—加入学习子句—断言传播”的顺序,省略重启、子句删除和公式预处理。根层冲突之所以足以回答 UNSAT,是因为此时矛盾不再依赖未撤销的分支假设;若需要让外部检查器验证结论,还要输出相应证明轨迹 [1, secs. 4.2, 4.4.1.4]。
双监视文字如何减少传播工作
单位传播频繁发生。若每赋值一个变量都重新统计所有相关子句的状态,维护成本可能很高。双监视文字为长度至少为 的子句维护两个监视位置,只在监视文字变假时检查是否需要移动监视或触发传播;单位子句单独处理 [1, pp. 149–151]。
设 刚刚变假,另一个监视文字为 :
- 若 为真,子句已经满足,可以暂不处理。
- 否则在非监视文字中寻找一个非假文字,即真或未赋值的文字;找到就用它替换 。
- 若找不到替代项,且 未赋值,子句成为单位子句,传播 为真。
- 若找不到替代项,且 也为假,子句冲突。
因此,“找不到替代项”不总是传播:还必须检查另一个监视文字。监视位置也不要求永远指向未赋值文字。
赋值立即生效,传播队列随后处理
trail 同时承担赋值记录和传播队列的作用。把文字 入队时,先把它设为真、记录当前决策层与原因,再将它追加到 trail。此时所有子句都能查询到这个真值,但由 引起的后续传播尚未处理。
用从 开始的指针 标记下一个待处理位置。正常无冲突地推进时, 中的事件已经处理, 中的事件仍在等待。读出 后递增 ,再检查那些监视了 的子句;新传播的文字继续追加到同一条 trail。无冲突且 时,才达到传播不动点 [2, secs. 3, 4.3]。
本文记 为监视文字 的子句列表,因此处理 时访问 。MiniSat 将监视条目存放在互补文字的索引下,源码相应访问 watches[p];两者只是索引约定不同,不能混用 [3]。
将四种情况放进传播循环
下面的伪代码假定初始化已经删除重复文字和重言式,在开始传播前为长度至少为 的子句建立了两个不同的监视位置,并将单位子句入队。空子句或互相矛盾的初始单位赋值由初始化过程报告冲突。
Algorithm 3 Propagation with Two Watched Literals
1:function Propagate()
2:while do
3:,
4:for all do
5: OtherWatch()
6:if then
7: FindNonFalse()
8:if then
9:MoveWatch()
10:else if then
11:return
12:else
13:Enqueue()
14:return
返回 、 或未赋值状态 ,并考虑文字的正负号。FindNonFalse 返回一个值不为 的候选文字;不存在时返回 。MoveWatch 同时修改子句中的监视位置,把对应条目从旧列表移到新列表。走到 Enqueue 时,另一个监视文字 必为未赋值状态,因此可以将 置真,并把 记录为原因。
Snapshot 表示遍历本轮待处理条目的快照,使移动监视时不会漏掉尚未访问的条目。这是便于说明语义的写法;实际实现可用读写指针原地压缩列表。冲突提前返回时,也必须保留未处理条目的监视关系。伪代码读取共享的 trail、指针和监视列表,辅助数据结构未展开,不能独立运行 [3]。
沿原来的冲突例子跟踪队列
沿用前面的六子句公式。假设 最初监视 。第 层处理 时, 变假; 尚未赋值,可以把这个监视位置移到 ,因此随后 监视的是 。
第 层传播完成后,,。第 层决定 后,可以按下面的合法顺序处理:
- 把 追加到 trail。处理 时, 从 变为 ,由 将 置真并入队。
- 处理 时, 变为 。先检查 :另一个监视文字 未赋值,剩余候选 为假,所以传播 。再检查 ,传播 。此时 trail 已是 。
- 处理 时, 变为 。在 中,另一个监视文字 已经为假,因而返回冲突。虽然 对应的队列事件尚未处理, 的赋值已经生效。
这说明冲突可以发生在队列尚未排空时。返回冲突后,当前条目的扫描也可能尚未完成,不能据此认为传播已达到不动点;外层循环需要先分析冲突并回跳。
回跳时重置队列,但不恢复历史监视位置
标准回跳截去 trail 中高于目标决策层的后缀,撤销相应赋值和原因记录,并把 设为保留前缀的长度。这里的前提仍是每次新决策前已经传播到不动点,所以保留下来的较低层前缀已经处理完 [3]。
本例回跳到第 层后保留 ,令 。加入学习子句 ,再将断言文字 入队,得到 。下一次 Propagate 从索引 开始,由 继续传播 。
新加入的非单位学习子句还需要正确建立监视。常见做法是监视断言文字,以及其余文字中决策层最高的一项,使后续回溯仍满足监视方案所依赖的赋值顺序;单位学习子句则单独在根层入队。MiniSat 的冲突分析会把这两项安排在学习子句的前两个位置,随后由子句入库过程建立监视 [3]。
在标准 trail 后缀回溯和正确维护监视列表的条件下,不需要恢复监视位置的历史状态。撤销赋值不会产生新的“监视文字变假”事件;以后再次赋值时,仍由当前监视列表触发检查。这里保留的是监视结构,变量赋值、原因和队列边界仍必须按回跳结果更新 [2, secs. 3, 4.3]。
阻塞文字:在读取子句前确认它已经满足
监视条目还可以缓存一个属于该子句的文字,称为阻塞文字(blocking literal)。若查询到它当前为真,就能确认子句已经满足,直接跳过这一条目,从而减少对子句存储的访问。它为假或未赋值时不能下结论,仍需读取子句并按前面的规则处理。
缓存的文字不必始终是当前另一个监视文字,但必须仍属于该子句;若子句被改写,相关缓存也要得到正确维护。上面的教学伪代码省略了这一优化,MiniSat 的 propagate 则在读取子句之前先检查 blocker [3]。
传播过程在当前子句数据库上维护赋值;预处理与内处理还会改变子句或变量表示,需要同时说明变换保持的语义和模型恢复方法。
分支、重启与子句管理
这些机制影响搜索效率,但需要与前面的正确性依据分开理解。完整的更新算例、重启规则与比较方法见 CDCL 搜索策略。
分支启发式。 VSIDS 一类方法提高近期参与冲突分析的变量活跃度,并通过衰减降低旧事件的权重;分支时优先选择活跃的未赋值变量。变量选择与取值极性是两个问题,相位保存可记住先前使用的取值。不同实现的加分对象和衰减方式可能不同 [1, pp. 148–149]。
搜索重启。 重启撤销非根层的搜索赋值,通常保留学习子句和部分启发式状态,再从根层继续。它不意味着忘掉已经证明的结论。任意的重启、学习与删除组合不自动保证终止;应结合保留足够进展信息或允许足够长搜索区间等条件讨论完备性 [4, sec. 3.6.4.5; 1, p. 148]。
LBD 与删除策略。 在记录时刻 ,子句 的文字分布在多少个不同决策层上,其文字块距离(literal block distance,LBD)就是多少:
本例学到 时,两个文字分别在第 、第 层,因此记录的 LBD 为 。LBD 与计算时的轨迹有关,不是子句语法固有的常数。低 LBD 常用于优先保留子句,但它是质量启发式,不能保证每个低 LBD 子句都更有用 [1, pp. 148–149]。
删除已被原公式蕴含的学习子句不会改变原公式的模型集合,但实现还须保留有效的原因引用和必要的进展条件。较少的子句降低内存和传播开销,也可能失去未来可复用的推理;这是需要实验评估的取舍 [2, sec. 4.7]。
阅读实现时检查什么
沿同一个例子跟踪求解器时,可以依次检查:
- 每个传播文字是否有一个在传播时确实为单位的原因子句。
- 冲突分析是否得到原公式蕴含的子句,并在预期位置停止。
- 回跳后学习子句是否成为单位子句,断言文字是否被加入传播队列。
- 监视列表、传播队列位置和原因引用是否与撤销后的轨迹一致。
- SAT 结果能否满足全部原始子句,UNSAT 结果是否有可独立检查的证明。
在这个基础上,再讨论分支策略、子句质量或并行共享的改进,才能区分算法正确性、搜索效果与实现成本。
参考文献
1. Joao Marques-Silva, Ines Lynce, and Sharad Malik. Conflict-driven clause learning SAT solvers. Handbook of satisfiability. 2021. DOI.
2. Niklas Eén and Niklas Sörensson. An extensible SAT-solver. Theory and applications of satisfiability testing. 2004. DOI.
3. Niklas Eén and Niklas Sörensson. MiniSat: Core solver implementation. 2013. 原文.
4. Adnan Darwiche and Knot Pipatsrisawat. Complete algorithms. Handbook of satisfiability. 2021. DOI.