跳转到内容

DPLL 1962 — 把"逻辑判定"从内存爆炸救成栈式回溯

是什么

DPLL(Davis-Logemann-Loveland)是 1962 年三个人在 CACM 发的一篇 4 页论文,把两年前 Davis-Putnam(DP)那个会内存爆炸的 SAT 算法,改成了栈式回溯版本。

一句话说清楚改动:

DP 用”消变量”——把含 p 和含 ¬p 的子句两两组合(叫归结),子句数量可能平方级膨胀,几十个变量就把 1960 年代的内存吃光。 DPLL 改成”猜一边 + 错了回退”——选一个变量 v,先假设 v=真,沿着推进;矛盾就退回来改成 v=假。整条搜索路径只占一个的空间。

这个改动看着像微小工程优化,但它是今天每一个工业 SAT 求解器(MiniSat / Glucose / CaDiCaL / Z3 内核)的骨架。1962 年那 4 页画的回溯流程图,2026 年的求解器主循环只是它的”加学习子句、加启发式”版本。

为什么重要

不理解 DPLL 的回溯结构,下面这些事都说不清:

  • 为什么 cargo build 解决依赖冲突有时跑很久——DPLL 风格的回溯在最坏情况下指数
  • 为什么 MiniSat 600 行就能教学——主循环就是 DPLL,剩下都是工程包装
  • 为什么 CDCL(1996)说自己是”DPLL + 学习” 而不是另起炉灶——它真的只在 DPLL 上加了一个动作
  • 为什么 Z3 验证一行代码可能秒过、可能跑半小时——DPLL 树的形状对实际输入极敏感

核心要点

DPLL 的伪代码可以缩到 6 行

DPLL(F):
F = unit_propagate(F)
F = pure_literal_eliminate(F)
if F 已空 → 可满足
if F 含空子句 → 不可满足
选一个未赋值变量 v
return DPLL(F ∧ v) or DPLL(F ∧ ¬v)

三个动作:

  1. 单元传播(Unit Propagation):某子句只剩一个文字 p,那 p 必须为真——立刻赋值,把所有含 p 的子句删掉(已满足),所有含 ¬p 的子句里把 ¬p 划掉(继续推下一轮)。
  2. 纯文字消除(Pure Literal):变量 q 在剩下子句里只以正面出现(或只以负面),直接令它取那个方向,相关子句全删掉。
  3. 分裂(Split / Decide):上面两步都不动了,挑一个变量 v先递归 v=T;这一支返回”不可满足”就回溯改试 v=F

注意:第三步的 or短路——左边一旦说”可满足”就立刻返回,不管右边。

回溯本身不需要重新存一份 F:在 1962 年的实现里,赋值变化用一个记录,回退就是 pop 到上一个决策点。这是和 DP 最关键的差别——空间从指数变成线性

实践案例

案例 1:手算一题,看回溯发生

(a ∨ b) ∧ (¬a ∨ c) ∧ (¬b ∨ ¬c) ∧ (¬a ∨ ¬c)

逐步走:

  1. 没有单元也没有纯文字 → 进入分裂
  2. a,先试 a=T
  3. 单元传播:(¬a ∨ c)(c)c=T(¬a ∨ ¬c)(¬c) → 矛盾 ✗
  4. 回溯:撤销 a=Tc=T,改试 a=F
  5. 单元传播:(a ∨ b)(b)b=T(¬b ∨ ¬c)(¬c)c=F
  6. 全部子句已满足 → 可满足a=F, b=T, c=F

整个过程从一次错误猜测里只回退一步就找到答案。这就是 DPLL 比 DP 高效的关键——失败局部化,不污染整个状态。

案例 2:DP 和 DPLL 的真实差异(不是同一个算法)

很多教材把两者并称”DPLL”,但 1960 年 DP 和 1962 年 DPLL 的内部行为完全不一样:

DP 1960DPLL 1962
消变量手段归结(resolution)分裂 + 回溯
空间复杂度子句数最坏指数决策栈线性
1960 年代实战上限~20 变量~200 变量
现代后裔几乎无(归结只在预处理)MiniSat / CaDiCaL 全家桶

CDCL(1996)就是 DPLL 加一条:“冲突时不只是回退,还学一条新子句记下这个区域不能再去”。

案例 3:为什么单元传播是最重要的优化

研究测出现代 SAT 求解器90% CPU 时间花在单元传播上——不是分裂,不是学习,就是这个最朴素的步骤。

原因:每次决策一个变量,往往会触发链式单元传播——一个变量定下来,几个子句变成单元,再定下几个,再触发更多。一个分裂可能瞬间确定几百个变量。

工程优化(watched literals, Moskewicz 2001)让单元传播的均摊代价是 O(1)。这是 SAT 求解器从”几千变量”跳到”百万变量”的真正功臣,而不是更聪明的启发式。

案例 4:在你电脑里 DPLL 在哪儿

  • cargo build / npm install:版本约束 → SAT → DPLL/CDCL 求解(PubGrub 是它的优化变体)
  • Z3 验证 assert(x + 1 > x):把溢出条件编成 SAT,DPLL 内核搜索反例
  • Intel/AMD 芯片设计:每条 RTL 用 BMC(Biere 1999)展开成 SAT,调 DPLL 检查
  • KLEE 符号执行:每条路径约束送 SAT 求解器决定可达性
  • Sudoku 数独应用:内部就是把 729 个 0/1 变量编成 SAT 求解

每次你按回车,背后都有一个 DPLL 的曾孙在跑回溯。

踩过的坑

  1. 变量选择顺序决定生死:朴素 DPLL 按字典序选变量,一个不好的顺序会让搜索树膨胀 100 倍。zChaff 2001 引入 VSIDS 启发式(按变量在最近冲突里出现频率打分),是 DPLL 走向工业的关键。

  2. 纯文字规则在工业 SAT 里几乎不用:维护”某变量只以正面出现”需要扫所有子句,开销大于收益。教材还在讲,CaDiCaL 这类 SOTA 已经只在预处理时用一次。

  3. 没有学习的 DPLL 会反复掉进同一个坑:纯回溯不记录”这条路死过”——同一个矛盾会在搜索树不同位置触发。CDCL 加的”冲突子句学习”正是补这个。

  4. 递归实现栈会爆:变量数超几千的实例不能用真递归,要写显式决策栈 + 迭代。这是早期实现的常见翻车点。

  5. 不要把 DP 和 DPLL 混为一谈:面试题里问”DP 算法”经常被答成”DPLL”——它们是两个不同的算法,1960 那篇用归结,1962 这篇才是回溯。

适用 vs 不适用场景

适用

  • 命题可满足性(SAT)—— DPLL/CDCL 是黄金路径
  • 离散组合搜索(数独、调度、依赖解析、图着色)
  • SMT 求解器的布尔骨架层(Z3 / CVC5)

不适用

  • 一阶逻辑带量词的完全判定 → 半判定问题,需 Herbrand 展开 + DPLL 当子程序
  • 实数 / 非线性约束 → SAT-modulo-Theories 的 Theory 层(DPLL 只管布尔骨架)
  • 优化问题(找最优而不是判定可满足) → MaxSAT / MIP
  • 概率推理 → DPLL 不擅长,用 #SAT / Bayesian 方法

历史小故事(可跳过)

  • 1960:Davis-Putnam JACM 论文,归结消变量。理论漂亮,工程不行。
  • 1962:Davis 找 NYU 学生 Logemann、Loveland,写了一个实际能在 IBM 704 上跑的版本——这就是 DPLL,CACM 4 页。
  • 1971:Cook 证明 SAT 是 NP-完全——给 DPLL 的最坏指数复杂度盖了帽。
  • 1996:Marques-Silva 在 GRASP 求解器引入 CDCL——DPLL 历史最大升级。
  • 2001:Moskewicz 等人写出 zChaff/Chaff——VSIDS + watched literals,第一次跑 100 万变量级。
  • 2003:Eén & Sörensson 写 MiniSat,600 行,成为后来所有教材的参考实现。
  • 2017+:CaDiCaL(Biere)成为新 SOTA,仍是 DPLL 主干。

从 1962 年那 4 页 CACM,到 2026 年百万行的 SMT 求解器,栈式回溯这个核心从未改变。

学到什么

  1. 算法演化常常不是”换思路”,而是”换数据结构”:DP → DPLL 没换问题,没换规则,只把”消变量”换成”决策栈”,立刻把空间从指数压成线性。
  2. 简单算法 + 工程打磨 > 复杂算法:DPLL 主循环 4 行;让它快的所有工作(VSIDS / watched literals / 学习子句 / 重启策略)都是外围工程,主干没动。
  3. 理论可判定 ≠ 工程可解:SAT 是 NP-完全,最坏指数;但工业上 1000 万变量也跑得动——靠的是输入结构友好 + 学习剪枝
  4. 一篇 4 页论文可以管 60 年:DPLL 1962 的回溯结构,今天的 CaDiCaL / Z3 还在用。

延伸阅读

  • 论文 PDF:Davis-Logemann-Loveland 1962 CACM(4 页,符号密度高,能读完)
  • 教科书:Biere et al. Handbook of Satisfiability(2009 / 第二版 2021,1000+ 页 SAT 圣经)
  • 自己写一遍:MiniSat 源码 600 行——现代 DPLL+CDCL 最小教学实现
  • 视频:Donald Knuth SAT Solvers 公开课(Stanford 2015)
  • davis-putnam-1960 —— DPLL 的直接前身,看二者差别
  • biere-bmc-1999 —— DPLL 在硬件验证的最大应用
  • clarke-cegar-2003 —— 抽象细化循环把验证拆成多次 SAT 调用
  • cook-levin —— SAT 是 NP-完全,DPLL 最坏指数的理论上限

关联

  • davis-putnam-1960 —— DP 是 DPLL 的前身;DPLL 把归结换成回溯
  • biere-bmc-1999 —— BMC 把硬件设计展开成 SAT 让 DPLL 跑
  • clarke-cegar-2003 —— CEGAR 抽象细化在每轮调一次 SAT 求解器
  • cook-levin —— 证明 SAT 是 NP-完全,给 DPLL 复杂度盖帽
  • hoare-logic —— 程序验证目标;条件常被翻成 SAT 由 DPLL 后裔自动处理

反向链接

  • biere-bmc-1999 —— Bounded Model Checking — 把硬件验证翻译成一道 SAT 题
  • chaff-2001 —— Chaff 2001 — 把 CDCL 工程化的两个杀手锏
  • clarke-cegar-2003 —— CEGAR — 用反例自动改进抽象,让大软件能被验证
  • cook-levin —— Cook-Levin 定理 — NP-完全性的诞生
  • davis-putnam-1960 —— Davis-Putnam 1960 — 让机器自动判断一堆逻辑式能不能同时成立
  • hoare-logic —— Hoare Logic — 把”程序对不对”变成”数学证明对不对”
  • marques-silva-grasp-1996 —— GRASP 1996 — 让 SAT 求解器从冲突里学到东西
  • minisat-2003 —— MiniSat 2003 — 600 行 C++ 把 CDCL 写成教科书
  • nelson-oppen-1979 —— Nelson-Oppen 1979 — 让多个判定程序坐下来交换”我刚发现 a=b”
  • nieuwenhuis-dpll-t-2006 —— Nieuwenhuis-Oliveras-Tinelli 2006 — 给 SMT 求解器写一套数学规则书