Lecture 4. CSPs I
Constraint Satisfaction Problems
CSP 全称 Constraint Satisfaction Problems,即约束满足问题
不同于搜索问题必须要找出可行解(最优解),CSP 是识别问题:给定一个状态,只需要判断是否为目标状态,不需要考虑具体的实现
CSP 由三个要素构成:变量、域、约束
- CSP 拥有一个域(集合)表示 CSP 可以取的所有可能的值
- 从域中取出的单一的值为变量
- 约束定义了对变量值的限制
CSP 的种类取决于变量和域的离散性。变量和域可以分别是离散的 / 连续的
对于一般的 CSP,无法找到一个在多项式时间内解决的方法,即一般的 CSP 属于 NP-Hard 问题,其完整的状态空间往往巨大(对于一个有 \(N\) 个变量、每个变量域大小为 \(O(d)\) 的 CSP 问题,总共有 \(O(d^{N})\) 种可能的赋值),使用朴素方法(穷举)的时间复杂度是巨大的
以 N 皇后问题为例,对应的 CSP 中:
- 变量为棋盘上的某一格子位置 \(X_{ij}\);
- \(X_{ij}\) 的域为 \(\lbrace 0, 1 \rbrace\),表示 \((i, j)\) 坐标是否存在皇后;
- 约束为“同一行、同一列、同一对角线上只能有一个皇后,总共有 \(N\) 个皇后”
如果只需要构造出一个解,该问题是 P 类问题;如果是预先放置部分皇后,问是否可以补齐到 \(N\) 个皇后,该问题是 NP-Complete(NP-Hard)问题;如果需要求出所有解,该问题是 #P-Complete(#P-Hard)问题(解的数量可参考 A000170 - OEIS)
对于一个 NP 问题,如果我们从问“是否存在”扩展到问“存在多少”,那么就是 #P 问题,很明显后者比前者更 Hard
如果考虑图搜索(回溯剪枝),后两个问题的时间复杂度为 \(O(N!)\),都无法在多项式时间内解决
对于 NP-Hard 的计算量难题,我们通常可以通过将 CSP 表述为搜索问题来规避这个难题,将状态定义为部分赋值(CSP 中对部分变量已赋值而其余变量未赋值的变量赋值)。相应地,CSP 状态的后继函数输出所有新赋值一个变量的状态,而目标测试则验证所有变量是否都已赋值以及正在测试的状态中是否所有约束都得到满足。
约束满足问题往往比传统搜索问题具有更显著的结构,我们可以通过将上述表述与适当的启发式方法相结合来利用这种结构,从而在可行的时间内找到解决方案。
约束满足问题通常建模为约束图,其中节点表示变量,边表示变量间的约束。约束根据涉及到的变量数量有不同处理:
- 一元约束只涉及 CSP 中的一个变量,不会表示在约束图中(图用来表示变量间关系,不便于表示单一变量约束,也就是“自环”),只会用于修剪变量的域
- 二元约束在约束图上表现为一条边
- 高阶约束不便于用一条边表示,需要特殊的标记方法
以图着色问题为例,对应的 CSP 中:
- 变量为地图上的每一块区域;
- 域为可用的颜色集合;
- 约束为“有共同边界的两个区域,颜色必须不同”,为二元约束
可以将每个地区表示为节点,相邻地区对应的节点连边,建模为约束图
约束图的价值在于我们可以利用它们提取有关我们正在解决的 CSP 结构的宝贵信息。通过分析 CSP 的图,我们可以确定它的一些特性,比如它是稀疏连接还是密集连接/约束,以及它是否是树状结构
约束满足问题的传统解法是对约束图使用回溯搜索(backtracking search),其相比传统的 DFS 满足两个原则:
- 按固定的顺序赋值:由于赋值操作的顺序是可变的,所以按照一个合理的顺序进行赋值,有助于减少状态转移(比如 A → B 和 B → A 不会同时发生),并且便于回溯
- 在遇到不可避免的冲突之前,确保当前的每一步不会冲突。如果遇到不可避免的冲突,就进行回溯操作,退回到之前的状态改变赋值
简单来说:Backtracking = DFS + variable-ordering + fail-on-violation
Improvement
回溯搜索存在一定的优化空间,这里给出三种优化思路:
- 过滤:通过提前删除变量值域中的不可能值,减少回溯的发生
- 排序:对变量(及其值)进行某种排序,按照该排序进行回溯搜索,也是为了减少回溯的发生
- 结构:对于特定的 CSP 约束图(比如树形图),使用特化的时间复杂度更低的算法进行回溯搜索
Filtering
过滤操作属于对域的剪枝,通过约束传播,提前删除变量值域中不可能的值。换句话说,预判一定会导致回溯的赋值操作并提前剪除未分配变量的域
Forward Checking
过滤的一种简单方法是前向检查(Forward Checking),每当给变量 \(X_i\) 分配一个值时,它就会剪除与 \(X_i\) 有约束且分配该值会违反约束的未分配变量的域。每当分配一个新变量时,我们可以运行前向检查并剪除约束图中与新分配变量相邻的未分配变量的域。
还是以图着色问题为例:
该 CSP 问题的域为 {R, G, B},考虑下面的约束图:
flowchart LR
A((A)) --- B((B)) --- C((C)) --- D((D))
D((D)) --- A((A))
B((B)) --- D((D))
将 A 染成红色(R)之后,不难发现 B 和 C 一定不会是红色(R),因此 B 和 D 的域过滤为 {G, B}
Arc Consistency && AC-3
将前向检查推广为弧一致性(Arc Consistency),不再局限于当前变量的邻居节点,而是进行全局传播。先给出一些前置知识:
首先引出“域一致性”概念
- 一个变量是“域一致”的,当且仅当域中的任何值都没有被任何约束排除。域一致性仅讨论涉及单个变量的约束
比如上面的图着色例子中,B 和 D 的域过滤为 {G, B},说明 B 和 D 都受到了颜色约束,说明此时 B、D 不是域一致的(C 依旧是域一致性的)。
我们记 \(\langle X, r(X, Y)\rangle\) 为一个弧(arc),其中 \(X\) 是一个节点,\(r(X, Y)\) 为一个约束本身(涉及到 \(X, Y\) 两个变量)。整个弧的含义是“\(X\) 参与了 \(r\) 这一约束”
弧专门用来处理二元约束,一个二元约束会产生双向的两条弧:\(\langle X, r(X, Y)\rangle\) 和 \(\langle Y, r(X, Y)\rangle\),我们接下来定义弧一致性的定义:
- 一个弧是弧一致的,当且仅当 \(\text{dom}(X)\) 中的所有 \(X\) 的取值,在 \(\text{dom}(Y)\) 中都存在某个 \(Y\) 的取值,使得约束 \(r(X, Y)\) 被满足
- 人话:对于 \(X\) 的每个值,都需要在 \(Y\) 的值域里找到至少一个值满足 \(r(X, Y)\),否则 \(\langle X, r(X, Y)\rangle\) 不是弧一致的
接下来引入弧一致性算法:用一个队列来管理所有需要检查的弧,进行如下的操作
- 初始化:把所有的弧按照一定的顺序存入队列 \(Q\)
- 循环体:
- 从 Q 中取出 \(X \to Y\) 的一条弧(即 \(\langle X, r(X, Y)\rangle\))
- 对 \(X\) 的所有取值进行弧一致性检查,如果 \(X\) 的某个取值 \(v\) 无法满足约束,将 \(v\) 从 \(X\) 的域中删除
- 如果 \(X\) 的至少一个取值被移除,那么对于所有的尚未赋值的变量 \(Z\),将 \(Z \to X\) 加入队列(如果已在队列中,则不需要重复入队)
- 如果队列变空,说明整个约束图满足弧一致性,否则某个变量的值域会被清空,即无解,此时进行回溯操作
上面描述的具体算法即为 AC-3 算法,其实现相对简单,空间复杂度低,但是时间复杂度偏高
每次弧一致性检查遍历整个值域 \(O(d^2)\),最坏时间复杂度 \(O(ed^3)\);空间复杂度 \(O(e)\)
其中 \(d\) 是值域大小,\(e\) 是弧的数量
AC-3 算法在之后有一些空间换时间的优化,这里举两个例子:
- AC-4:预先计算所有节点 \(X\) 对应的 \(X\to Z\) 的弧一致的个数,记为 support count,当删除 \(X\) 的一个值 \(v\) 时,找到所有依赖于 \(v\) 的其他值(即那些把 \(v\) 当作唯一支持的值),减少它们的 support count 值,如果某个值的 support count 降到 0,删除它并继续传播
- 最坏时间复杂度 \(O(ed^2)\),但空间复杂度 \(O(ed^2)\)
- AC-2001:对于每个值 \(v ∈ dom(X)\),记住它在 \(Y\) 中上一次成功匹配的值,在下一次检查时从该值开始查找
- 最坏时间复杂度 \(O(ed^2)\),但空间复杂度 \(O(ed)\)
Extra: k-consistency
弧一致性可以进一步扩展为 \(k\) - 一致性:在 \(k\) - 一致性成立的情况下,必须保证对于 CSP 中的任何 \(k\) 节点集合,对任何 \(k-1\) 节点子集的赋值后,可以确保剩下的那个节点一定可以有一个满足约束的取值
满足 \(k\) - 一致性的约束图也满足 \(k-1\) - 一致性
弧一致性就是 2 - 一致性
在域剪枝技术方面,弧一致性比前向检查更加全面,可以带来更少的回溯次数,但是也会带来更多计算,需要进行权衡
Ordering
我们引入两种排序的原则:“最少剩余值”和“最少约束值”。利用这两种原则来动态计算下一个变量和相应的取值,可以优化回溯搜索的速率
(这两个赋值策略是先后进行的)
- 变量选择:最少剩余值(MRV, Minimum Remaining Values)—— 当选择接下来给哪个变量赋值时,使用MRV策略会选择剩余合法值最少的未赋值变量(即约束最多的变量)。这很直观,因为约束最多的变量最有可能耗尽可能的值,如果暂不赋值就很容易导致回溯,所以最好尽早给它赋值。
- 比如图着色问题一般喜欢优先对最“核心”的地区上色,可以浅浅意会
- 高中做那种求填色方案数的题目,不知不觉就会采用 MRV 策略
- 值选择:最少约束值(LCV, Least Constraining Value)—— 类似地,当选择接下来给当前变量赋哪个值时,一个好的策略是选择那个从剩余未赋值变量的值域中剪除最少值的取值。值得注意的是,这需要额外的计算(例如,为每个值重新运行弧一致性/前向检验或其他过滤方法以找到LCV),但根据使用情况,仍然可以带来速度提升。
Structure
之前提到,对于一个有 \(N\) 个变量、每个变量域大小为 \(O(d)\) 的 CSP 问题,朴素求解的时间复杂度为 \(O(d^N)\)。如果约束图中没有环,那么约束传播不会发生循环依赖,就不再需要回溯
我们可以采用特殊的树结构 CSP 算法,其时间复杂度 \(O(Nd^2)\):
- 任取一个节点作为树的根节点,将所有无向边转化为指向远离根的方向的有向边,得到有向无环图 DAG
- 对 DAG 进行拓扑排序,得到拓扑序
- 从最后一个节点向根节点方向反向执行弧一致性检查,更新每个节点的域
- 从根节点向最后一个节点依次赋值,只要确保每一步赋值不与父节点冲突即可
因为前向赋值的过程一定不会发生回溯,所以时间复杂度从 \(O(d^N)\) 直接降到 \(O(Nd^2)\)
时间瓶颈在于对 \(n-1\) 条边进行弧一致性检查,时间复杂度 \(O(Nd^2)\)
上述算法要求 CSP 约束图完全没有环,而对于有环但不多的约束图,可以进行割集调整:
- 获取约束图的最小割集,其大小为 \(c\)
- 对割集内的所有节点进行赋值尝试,总共有 \(d^c\) 种
- 在对割集赋值之后,剩下的变量的约束图组成一棵树,进行树结构 CSP 算法
总时间复杂度 \(O\left(d^c\cdot (n-c)d^2\right)\),对于较小的 \(c\) 有非常好的优化效果
