2-SAT

一、SAT问题以及2-SAT问题的定义

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

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

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

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

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

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

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

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

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

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

布尔表达式的可满足性:对于一个布尔表达式,如果存在各变量的一组赋值,使得该表达式的值为1,则称该表达式是可满足的。对于合取范式,因为各子句之间是且的关系,所以要使它取值为1就必须使各子句取值都为1。

对于上面给出的式子\((a \lor \neg b) \land (b \lor c) \land (c \lor \neg f) \lor (\neg d \lor e)\),令\(a = 1, b = 1, c = 1, d = 0, e = 0, f = 1\)可以使其取值为1,所以它是可满足的;但对于式子\((a \lor \neg b) \land (b \lor c) \land (b \lor \neg c) \land (\neg a \lor \neg b)\),不论\(a,b,c\)取何值,式子的值都是0,因此它是不可满足的。

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

二、实际问题

P4782 【模板】2-SAT

题目描述

\(n\) 个布尔变量 \(x_1\sim x_n\),另有 \(m\) 个需要满足的条件,每个条件的形式都是 「\(x_i\)true / false\(x_j\)true / false」。比如 「\(x_1\) 为真或 \(x_3\) 为假」、「\(x_7\) 为假或 \(x_2\) 为假」。

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

如何求解?

1. 转化为蕴含关系

考虑2-CNF的一个子句(a \(\lor\) \(\neg\)b),要让这个子句满足,就是让(a \(\lor\) \(\neg\)b) \(=\) 1。现在令a \(=\) 0,那么\(\neg\)b一定等于1,因为如果\(\neg\)b也等于0,那么a和\(\neg\)b同时为0,该子句的取值就是0了;同理,若\(\neg\)b \(=\) 0,则可以知道a一定是1。

换言之:\(\neg\)a(a \(=\) 0)可以推出\(\neg\)b,而b(\(\neg\)b \(=\) 0,即\(\neg\)(\(\neg\)b) \(=\) b \(=\) 1)可以推出a。所以,(a \(\lor\) \(\neg\)b)就等价于\((\neg\)a \(\rightarrow\) \(\neg\)b) \(\land\) (b \(\rightarrow\) a)。

一般地,设p,q是两个文字,则子句(p \(\lor\) q)等价于两个蕴含关系\(\neg\)p \(\rightarrow\) q和\(\neg\)q \(\rightarrow\) p。若p \(=\) a,则\(\neg\)p \(=\) \(\neg\)a;若p \(=\) \(\neg\)a,则\(\neg\)p \(=\) \(\neg\)(\(\neg\)a) \(=\) a。

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

子句 蕴含关系
a \(\lor\) b \(\neg\)a \(\rightarrow\) b和\(\neg\)b \(\rightarrow\) a
a \(\lor\) \(\neg\)b \(\neg\)a \(\rightarrow\) \(\neg\)b和b \(\rightarrow\) a
\(\neg\)a \(\lor\) b a \(\rightarrow\) b和\(\neg\)b \(\rightarrow\) \(\neg\)a
\(\neg\)a \(\lor\) \(\neg\)b a \(\rightarrow\) \(\neg\)b和b \(\rightarrow\) \(\neg\)a

2. 建图

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

3. 判断可满足性

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

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

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

4.具体步骤

把每个变量看成点,把 \(n\) 个点拆成 \(2n\) 个点,即把 \(x_i\) 拆成 \(i\)\(i + n\)\(i\) 对应 \(x_i = 0\)\(i + n\) 对应 \(x_i = 1\)

建图就是连有向边,例如,\(x_i=a \lor x_j=b\),其中 \(a = 0/1\)\(b = 0/1\)

蕴含关系 \(\neg(x_i=a) \rightarrow x_j=b\) 可以表示为 从 \(i + !a * n\)\(j + b * n\) 的有向边。

蕴含关系 \(\neg(x_j=b) \rightarrow x_i=a\) 可以表示为 从 \(j + !b * n\)\(i + a * n\) 的有向边。

例如,\(1\ 0\ 2\ 1\) 建图:\(1 + n \rightarrow 2 + n\)\(2 \rightarrow 1\)

先跑 tarjan 缩点,缩点后判断:

(1) 如果 \(i\)\(i + n\) 在同一 SCC 里(即在一个环内),那么显然无解。因为 \(x_i\) 不能既取 0 又取 1。

(2) 否则,\(i\)\(i + n\) 可能在一条链上或无关,那么一定存在可行解。(举例归纳)

● 构造可行解:

如果有 \(i \rightarrow \dots \rightarrow i + n\),那么就选择 \(i + n\),即令 \(x_i = 1\)
如果有 \(i + n \rightarrow \dots \rightarrow i\),那么就选择 \(i\),即令 \(x_i = 0\)

蕴含关系具有传递性,选择后者,冲突性更小。

Tarjan 缩点后,得到的是拓扑逆序,
因为先访问的节点后出栈,后出栈的节点,其 scc 的编号就大。

如果有 \(\text{scc}[i] > \text{scc}[i + n]\),则令 \(x_i = 1\)
如果有 \(\text{scc}[i] < \text{scc}[i + n]\),则令 \(x_i = 0\)

代码实现:

点击查看代码
struct TwoSat {
    int n;
    std::vector<std::vector<int>> e;
    std::vector<bool> ans;
    TwoSat(int n) : n(n), e(2 * n + 1), ans(n + 1) {}
    // 加析取条件 (u,v),f,g代表x,y是否为非
    void addClause(int u, bool f, int v, bool g) {
        e[u + !f * n].push_back(v + g * n);
        e[v + !g * n].push_back(u + f * n);
    }
    // 主体,找是否有合法解
    bool satisfiable() {
        std::vector<int> scc(2 * n + 1), dfn(2 * n + 1), low(2 * n + 1);
        std::vector<int> stk, instk(2 * n + 1), siz(2 * n + 1);
        int idx = 0, cnt = 0;
        std::function<void(int)> tarjan = [&](int u) {
            stk.push_back(u);
            instk[u] = 1;
            dfn[u] = low[u] = ++idx;
            for (auto v : e[u]) {
                if (!dfn[v]) {
                    tarjan(v);
                    low[u] = std::min(low[u], low[v]);
                } else if (instk[v]) {
                    low[u] = std::min(low[u], dfn[v]);
                }
            }
            if (dfn[u] == low[u]) {
                int y;cnt++;
                do {
                    y = stk.back();stk.pop_back();
                    instk[y] = 0;
                    scc[y] = cnt;
                    siz[cnt] ++;
                } while (y != u);
            }
        };
        for (int i = 1; i <= 2 * n; ++i) {
            if(!dfn[i]) {
                tarjan(i);
            }
        }
        for (int i = 1; i <= n; i ++) {
            if (scc[i] == scc[i + n]) return false;
            ans[i] = scc[i] > scc[i + n];
        }
        return true;
    }
    //返回一种 (x1,...,xn) 的合法解
    std::vector<bool> answer() { return ans; }
};
void solve() {
    int n, m;cin >> n >> m;
    TwoSat ts(n);
    for(int i = 1; i <= m; i ++) {
        int a, f, b, g;cin >> a >> f >> b >> g;
        ts.addClause(a, f, b, g);
    }
    if(ts.satisfiable()) {
        auto ans = ts.answer();
        cout << "POSSIBLE\n";
        for(int i = 1; i <= n; i ++) {
            cout << ans[i] << " \n"[i == n];
        }
    } else {
        cout << "IMPOSSIBLE\n";
    }
}
posted @ 2025-12-23 21:55  orzkeyhacker  阅读(14)  评论(0)    收藏  举报