题解:洛谷 P4782 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 问题的目标是给每个变量赋值使得所有条件得到满足。

【输入】

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

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

【输出】

如无解,输出 IMPOSSIBLE

否则输出 POSSIBLE,下一行 \(n\) 个整数 \(x_1\sim x_n\)\(x_i\in\{0,1\}\)),表示构造出的解。

【输入样例】

3 1
1 1 3 0

【输出样例】

POSSIBLE
0 0 0

【核心思想】

  1. 问题分析:有 \(n\) 个布尔变量 \(x_1 \sim x_n\),以及 \(m\) 个形如“\(x_i\)\(a\)\(x_j\)\(b\)”的约束(\(a,b \in \{0,1\}\))。需要判断是否存在一组赋值满足所有约束,若存在则输出任意一组解。这是典型的 2-SAT(2-可满足性问题),每个子句是两个文字(literal)的析取,目标是为所有变量赋布尔值使所有子句为真。

  2. 算法选择

    • 2-SAT + Tarjan 强连通分量:将每个变量 \(x_i\) 拆成两个节点:\(i\) 表示 \(x_i\) 为真,\(i+n\) 表示 \(x_i\) 为假(或反过来)。每个约束 \((x_i = a) \lor (x_j = b)\) 等价于两个蕴含关系:\(\neg (x_i = a) \Rightarrow (x_j = b)\)\(\neg (x_j = b) \Rightarrow (x_i = a)\)。在建图中添加对应的有向边。利用 Tarjan 算法求出所有强连通分量(SCC)。若存在某个变量 \(x_i\) 的真假两个节点在同一个 SCC 中,则无解;否则有解。构造解时,取每个变量两个节点中 SCC 编号较大的那个值(因为 Tarjan 的 SCC 编号顺序是逆拓扑序,编号较大的属于拓扑序靠后的分量,对应取真值)。
  3. 关键步骤

    • 读入与建图:读入 \(n, m\),对每个约束 \((i, a, j, b)\)
      • \(a=1\),则 \(x_i\) 为真对应节点 \(i\);若 \(a=0\),则 \(x_i\) 为假对应节点 \(i+n\)(反之亦可)。
      • 添加边 \((i + (1-a) \times n) \to (j + b \times n)\),表示 \(\neg (x_i = a) \Rightarrow (x_j = b)\)
      • 添加边 \((j + (1-b) \times n) \to (i + a \times n)\),表示 \(\neg (x_j = b) \Rightarrow (x_i = a)\)
    • Tarjan 求 SCC:对 \(2n\) 个节点运行 Tarjan 算法,得到每个节点的 SCC 编号 scc[id]
    • 矛盾检查:遍历每个变量 \(i\),若 scc[i] == scc[i+n](即真和假在同一 SCC 中),则无解,输出 IMPOSSIBLE
    • 构造解:若可行,输出 POSSIBLE,然后对于每个变量 \(i\),比较 scc[i]scc[i+n] 的大小,取编号较大的分量对应的值(通常取真)。因为 SCC 编号顺序与拓扑序相反,编号较大的分量为拓扑序更靠后的分量,该分量中的文字应取真值。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n + m)\),Tarjan 算法线性。
    • 空间复杂度:\(O(n + m)\),存储图和辅助数组。
  5. 2-SAT 的核心思想

    • 逻辑蕴含图:每个变量对应两个节点,每个子句转换为两条蕴含边,构成一个有向图。若一个文字能推出另一个文字,则存在路径。
    • 强连通分量与矛盾:若一个变量的真和假在同一个强连通分量中,则存在矛盾,无解。
    • 拓扑序赋值:Tarjan 的 SCC 编号顺序是反拓扑序,因此选择编号较大的分量中的文字作为真值,可以保证所有蕴含关系被满足。
    • 适用场景:适用于变量取布尔值、约束为析取式的可满足性问题,如排课、调度、电路设计等。

【算法标签】

提高+ #2-SAT

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 2000005, M = N * 2;
int n, m;  // n: 变量个数, m: 约束个数
int dfn[N], low[N], tim, stk[N], top, scc[N], cnt;  // Tarjan算法所需数组
int h[N], e[M], ne[M], idx;  // 邻接表

void add(int a, int b)  // 添加有向边
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

void tarjan(int x)  // Tarjan算法求强连通分量
{
    dfn[x] = low[x] = ++tim;  // 初始化时间戳
    stk[++top] = x;  // 入栈
    for (int i = h[x]; i != -1; i = ne[i])  // 遍历邻接点
    {
        int y = e[i];
        if (!dfn[y])  // 未访问
        {
            tarjan(y);
            low[x] = min(low[x], low[y]);  // 更新low
        }
        else if (!scc[y])  // 已访问但未确定SCC
        {
            low[x] = min(low[x], dfn[y]);
        }
    }
    if (low[x] == dfn[x])  // 是SCC的根
    {
        ++cnt;  // 新的SCC编号
        for (int y = -1; y != x;)
        {
            y = stk[top--];  // 出栈
            scc[y] = cnt;  // 标记SCC编号
        }
    }
}
int main()
{
    cin >> n >> m;  // 输入变量数和约束数
    memset(h, -1, sizeof(h));  // 初始化邻接表
    while (m--)  // 处理每个约束
    {
        int i, a, j, b;
        scanf("%d%d%d%d", &i, &a, &j, &b);
        // 2-SAT建图
        add(i + !a * n, j + b * n);  // ¬i ⇒ j
        add(j + !b * n, i + a * n);  // ¬j ⇒ i
    }
    for (int i = 1; i <= 2 * n; i++)  // 遍历所有点
    {
        if (!dfn[i])  // 未访问
        {
            tarjan(i);  // 求SCC
        }
    }
    for (int i = 1; i <= n; i++)  // 检查是否有矛盾
    {
        if (scc[i] == scc[i + n])  // i和¬i在同一个SCC
        {
            cout << "IMPOSSIBLE" << endl;
            return 0;
        }
    }
    cout << "POSSIBLE" << endl;
    for (int i = 1; i <= n; i++)  // 构造解
    {
        printf("%d ", (scc[i] > scc[i + n]));  // 拓扑序小的为真
    }
    cout << endl;
    return 0;
}

【运行结果】

3 1
1 1 3 0
POSSIBLE
0 0 0
posted @ 2026-09-01 23:09  团爸讲算法  阅读(17)  评论(0)    收藏  举报