题解:洛谷 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
【核心思想】
-
问题分析:有 \(n\) 个布尔变量 \(x_1 \sim x_n\),以及 \(m\) 个形如“\(x_i\) 为 \(a\) 或 \(x_j\) 为 \(b\)”的约束(\(a,b \in \{0,1\}\))。需要判断是否存在一组赋值满足所有约束,若存在则输出任意一组解。这是典型的 2-SAT(2-可满足性问题),每个子句是两个文字(literal)的析取,目标是为所有变量赋布尔值使所有子句为真。
-
算法选择:
- 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 编号顺序是逆拓扑序,编号较大的属于拓扑序靠后的分量,对应取真值)。
-
关键步骤:
- 读入与建图:读入 \(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 编号顺序与拓扑序相反,编号较大的分量为拓扑序更靠后的分量,该分量中的文字应取真值。
- 读入与建图:读入 \(n, m\),对每个约束 \((i, a, j, b)\):
-
时间/空间复杂度:
- 时间复杂度:\(O(n + m)\),Tarjan 算法线性。
- 空间复杂度:\(O(n + m)\),存储图和辅助数组。
-
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
浙公网安备 33010602011771号