一、问题引入

在了解2-SAT的定义之前,我们需要给出一些基础定义。

布尔变量(Boolean variable):只能取 1 1 1(true)或 0 0 0(false)的变量。

否定连接词 ¬ \neg ¬(negation):取布尔变量的否定。例如 ¬ 1 = 0 \neg1=0 ¬1=0 ¬ 0 = 1 \neg0=1 ¬0=1 ¬ ( ¬ a ) = a \neg(\neg a)=a ¬(¬a)=a

合取连接词 ∧ \land (conjunction):表示“且”。 a ∧ b = 1 a\land b=1 ab=1当且仅当 a , b a,b a,b同时为 1 1 1

析取连接词 ∨ \lor (disjunction):表示“或”。 a ∨ b = 1 a\lor b=1 ab=1当且仅当 a , b a,b a,b之中至少有一个为 1 1 1

蕴含连接词 → \to (implication): a → b a\to b ab等价于 ¬ a ∨ b \neg a\lor b ¬ab a → b = 1 a\to b=1 ab=1当且仅当 a = 0 a=0 a=0 b = 1 b=1 b=1。换言之,若 a = 1 a=1 a=1,则必有 b = 1 b=1 b=1,否则蕴含关系不成立;若 a = 0 a=0 a=0,则不论 b b b取何值,蕴含关系都成立。

文字(literal):变量 x x x及其否定 ¬ x \neg x ¬x称为文字。

子句/简单析取式(clause):若干个文字由析取连接词连接起来形成的布尔表达式称为简单析取式。例如 a ∨ b ∨ ¬ c a\lor b\lor\neg c ab¬c ¬ d ∨ d ∨ e ∨ ¬ f \neg d\lor d\lor e\lor\neg f ¬dde¬f

合取范式(CNF,Conjunctive Normal Form):若干个简单析取式由合取连接词连接起来形成的布尔表达式称为合取范式(即“简单析取式的合取”)。例如 ( p ∨ ¬ q ) ∧ ( ¬ p ∨ r ∨ s ) ∧ ¬ r ∧ ( s ∨ ¬ s ) (p\lor \neg q)\land(\neg p\lor r\lor s)\land \neg r\land(s\lor\neg s) (p¬q)(¬prs)¬r(s¬s),它有四个子句。

2-CNF:每个子句仅包含两个文字的合取范式。例如 ( a ∨ ¬ b ) ∧ ( b ∨ c ) ∧ ( c ∨ ¬ f ) ∨ ( ¬ d ∨ e ) (a\lor\neg b)\land(b\lor c)\land(c\lor\neg f)\lor(\neg d\lor e) (a¬b)(bc)(c¬f)(¬de)

布尔表达式的可满足性:对于一个布尔表达式,如果存在各变量的一组赋值,使得该表达式的值为 1 1 1,则称该表达式是可满足的。对于合取范式,因为各子句之间是且的关系,所以要使它取值为 1 1 1就必须使各子句取值都为 1 1 1。对于上面给出的式子 ( a ∨ ¬ b ) ∧ ( b ∨ c ) ∧ ( c ∨ ¬ f ) ∨ ( ¬ d ∨ e ) (a\lor\neg b)\land(b\lor c)\land(c\lor\neg f)\lor(\neg d\lor e) (a¬b)(bc)(c¬f)(¬de),令 a = 1 , b = 1 , c = 1 , d = 0 , e = 0 , f = 1 a=1,b=1,c=1,d=0,e=0,f=1 a=1,b=1,c=1,d=0,e=0,f=1可以使其取值为 1 1 1,所以它是可满足的;但对于式子 ( a ∨ ¬ b ) ∧ ( b ∨ c ) ∧ ( b ∨ ¬ c ) ∧ ( ¬ a ∨ ¬ b ) (a\lor\neg b)\land(b\lor c)\land(b\lor\neg c)\land(\neg a\lor\neg b) (a¬b)(bc)(b¬c)(¬a¬b),不论 a , b , c a,b,c a,b,c取何值,式子的值都是 0 0 0,因此它是不可满足的。

2-SAT:也称2-CNF-SAT,是判断2-CNF是否可满足的问题。(判断一般的合取范式是否可满足的问题是 N P \mathsf{NP} NP完全问题,至今没有发现多项式时间的算法;但2-SAT是一个特例,具有高效算法。)

二、问题求解

1. 转化为蕴含关系

考虑2-CNF的一个子句 ( a ∨ ¬ b ) (a\lor\neg b) (a¬b),要让这个子句满足,就是让 ( a ∨ ¬ b ) = 1 (a\lor\neg b)=1 (a¬b)=1。现在令 a = 0 a=0 a=0,那么 ¬ b \neg b ¬b一定等于 1 1 1,因为如果 ¬ b \neg b ¬b也等于 0 0 0,那么 a a a ¬ b \neg b ¬b同时为 0 0 0,该子句的取值就是 0 0 0了;同理,若 ¬ b = 0 \neg b=0 ¬b=0,则可以知道 a a a一定是 1 1 1。换言之: ¬ a \neg a ¬a a = 0 a=0 a=0可以推出 ¬ b \neg b ¬b,而 b b b ¬ b = 0 \neg b=0 ¬b=0,即 ¬ ( ¬ b ) = b = 1 \neg(\neg b)=b=1 ¬(¬b)=b=1可以推出 a a a。所以, ( a ∨ ¬ b ) (a\lor\neg b) (a¬b)就等价于 ( ¬ a → ¬ b ) ∧ ( b → a ) (\neg a\to\neg b)\land(b\to a) (¬a¬b)(ba)。一般地,设 p , q p,q p,q是两个文字,则子句 ( p ∨ q ) (p\lor q) (pq)等价于两个蕴含关系 ¬ p → q \neg p\to q ¬pq ¬ q → p \neg q\to p ¬qp p = a p=a p=a,则 ¬ p = ¬ a \neg p=\neg a ¬p=¬a;若 p = ¬ a p=\neg a p=¬a,则 ¬ p = ¬ ( ¬ a ) = a \neg p=\neg(\neg a)=a ¬p=¬(¬a)=a

具体地说,我们获得蕴含关系方式由下表给出:

子句蕴含关系
a ∨ b a\lor b ab ¬ a → b \neg a\to b ¬ab ¬ b → a \neg b\to a ¬ba
a ∨ ¬ b a\lor\neg b a¬b ¬ a → ¬ b \neg a\to\neg b ¬a¬b b → a b\to a ba
¬ a ∨ b \neg a\lor b ¬ab a → b a\to b ab ¬ b → ¬ a \neg b\to\neg a ¬b¬a
¬ a ∨ ¬ b \neg a\lor\neg b ¬a¬b a → ¬ b a\to\neg b a¬b b → ¬ a b\to\neg a b¬a

2. 建图

我们把每个子句表示成蕴含关系后,就可以根据蕴含关系建图。假设有 n n n个布尔变量,那么图有 2 n 2n 2n个节点,每个变量及其否定各有一个节点(比如变量 a a a对应两个节点: a a a ¬ a \neg a ¬a)。根据蕴含关系连边,若有 ¬ a → ¬ b \neg a\to\neg b ¬a¬b就从 ¬ a \neg a ¬a ¬ b \neg b ¬b连一条边;若有 b → a b\to a ba就从 b b b a a a连一条边。注意,每个子句连 2 2 2条边:若 p p p q q q有一条边,那么 ¬ q \neg q ¬q ¬ p \neg p ¬p也有一条边(可以通过逆否命题和原命题等价来理解)。

3. 判断可满足性

我们说“ a → b a\to b ab”,指的是若 a = 1 a=1 a=1,那么 b b b一定等于 1 1 1;若 a ≠ 1 a\ne1 a=1,那么 b b b等于什么都可以。蕴含关系有传递性,例如 a → b a\to b ab b → c b\to c bc就有 a → c a\to c ac。若图中有从 a a a b b b的一条路径,那么就有 a → b a\to b ab。若 a a a b b b处于同一个强连通分量中,即 a a a b b b有路径、 b b b a a a有路径,那么就有 a → b a\to b ab b → a b\to a ba,此时,若 a = 1 a=1 a=1,则 b = 1 b=1 b=1;若 b = 1 b=1 b=1,则 a = 1 a=1 a=1;若 a = 0 a=0 a=0,则 b = 0 b=0 b=0,因为如果 b = 1 b=1 b=1的话就有 a = 1 a=1 a=1了。于是我们得出结论:若 a a a b b b处于同一个强连通分量中,则有 a → b ∧ b → a a\to b\land b\to a abba,此时 a a a b b b取值必须相同。

什么时候不可满足呢?就是出现矛盾的时候。所谓矛盾,就是存在变量 x x x,使得 x x x ¬ x \neg x ¬x相等。换言之,如果某个变量和其否定出现在了同一个强连通分量中,那么这个式子就是不可满足的。如果没有这种情况,就是可满足的。

详细地说,如果 x x x ¬ x \neg x ¬x在同一个强连通分量中,那么我们有 x → ¬ x x\to\neg x x¬x ¬ x → x \neg x\to x ¬xx。如果 x = 1 x=1 x=1,那么由 x → ¬ x x\to\neg x x¬x ¬ x = 1 \neg x=1 ¬x=1,即 x = 0 x=0 x=0,但是一个变量不能同时取 1 1 1 0 0 0,所以不成立;如果 x = 0 x=0 x=0,那么 ¬ x = 1 \neg x=1 ¬x=1,由 ¬ x → x \neg x\to x ¬xx x = 1 x=1 x=1,同样出现了矛盾。反之,如果没有一个变量,使得它本身和它的否定在同一个强连通分量中,那么我们总能构造出一个可满足赋值,这就是下一段讨论的内容。因此,“某一个变量的本身和否定在同一个强连通分量中”是输入的2-CNF不可满足的充要条件。

4. 赋值

当我们知道某个式子是可满足的时候,我们希望找到使其值为 1 1 1的一组变量的赋值。此时对于变量 x x x x x x ¬ x \neg x ¬x一定不属于同一个强连通分量。要求出强连通分量,就需要对原图进行缩点,顺便可以求出得到的有向无环图的拓扑序。令 x x x所在强连通分量的拓扑序为 t x t_x tx。我们这样进行赋值:

  • t x < t ¬ x t_x<t_{\neg x} tx<t¬x,则令 x = 0 x=0 x=0
  • t x > t ¬ x t_x>t_{\neg x} tx>t¬x,则令 x = 1 x=1 x=1

t x = t ¬ x t_x=t_{\neg x} tx=t¬x的情况是不存在的,因为 x x x ¬ x \neg x ¬x不在同一个强连通分量中。

如果有从 x x x ¬ x \neg x ¬x或从 ¬ x \neg x ¬x x x x的路径,那么这个很好理解。

  • t x < t ¬ x t_x<t_{\neg x} tx<t¬x就是说有从 x x x ¬ x \neg x ¬x的路径,这种情况下我们有 x → ¬ x x\to\neg x x¬x,只有当 x = 0 x=0 x=0的时候才可满足。
  • t x > t ¬ x t_x>t_{\neg x} tx>t¬x就是说有从 ¬ x \neg x ¬x x x x的路径,这种情况下我们有 ¬ x → x \neg x\to x ¬xx,只有当 x = 1 x=1 x=1的时候才可满足。

下面我们来证明这种赋值办法不会违反任何蕴含关系。

  • 首先,它不会违反 x x x ¬ x \neg x ¬x之间的蕴含关系,因为 x x x ¬ x \neg x ¬x属于不同的强连通分量,二者之间最多只能有一个蕴含关系,而我们赋值的方法总能满足这个蕴含关系。
  • 其次,我们考虑变量 x x x和另一个变量 y y y的蕴含关系。我们假设有 x → y x\to y xy(如果有 ¬ x → y \neg x\to y ¬xy x → ¬ y x\to\neg y x¬y ¬ x → ¬ y \neg x\to\neg y ¬x¬y同理)。我们担心的情况是: x = 1 x=1 x=1 y = 0 y=0 y=0,这样蕴含关系就被违反了。下面我们用反证法证明这种情况不存在。假设 x = 1 x=1 x=1 y = 0 y=0 y=0 x → y x\to y xy三个条件,那么我们可以得出以下结论:
    • x = 1 x=1 x=1知道 t ¬ x < t x t_{\neg x}<t_x t¬x<tx(见两个赋值规则);
    • y = 0 y=0 y=0知道 t y < t ¬ y t_y<t_{\neg y} ty<t¬y
    • x → y x\to y xy知道 t x ≤ t y t_x\le t_y txty。(如果 x x x y y y在同一个强连通分量里,则 t x = t y t_x=t_y tx=ty;如果在不同的强连通分量里,那么我们有从 x x x y y y的一条边,因此 x x x y y y先被访问。至于为什么如果有 x x x y y y的边那么 x x x y y y先被访问,这是拓扑排序的定义。)
  • 综合这三个不等式,我们得到 t ¬ x < t x ≤ t y < t ¬ y t_{\neg x}<t_x\le t_y<t_{\neg y} t¬x<txty<t¬y。但是,我们知道原命题等价于逆否命题,也就是说, x → y x\to y xy成立意味着 ¬ y → ¬ x \neg y\to\neg x ¬y¬x也一定成立,这表明 t ¬ y ≤ t ¬ x t_{\neg y}\le t_{\neg x} t¬yt¬x,与绿色不等式矛盾。因此假设不成立,违反 x → y x\to y xy的情况不存在,换言之,所有蕴含关系都被满足了,这意味着输入的2-CNF公式被满足了。综上所述,我们的赋值策略是正确的。

三、两个例子

第一个例子

给定的2-CNF表达式为 ( a ∨ ¬ b ) ∧ ( ¬ a ∨ b ) ∧ ( b ∨ ¬ c ) ∧ ( a ∨ c ) (a\lor\neg b)\land(\neg a\lor b)\land(b\lor\neg c)\land(a\lor c) (a¬b)(¬ab)(b¬c)(ac)

例子1

图包含四个强连通分量: { a , b } , { c } , { ¬ a , ¬ b } , { ¬ c } \{a,b\},\{c\},\{\neg a,\neg b\},\{\neg c\} {a,b},{c},{¬a,¬b},{¬c}。没有出现某个变量及其否定在同一个强连通分量的情况,所以是可满足的。进行缩点:

例子1缩点

拓扑序为 t ¬ a , ¬ b < t c < t ¬ c < t a , b t_{\neg a,\neg b}<t_c<t_{\neg c}<t_{a,b} t¬a,¬b<tc<t¬c<ta,b。因为 t ¬ a < t a t_{\neg a}<t_a t¬a<ta,所以 a = 1 a=1 a=1。同理 b = 1 b=1 b=1。因为 t c < t ¬ c t_c<t_{\neg c} tc<t¬c,所以 c = 0 c=0 c=0

a = 1 , b = 1 , c = 0 a=1,b=1,c=0 a=1,b=1,c=0代入2-CNF中验算可得其值为 1 1 1

第二个例子

给定的2-CNF表达式为 ( a ∨ ¬ b ) ∧ ( b ∨ c ) ∧ ( b ∨ ¬ c ) ∧ ( ¬ a ∨ ¬ b ) (a\lor\neg b)\land(b\lor c)\land(b\lor\neg c)\land(\neg a\lor\neg b) (a¬b)(bc)(b¬c)(¬a¬b)

例子2

所有节点都在同一个强连通分量中,所以这个式子不可满足。

四、基于Tarjan算法的代码实现

详细思路

设有 n n n个变量, m m m个子句,则有 2 n 2n 2n个节点。编号 i i i 1 ≤ i ≤ n 1\le i\le n 1in)的节点对应变量 x i x_i xi i + n i+n i+n对应 ¬ x i \neg x_i ¬xi。根据每个子句建图,图中共 2 m 2m 2m条边。使用Tarjan算法进行缩点,co数组存储每个节点的染色,实际上就是有向无环图的逆拓扑序(因为在DFS树中越深的节点越先被染色)。对于每个变量 x i x_i xi,考察节点 i i i和节点 i + n i+n i+n的染色是否相同,若相同说明 x i x_i xi ¬ x i \neg x_i ¬xi在同一个强连通分量,出现矛盾。而后根据拓扑序进行赋值:若 i i i的拓扑序小于 i + n i+n i+n,对应co[i] > co[i + n] x i = 0 x_i=0 xi=0;反之 x i = 1 x_i=1 xi=1。可以简写为 x i = x_i= xi=co[i] < co[i + n]

洛谷P4782 【模板】2-SAT 问题

题目描述

n n n 个布尔变量 x 1 x_1 x1~ x n x_n xn,另有 m m m 个需要满足的条件,每个条件的形式都是 「 x i x_i xitrue / false x j x_j xjtrue / false」。比如 「 x 1 x_1 x1 为真或 x 3 x_3 x3 为假」、「 x 7 x_7 x7 为假或 x 2 x_2 x2 为假」。

2-SAT 问题的目标是给每个变量赋值使得所有条件得到满足。

输入格式

第一行两个整数 n n n m m m,意义如题面所述。

接下来 m m m 行每行 4 4 4 个整数 i i i, a a a, j j j, b b b,表示 「 x i x_i xi a a a x j x_j xj b b b」( a , b ∈ { 0 , 1 } a, b\in \{0,1\} a,b{0,1})

输出格式

如无解,输出 IMPOSSIBLE;否则输出 POSSIBLE

下一行 n n n 个整数 x 1 x_1 x1~ x n x_n xn x i ∈ { 0 , 1 } x_i\in\{0,1\} xi{0,1}),表示构造出的解。

样例 #1

样例输入 #1
3 1
1 1 3 0
样例输出 #1
POSSIBLE
0 0 0

提示

1 ≤ n , m ≤ 1 0 6 1\leq n, m\leq 10^6 1n,m106 , 前 3 3 3 个点卡小错误,后面 5 5 5 个点卡效率。

由于数据随机生成,可能会含有(10 0 10 0)之类的坑,但按照最常规写法的写的标程没有出错,各个数据点卡什么的提示在标程里

分析

我们需要把条件“ x i x_i xi a a a x j x_j xj b b b”转化为子句。其实 a = 0 a=0 a=0表示子句中 x i x_i xi前面有否定连接词, b = 0 b=0 b=0表示子句中 x j x_j xj前面有否定连接词。转换关系如下:

a a a b b b子句第一个蕴含关系第二个蕴含关系
1 1 1 1 1 1 x i ∨ x j x_i\lor x_j xixj ¬ x i → x j \neg x_i\to x_j ¬xixj ¬ x j → x i \neg x_j\to x_i ¬xjxi
1 1 1 0 0 0 x i ∨ ¬ x j x_i\lor\neg x_j xi¬xj ¬ x i → ¬ x j \neg x_i\to\neg x_j ¬xi¬xj x j → x i x_j\to x_i xjxi
0 0 0 1 1 1 ¬ x i ∨ x j \neg x_i\lor x_j ¬xixj x i → x j x_i\to x_j xixj ¬ x j → ¬ x i \neg x_j\to\neg x_i ¬xj¬xi
0 0 0 0 0 0 ¬ x i ∨ ¬ x j \neg x_i\lor\neg x_j ¬xi¬xj x i → ¬ x j x_i\to\neg x_j xi¬xj x j → ¬ x i x_j\to\neg x_i xj¬xi

因为 ¬ y \neg y ¬y在图中对应的节点编号为 y y y的编号加 n n n,所以加入第一个蕴含关系时我们判断 a a a是否为 1 1 1,若为 1 1 1则给 x i x_i xi的编号加 n n n变成 ¬ x i \neg x_i ¬xi;若 b b b 0 0 0则给 x j x_j xj的编号加 n n n变成 ¬ x j \neg x_j ¬xj。加入第二个蕴含关系时,若 b = 1 b=1 b=1 x j x_j xj n n n,若 a = 0 a=0 a=0 x i x_i xi n n n

代码实现

#include <iostream>
#include <cstdio>
#include <stack>

using namespace std;

const int MAXN = 2e6 + 5, MAXM = 2e6 + 5;

int n, m, tot, nxt[MAXM], first[MAXN], go[MAXM],
    dfn[MAXN], low[MAXN], co[MAXN], col; 
// n为变量个数,m为条件(子句)个数,nxt、first、go是链式前向星用到的数组,
// dfn为Tarjan算法的DFS序数组,low为Tarjan算法的low数组,
// co是颜色(即节点的强连通分量编号),col为强连通分量个数
stack<int> stk;

inline void add_edge(int u, int v) // 加边
{
    nxt[++tot] = first[u];
    first[u] = tot;
    go[tot] = v;
}

void Tarjan(int u) // Tarjan算法求强连通分量
{
    low[u] = dfn[u] = ++tot;
    stk.push(u);
    for(int e = first[u]; e; e = nxt[e])
    {
        int v = go[e];
        if(!dfn[v])
        {
            Tarjan(v);
            low[u] = min(low[u], low[v]);
        }
        else if(!co[v])
        {
            low[u] = min(low[u], low[v]);
        }
    }
    if(low[u] == dfn[u])
    {
        co[u] = ++col;
        while(stk.top() != u)
            co[stk.top()] = col, stk.pop();
        stk.pop();
    }
}

int main()
{
    cin >> n >> m;
    for(int t = 1; t <= m; ++t) // 连边
    {
        int i, a, j, b;
        cin >> i >> a >> j >> b;
        int u = i;
        if(a) u += n;
        int v = j;
        if(!b) v += n;
        add_edge(u, v);
        u = j;
        if(b) u += n;
        v = i;
        if(!a) v += n;
        add_edge(u, v);
    }
    for(int i = 1; i <= 2 * n; ++i)
        if(!dfn[i]) Tarjan(i); // Tarjan缩点
    for(int i = 1; i <= n; ++i)
        if(co[i] == co[i + n]) // 判断冲突
        {
            puts("IMPOSSIBLE");
            return 0;
        }
    puts("POSSIBLE");
    for(int i = 1; i <= n; ++i) // 赋值
        cout << (co[i] < co[i + n]) << ' ';
    cout << endl;
    return 0;
}
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐