SAT 问题概览:从求值到搜索
SAT 问题指的是布尔可满足问题,它基于布尔逻辑,求解的问题可能来自于逻辑系统,但最后要求解的问题形式和方法上和逻辑基本没有太多关系。
正向问题:布尔逻辑和真假(0/1)求值器
出于历史我们还是从逻辑角度去定义布尔逻辑:一种关于句子真假求值的符号化语言。
在这个框架下,人首先要把现实场景组织成陈述句,例如“今天下雨”,“今天下雨又下雪了”,然后尝试判断这些句子的真假。
基于直觉和长期使用,人们逐渐有了关于 “not”“and”“or” 等连接词的真假规则。这样我们就能写出更长的句子,并根据“原子”句子的真假,通过一套固定规则计算整个句子的真假值。
因此,布尔逻辑与普通表达式计算并没有本质区别。区别只在于:
- 算术表达式处理的是数值:
1 * (2 + 3)先计算 2+3 等于 5, 然后 1 乘以 5 等于 5 - 布尔逻辑表达式处理的是真假值或 0 和 1:对于
p & (q | r),如果 p 为“真”,q 为“假”, r 为“真”,那么先计算 q|r 为“真”,然后 p 和真求与得到“真”。
真值表类似于 10 以内的乘法表和加法表,抛开其逻辑解释等问题,对这种真假表达式求值的认知复杂度不会超过小学二年级。
解析与抽象语法树
但要让非人类的机器达到小学二年级里计算数学表达式的能力并不简单,我们冻结关于计算机的原理,在已经有现代计算机的基础上,机器要能对表达式求值,首先要完成的是对字符串的解析(parsing)。
它其实是对字符串建立内部表征,被称为抽象语法树(AST)。
如果建立表征失败,意味着这个字符串并不符合布尔逻辑的语法,比如 &(pq ,解析器看到有 & 之后,它应该预期其前面有一个命题,之后也有一个命题,但它前方没有命题符号,于是停止理解抛出异常。
~(p&q) 是合法表达式,那么就会得到它的 AST 树,比如是
Not(
And(
p,
q
)
)
这里:
- 根节点是 Not
- 它有一个子节点 And
- And 再连接两个叶子节点 p 和 q
相比于字符串,AST 树在求值这个目标下被认为是更稳定的结构,比如 ~(p&q) 可以写成前缀表达式 ~(&pq) 但只要稍微修改解析规则就可以得到一样的 AST ,以内 AST 可以忽略对求值不太重要的表面细节,比如空格,括号是用 () 还是 [], 运算符写在前面还是中间。这些主要是满足人的审美和情绪价值的。
或者说,布尔逻辑表达式的核心不是字符排列,而是其运算结构。
求值
AST 表征对计算机是友好的,因为它是一个递归性质的数据结构,有各种处理这种树的算法,比如遍历,替换子树等等。
对 AST 求值的过程和对一般代数表达式求值完全是一样的。
对于布尔逻辑由于我们关注的最小单位是原子句子,比如 p,q,r 这样的的对象,因此对于只包含 p 和 q 两个原子(叶子)命题的句子,比如 ~(p&q) 它的一个变量赋值就是 {p: 真,q: 假}, 这就是一组赋值,有了它,根据真值表查出 p&q 是假,而假的否定是真,因此最终 ~(p&q) 在 {p: 真,q: 假} 赋值下为真。
这里纯粹是一个给定 value("p")=1, value("q")=0; 计算 value("~(p&q)") 的问题。
它和给定 value("x")=1, value("y")=2; 计算 value("x(x+y)") 在思路上没有什么区别。
对应到 AST 上就是对树进行后序遍历操作,根据子树根节点的运算符求出子树的值,然后不断向上规约。
推理在哪里?
注意,到目前为止,整个系统只是:
- 表达布尔表达式
- 建立 AST
- 根据赋值进行真假求值
日常语境下,通过一些零散的句子 p,q,r 的真相,得到一个更大的句子 p&(q|r) 的真假是一种按照规则进行的信息变换,它可以看作推理。
但严格来说,这里并没有“演绎推理”(deductive inference),因为整个过程中:
- 没有前提集合(premises)
- 没有“从公式推出新公式”的规则
应用这些要素去得到新的句子才是通常所说的逻辑推理。
这里关心的只是布尔逻辑的求值系统,而且即便只有这一部分,就足够去建模显示中的复杂应用,比如组合逻辑电路中实现 ALU,同时也会出现非常复杂的问题,如 SAT 。
局部逆向问题:组合逻辑电路设计和匹配
先看布尔逻辑在数字电路方面的应用。在该领域中,and/or/not 被纯粹看作离散输入输出函数。
从计算角度看,它们只是定义在 {0,1} 上的有限函数,用来作为更复杂函数的组件,类似于数学中的线性函数、指数函数、三角函数在连续系统中的角色。
这里不需要基于任何逻辑推理,只是关于如何通过输入输出的扩充和组合构造出更复杂的有用的离散函数,比如 XOR 门, 半加器、全加器、多路选择器、ALU 。
甚至在工程实践中,根据物理上的成本和效率,基础函数也不一定是 and/or/not, 而可以压缩为单一的 NAND 门,有它就足够组合出 and/or/not。
如果我们已经设计并制作好了电路,比如一个全加器,那么它可以看作一个物理现实版的求值器,其中电路结构就是一组 AST (因为有多个输出,其中有节点会共享,因此更多时候称为计算图),输入电信号,用电进行了正向的求值。
相比之下,在电路设计中,我们面对的是:“从功能描述出发,构造满足该行为的电路结构” 的问题。
例如在设计半加器时,我们希望满足如下输入输出关系:
- (1,0) -> sum = 1
- (0,1) -> sum = 1
- (0,0) -> sum = 0
- (1,1) -> sum = 0
设计者可以从有限的基础函数集合中(如 XOR、AND、OR)寻找满足该行为的组合结构,例如:
- XOR 满足 sum 的行为
- AND 满足 carry 的行为
这看上去这类似“从约束中寻找满足条件的表达式”,是求值的逆向问题,但它并不是一般意义上的组合搜索问题。
因为电路设计需求里特定模块的运算结构一般会非常清晰,覆盖了所有不同输入下的值。
比如你被要求设计一个带有特殊功能的 ALU, 那么需求里必然说清楚它能实现那些功能,而逆只需要基于现有的各类加法器,MUX 等去实现。即每次设计往往是在某一层模块空间中做匹配,搜索空间被已经积累的经验缩小到“模式识别级别”,几乎是一个常数查表过程。
这与下一节谈到的 SAT 有显著差别。
全局逆向问题:解方程和搜索
加法器第 8 位是否为 1
回顾前文,求值问题的例子是,给定 a&b 以及一组赋值 a=1, b=0, 然后根据运算表去查结果为 1, 这过程非常快,和表达式的 大小(AST 节点数量)呈线性关系。
逻辑电路设计中,给定了一个函数 f(x,y)=z 的全部定义,去寻找函数 f, 这类似机器学习问题,但 f 的空间并非连续,而仅仅是一个有限的小的已有模块集合。所以它虽然是求值的逆过程,但处理起来也非常高效。
但现在考虑这个问题: "给定一个已经设计好的 8 位全加器,是否存在输入,使最高位输出为 1?"
乍看之下,这似乎毫不困难。我们根据"整数加法"的语义:最高位为 1 ,那么 10000000 就是一个输出,那么输入是 10000000 和 0 就可以了。
我们利用了加法的性质 x+y=z, 当给定 z 之后,要寻找 (x,y) 的解空间,一个线性方程两个未知变量,因此不单纯在某组满足方程的 x,y, 这种解还有无穷个。
虽然加法例子因为线性而简单,但它确实是一个 SAT 问题,即约束可满足问题,或者说就是解方程问题。
解方程的复杂性
现在问题是,给定一个乘法 x*y=z, 如果 z 是一个超大的数,那是否有解?
当然 x=1, y=z 是一组解,但如果 x,y 都小于 z, 那么这就变成一个质数判定问题,它瞬间变得非常困难。
这里重要的不是质数本身,而是 “正向计算或设计”和“逆向求解” 在复杂性上可能完全不同。
SAT 问题也是类似的现象。
给定一个复杂的由 not, and, or 组合的布尔逻辑表达式:
\[ \varphi(x_1,\dots,x_n) \]
问是否存在某组赋值,使整个表达式为真?
即:
\[ \exists x_1,\dots,x_n \quad \varphi(x_1,\dots,x_n)=1 \]
由于不同变量之间会互相耦合, 一个局部选择可能影响整个表达式。虽然每个局部运算规则都非常简单,但整体满足条件的赋值结构可能极其复杂。
理论上,n 个变量,共有 \( 2^n \) 种不同赋值,SAT 问题的目标就是要从在这些可能性中寻找满足全部约束的某个解
因此 SAT 的暴力算法是指数规模搜索。当然,根据上一节加法问题启发,优化的算法一般是要能从表达式中找出某种特殊的结构来简化搜索。
SAT 的不同形态和难度
DNF SAT
首先,任意布尔表达式都可以写成 DNF(析取范式)
其结构是:
\[ (T_1)\lor(T_2)\lor\cdots \]
其中每个 \( T_i \) 称为 term , 它是若干 literal 的 and。
例如以下是一个 DNF 表达式:
\[ (a\land b)\lor(\neg c\land d) \]
线性扫描每一个 term ,只要存在一个不是互相矛盾的表达式(如 \( \neg a\land a \)),整个表达式就可满足了,所以 DNF 形式的 SAT 非常简单,是多项式复杂度。
但实际上,这种 DNF 展开形式就是把所有的可满足的解全部写出来了,它不单单是可满足判定,而是直接给出所有解。
因此,将一般表达式转成 DNF 形式比判断它是否有解更为复杂。
暴力的 DNF 算法是遍历 \( 2^n \) 语义空间,列出巨大的真值表,然后把真值表中结果为 True 的行选出来构成 term 。
比如以 f(a, b) = a XOR b 为例
真值表共 \(2^2 = 4\) 行:
| a | b | 输出 |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
找出输出为 1 的行:
- 第 2 行:a=0, b=1 ; 构成最小项:¬a ∧ b
- 第 3 行:a=1, b=0 ; 构成最小项:a ∧ ¬b
得到 DNF \( f(a,b) = (\neg a \land b) \lor (a \land \neg b) \)
CNF SAT
CNF 的结构是:
\[ (C_1)\land(C_2)\land\cdots \]
其中每个 \( C_i \) 称为 clause ,它是若干 literal 的 or。
例如:
\[ (a\lor b)\land(\neg c\lor d) \]
和 DNF 类似,可以暴力展开 n 个变量的 \( 2^n \) 行的真值表方式构建。
但不同于 CNF 的是,它的展开并不是暴露所有解,它还是一种解之间互相约束的“压缩”解空间状态,要满足 CNF 表达式,意味着所有 clause 都要同时满足,所以判定它并不简单。
这种“仍旧压缩”的状态使得有更高效地转成 CNF 的算法,比如 Tseitin Transformation ,它通过引入辅助变量, 在线性规模内将任意电路或布尔表达式转成等可满足(equisatisfiable)的 CNF。
注意这里并不是转成等价的 CNF 表达式,因为其中增加了变量,所以求解变复杂了一些,但只要能找到转换后的 CNF 的解,那么也找到了原表达式的解。
而 CNF 下表达式被拆分成了多个小的局部子约束,而这些子约束互相之间又相互约束,总体更加结构化了,适合算法去处理。
所以 SAT 的标准形式通常采用 CNF。
2SAT 和 Horn SAT
在 CNF 中还包括 2SAT 和 Horn SAT 两种简单形式,2SAT 每个 clause 最多两个 literal:
\( (a\lor b) \)
由于它可以转化为 \( \neg a \to b \) 形式,因此如果 (a=False) ,为了让 clause 成立,就必须有 (b=True)。
同理还可以得到 \( \neg b \to a \) ,所以一个 literal 相当于只有一个变量取值,整个 2SAT 公式可以转化为一个“蕴含图”问题,并利用强连通分量等图算法在线性时间内求解。
Horn SAT 的每个 clause 最多只有一个正 literal:
\[ (\neg a\lor \neg b\lor c) \]
它对应:
\[ a\land b \to c \]
和 2SAT 类似,可以在多项式时间内解决。
但问题在于没有多项式算法能将一般 CNF 形式等价转成 2SAT 或者 Horn SAT 。
3SAT
3SAT 的结构是,每个 clause 最多包含 3 个 literal
例如:
\[ (a\lor b\lor \neg c) \]
虽然一般 CNF-SAT 无法转到 2SAT, 但却可以在多项式时间规约到 3SAT
每个 clause 只包括 3 个 literal 使得它能够很好地作为分析复杂度的理论工具,3SAT 是 NPC 问题证明参考 16. Cook-Levin Theorem_哔哩哔哩_bilibili
但实践中求解它的算法和求解一般 CNF 是一样的,并没有更多结构优势,反而还会引入额外辅助变量,因此实际中算法还是在 CNF 形式上的。
CNF SAT 基本算法
这部分许多编程或 AI 课程里都有介绍,比如搜索加回溯算法
DPLL
参考 satisfiablility 或其他文章
SAT 为什么难以及优化方向
SAT 其实就是 f(x)=1 的解方程的问题,但相比于最简单的连续线性方程,布尔逻辑表达式里的 and/or/not 都是非线性的,完全没有便于求逆的线性性质。
相比于一般连续函数 f(x), 布尔逻辑表达式是离散的,没有梯度、局部近似、凸性等支持迭代求解的稳定的“启发”信息。单个变量只能从 0 到 1 或从 1 到 0 的 翻转,而这可能导致大量 clause 的满足状态突变。
相比于离散数学里由图,树参与的约束问题,一般 SAT 的 clause 之间没有递归性质,整个解空间被约束打地很散,局部的信息无法高效传播出去,所以基本只能基于搜索,这也是一般 NP 问题的结构。
而 2SAT 之所以简单是因为其中出现了图结构,局部信息通过联通性又能传递出去。
一般 SAT 的难度更接近数论里某一类丢番图方程,都是离散的非线性的。
从线性系统到连续系统,再到图与树结构,最后到一般离散非线性系统,可以看作是高效计算结构逐渐丧失的过程。
优化算法的思路是向离散空间重新引入某些“结构”,用其他更有结构的对象(统计物理模型,神经网络)去给这些离散的点的背景建立额外的几何或统计关联,从而加入更多传播信息的通道,减少纯组合搜索的复杂度。