• 【算法/图论】2-SAT问题详解


    一、问题引入

    在了解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是否可满足的问题。(判断一般的合取范式是否可满足的问题是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

    2. 建图

    我们把每个子句表示成蕴含关系后,就可以根据蕴含关系建图。假设有 n n n个布尔变量,那么图有 2 n 2n 2n个节点,每个变量及其否定各有一个节点。根据蕴含关系连边,若有 ¬ 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相等。换言之,如果某个变量和其否定出现在了同一个强连通分量中,那么这个式子就是不可满足的。如果没有这种情况,就是可满足的。

    4. 赋值

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

    1. 若有从 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_xtx<t¬x

    2. 若有从 ¬ x \neg x ¬x x x x的路径,即 ¬ x → x \neg x\to x ¬xx,那么只有当 x = 1 x=1 x=1的时候才能满足,此时 t x > t ¬ x t_x>t_{\neg x} tx>t¬x

    3. 若从 x x x ¬ x \neg x ¬x没有路径,我们需要使各个变量的取值不会违反蕴含关系。设有蕴含关系 x → y x\to y xy,则必有 ¬ y → ¬ x \neg y\to\neg x ¬y¬x

      • 若先访问 x x x、后访问 ¬ y \neg y ¬y,则 t x < t y < t ¬ y < t ¬ x t_xtx<ty<t¬y<t¬x

      • 若先访问 ¬ y \neg y ¬y,后访问 x x x,则 t ¬ y < t ¬ x < t x < t y t_{\neg y}t¬y<t¬x<tx<ty

      因此,如果我们仍然令 x = { 0 ,   t x < t ¬ x 1 , t x > t ¬ x x=

      {0, tx<t¬x1,tx>t¬x" role="presentation" style="position: relative;">{0, tx<t¬x1,tx>t¬x
      x={0, 1,tx<t¬xtx>t¬x y y y同理,那么 x x x y y y取值相同,均不违反 x → y x\to y xy

    所以,对于变量 x x x,我们对它的赋值是 x = { 0 ,   t x < t ¬ x 1 , t x > t ¬ x x=

    {0, tx<t¬x1,tx>t¬x" role="presentation" style="position: relative;">{0, tx<t¬x1,tx>t¬x
    x={0, 1,tx<t¬xtx>t¬x

    现在我们有一个疑问:既然可以这样决定变量的值,还怎么保证同一个强连通分量中的变量值相等呢?其实我们不用担心,因为应用这种赋值策略,同一个强连通分量中的变量的值一定是相同的。我们有以下结论:

    命题 x x x y y y属于同一个强连通分量,则 ¬ x \neg x ¬x ¬ y \neg y ¬y属于同一个强连通分量。

    要证明这个结论,需要用到

    引理 若存在 x x x y y y的路径,则存在 ¬ y \neg y ¬y ¬ x \neg x ¬x的路径。

    证明:设 x x x y y y的路径为 x → p 1 → p 2 → ⋯ → p k → y x\to p_1\to p_2\to\cdots\to p_k\to y xp1p2pky。我们上面提到,若有 a → b a\to b ab,则有 ¬ b → ¬ a \neg b\to\neg a ¬b¬a;所以下列边存在: ¬ y → ¬ p k \neg y\to\neg p_k ¬y¬pk ¬ p k → ¬ p k − 1 \neg p_k\to\neg p_{k-1} ¬pk¬pk1,…, ¬ p 1 → ¬ x \neg p_1\to\neg x ¬p1¬x。所以 ¬ y → ¬ p k → ¬ p k − 1 → ⋯ → ¬ p 1 → ¬ x \neg y\to\neg p_k\to\neg p_{k-1}\to\cdots\to\neg p_1\to\neg x ¬y¬pk¬pk1¬p1¬x就是从 ¬ y \neg y ¬y ¬ x \neg x ¬x的一条路径。

    下面证明命题:

    证明:由于 x x x y y y属于同一个强连通分量,那么有 x x x y y y的路径,也有 y y y x x x的路径。因此根据引理有 ¬ y \neg y ¬y ¬ x \neg x ¬x的路径,也有 ¬ x \neg x ¬x ¬ y \neg y ¬y的路径。于是 ¬ x \neg x ¬x ¬ y \neg y ¬y属于同一个强连通分量。

    于是,对于同一个强连通分量中的两个变量 x x x y y y,,这样得出 x x x y y y的赋值一定是相等的。

    三、两个例子

    第一个

    给定的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¬a,¬b<tc<t¬c<ta,b。因为 t ¬ a < t a t_{\neg a}t¬a<ta,所以 a = 1 a=1 a=1。同理 b = 1 b=1 b=1。因为 t c < t ¬ c t_ctc<t¬c,所以 c = 0 c=0 c=0

    a = b = 1 , c = 0 a=b=1,c=0 a=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 ∼ \sim 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 n x_1\sim x_n x1xn x i ∈ { 0 , 1 } x_i\in\{0,1\} xi{0,1}),表示构造出的解。

    样例 #1

    样例输入 #1

    3 1
    1 1 3 0
    
    • 1
    • 2

    样例输出 #1

    POSSIBLE
    0 0 0
    
    • 1
    • 2

    提示

    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)之类的坑,但按照最常规写法的写的标程没有出错,各个数据点卡什么的提示在标程里

    代码实现

    #include 
    #include 
    #include 
    
    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;
    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)
    {
        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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
  • 相关阅读:
    JavaScript-2-菜鸟教程
    【Java】学习日记 Day20
    Redis下载安装配置(linux版本)
    android NDK交叉编译ffmpeg方法
    万字爽文一篇带你掌握Java8新特性
    Blob 对象 下载文件时 type 类型 大全,及下载方法封装
    pytorch:debug dataLoader for循环的时候,很卡顿,进不去transform里面断点
    排序算法:选择排序
    会议管理系统SSM记录(二)
    微信小程序weui-form表单的使用
  • 原文地址:https://blog.csdn.net/qaqwqaqwq/article/details/126124806