前言

BCP 完成后,如果在这个过程中发现了冲突(有一个子句的文字全部为假),那么我们认为发生了冲突,需要撤销赋值并回溯。

DPLL 中,我们的做法十分简单,由于在 trail 中记录了已被赋值的文字,且每个变量是通过决策赋值 还是传播赋值 也被记录

因此我们只需要撤销这次决策后传播的所有变量的赋值,并将决策的变量换一个 相位 进行赋值(类似于 DFS),如果这个决策变量两个相位都被尝试了,那么我们考虑回溯到更早的决策变量,然后尝试新的相位。

Attention

注意,发生冲突并不是坏事,就像 VSIDS 启发式 中说的,想成功先失败,越早的发生冲突意味着我们能越早的剪枝,从而减小搜索空间

CDCL 中,我们通过冲突分析与子句学习技术,使得 CDCL 能够进行非时序回溯(也就是回跳),且学习到 CNF 中一些隐含的信息,我们称为学习子句

我们回顾 SAT 问题简介 中提到的实例:

p cnf 7 8
-5 7 0
-1 -5 6 0
-1 -6 -7 0
-1 -2 5 0
-1 -3 5 0
-1 -4 5 0
-2 -3 -4 5 0
-1 2 3 4 5 -6 0

通过这个实例来解释这一节的内容

子句学习

假定在上文的例子中,我们的部分赋值为 ,通过一次 BCP 传播,我们可以得到部分赋值 ,此时我们发现子句 为假,说明我们遇到了冲突

回顾 DPLL ,我们的做法是撤销 的赋值,然后尝试 ,继续做 BCP

然而,我们在 CDCL 中可以通过冲突分析,学习到一个 CNF 中蕴含的约束:,也就是说 最多只能有一个为真,有了这样优质的约束,我们就能够快速 BCP 出其他变量的赋值(信息越多,我们能做出的决策就更好)

为了做到子句学习,我们首先引入一些概念

蕴含图 (Implication Graph)

蕴含图记录变量赋值决策和推导关系的有向无环图,例如上文的例子,其蕴含图如下所示:

image.png|200

可以发现我们在第三条子句发生了冲突

唯一蕴含点 (UIP)

在当前的决策层中,如果一个节点满足:从当前决策节点到冲突节点的所有路径都必须经过该节点,则称为 UIP。

第一唯一蕴含点(First-UIP)就是离冲突节点最近的 UIP。

在上图中,决策节点只有两个

归结(Resolution)

给定两个子句:

我们可以归结得到:

归结是 SAT 推理的理论基础

具体流程

构建学习子句的过程本质就是找到 First-UIP 的过程,以上文为例子,我们的做法是:

假定初始的冲突子句为

  1. 忽略 ,因为是决策得到的

  2. 是通过子句 得到,我们将冲突子句与原因子句归结,得到

  3. 通过子句 得到,归结后得到

此时,学习子句中只有一个 是来自于当前决策层的变量,我们就找到了 First-UIP,也得到了学习子句

代码层面的实现

其思路如下:

首先学习子句设置为空,冲突子句设置为 ,计数器 open 设置为 ,我们从冲突子句开始,对子句中的每个文字进行分析:

  1. 对于 ,由于我们在第一层进行决策,其决策层小于当前层级,于是我们将其加入学习子句中 ,并将此变量标记为 seen,防止重复添加
  2. 对于 ,决策层级相同,我们不加入学习子句中,但将其标记为 seen 并增加 open 计数(增加
  3. 对于 ,我们的做法与上一步一样

此时,我们分析完了冲突子句,开始寻找 First-UIP,假定当前的 trail,我们后向前进行遍历

  1. 第一个被标记为seen且在当前层级的变量是 ,我们设置 uip,将 open,并获取 的原因子句 ,并对原因子句进行分析(不分析当前 uip):

    1. 对于 ,其决策层相同,因此我们不加入学习子句,只是将其标记为 seen 并增加 open 计数(增加
  2. 第二个被标记为seen且在当前层级的变量是 ,我们设置 uip,将 open,并获取 的原因子句 ,并对原因子句进行分析(不分析当前 uip):

    1. 这里三个变量都已经被 seen,直接省略
  3. 第二个被标记为seen且在当前层级的变量是 ,此时,open 已经为 ,我们就找到了 First-UIP,我们将 这个文字加入到学习子句中

于是我们得到了学习子句为:

回跳

Tip

在构建完成后,我们会对当前学习子句的文字做一次排序,按照赋值的顺序来排序,那么学习子句会变为

CDCL 找到 First-UIP 后,我们回跳到学习子句第二个文字所在的决策层级,然后撤销 trail 中大于该层级的所有赋值,然后 BCP 或翻转 UIP 的赋值(如果 UIP 是被决策的话)