「笔记」2-SAT
写在前面
草草草疫情期间我都在家水了些啥啊啊啊啊= =
蛮有意思的建模技巧,对图论有了新的认识。
引入
k-SAT(Satisfiability)问题的形式如下:
有 \(n\) 个 01 变量 \(x_1\sim x_n\),另有 \(m\) 个变量取值需要满足的限制。
每个限制是一个 \(k\) 元组 \((x_{p_1}, x_{p_2}, \cdots, x_{p_k})\),满足 \(x_{p_1}\oplus x_{p_2}\oplus\cdots\oplus x_{p_k} = a\)。其中 \(a\) 是 \(0/1\),\(\oplus\) 是某种二元 bool 运算。
要求构造一种满足所有限制的变量的赋值方案。
\(k > 2\) 的情况是 NP 完全的,只能暴力求解。而 2-SAT 问题可以通过建立图论模型,在 \(O(n+m)\) 的时间复杂度内判断是否有解,若有解可以构造出一组合法解。
分析
不同题目中的二元 bool 运算可能有差异,但建图的思路相同。以 P4782 【模板】2-SAT 问题 为例:
有 \(n\) 个布尔变量 \(x_1\sim x_n\),另有 \(m\) 个需要满足的条件,每个条件的形式都是 「\(x_i\) 为 true / false 或 \(x_j\) 为 true / false」。
要求判断是否有解,若有解可以构造出一组合法解。
\(1\le n,m\le 10^6\)。
1S,512MB。
首先将变量 \(x_i\) 拆成两个节点 \(i\) 与 \(i+n\),分别代表事件 \(x_i =1/0\)。显然两个事件是互斥的。
对于限制条件「\(a\) 或 \(b\)」(\(a\) 和 \(b\) 是两个 \(x_i = y\) 形式的命题),两个命题中至少有一个为真,于是可将其拆成两个充分条件形式的限制:\(\lnot a\to b\) 与 \(\lnot b\to a\)。
根据上述充分条件,建立图论模型,从节点 \(\lnot a\) 向节点 \(b\) 连边,从节点 \(\lnot b\) 向节点 \(a\) 连边。
之后考察节点 \(i\) 与 \(i + n\) 在图中的关系。若它们互相可达,即在同一个强连通分量中,说明在赋值限制下,它们代表的一对互斥事件会同时发生。则不存在一组合法的赋值方案。
否则说明有解,考虑如何构造一组合法解。
考虑先对建出的图进行缩点得到一张 DAG。对于变量 \(x_i\),考察节点 \(i\) 与 \(i+n\) 所在强连通分量的拓扑关系。若两分量不连通,则 \(x_i\) 可取任意一个值。否则只能取属于拓扑序较大的分量的值。因为若取拓扑序较小的值,可以根据逻辑关系推出取另一个值也是同时发生的。
可以使用 Tarjan/Kosaraju 算法求得强连通分量。值得注意的是 Tarjan 算法赋给强连通分量的编号顺序与拓扑序是相反的,输出时的判断条件要反着写。
代码
P4782 【模板】2-SAT 问题 的代码,使用 Tarjan 实现。
//知识点:2-SAT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <stack>
#define LL long long
const int kN = 2e6 + 10;
//=============================================================
int n, m, e_num, head[kN], v[kN << 1], ne[kN << 1];
int dfn_num, bel_num, dfn[kN], low[kN], bel[kN];
std::stack <int> st;
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
void Add(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Tarjan(int u_) {
dfn[u_] = low[u_] = ++ dfn_num;
st.push(u_);
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
if (!dfn[v_]) {
Tarjan(v_);
Chkmin(low[u_], low[v_]);
} else if (!bel[v_]) {
Chkmin(low[u_], dfn[v_]);
}
}
if (dfn[u_] == low[u_]) {
++ bel_num;
while (true) {
int top = st.top(); st.pop();
bel[top] = bel_num;
if (top == u_) break;
}
}
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= m; ++ i) {
int x = read(), a = read(), y = read(), b = read();
if (a && b) Add(x + n, y), Add(y + n, x);
if (!a && b) Add(x, y), Add(y + n, x + n);
if (a && !b) Add(x + n, y + n), Add(y, x);
if (!a && !b) Add(x, y + n), Add(y, x + n);
}
for (int i = 1; i <= 2 * n; ++ i) {
if (!dfn[i]) Tarjan(i);
if (i <= n && bel[i] == bel[i + n]) {
printf("IMPOSSIBLE\n");
return 0;
}
}
printf("POSSIBLE\n");
for (int i = 1; i <= n; ++ i) printf("%d ", bel[i] < bel[i + n]); //Tarjan 算法赋给强连通分量的编号顺序与拓扑序相反
return 0;
}
例题
板题:P4171 [JSOI2010]满汉全席 与 P5782 [POI2001]和平委员会。
写在最后
鸣谢:
《算法竞赛进阶指南》
题解 P4782 【【模板】2-SAT 问题】 - Great_Influence 的博客
2-SAT - OI Wiki