「学习笔记」2-SAT
一.什么是 \(\text{2-SAT}\)
\(SAT\) 是适定性 \(Satisfiability\) 问题的简称。一般形式为 \(k -\)适定性问题,简称 \(\text{k-SAT}\)。而当 \(k>2\) 时,该问题为 \(NP\) 完全的。所以我们只研究 \(k=2\) 的情况。
形象地来说,给定 \(n\) 个布尔变量 \(a_i\),同时给出若干个约束条件:
\((\text{not})a_i\) \(op\) \((\text{not})a_j = \text{true/false}\)。
而求解 \(\text{2-SAT}\) 就是求出一组 \(a_i\) 的解。
二.将 \(\text{2-SAT}\) 问题转换为图论问题
首先定义一条有向边的定义 \(x\to y\):如果满足 \(x\) 就必须满足 \(y\)。
发现一个点有两个状态真或假,可以想到将点 \(u\) 拆为点 \(u_0,u_1\),分别表示假或真。
\(1.\) \(a\land b = \text{true}\) \(\to (a1, b1),(b1, a1)\)。
\(2.\) \(a\land b = \text{false}\) \(\to (a1, b0),(b1, a0)\)。
\(3.\) \(a\lor b = \text{true}\) \(\to (a0, b1),(b0, a1)\)。
\(4.\) \(a\lor b = \text{false}\) \(\to (a0, b0),(b0, a0)\)。
这样我们就将 \(\text{2-SAT}\) 问题转换为图论问题了,可以运用图论的方式解决。
三.解决 \(\text{2-SAT}\) 问题
-
\(1.\) 判断有无解
由上文定义的状态可以得知,\(u_0\) 和 \(u_1\) 不能同时被满足。
那么存在点 \(x_0\) 和 \(x_1\) 在同一个强连通分量中时,那么无解。
反之则有解。 -
\(2.\) 求方案
由我们的建边可得:对于每个点 \(u\),选择其拓扑序较大的那种状态更优。
为什么呢?
如下图所示:
对于 \(x1\) 的两种状态,如果选择 \(\text{false}\),那么推导出 \(x2\) 为 \(\text{true}\),又推导出 \(x1\) 为 \(\text{true}\),又推导出 \(x2\) 为 \(\text{false}\),发现这种解法矛盾了。
但如果对于 \(x1\) 选择 \(\text{true}\),那么 \(x2\) 为 \(\text{false}\),则满足要求。
由此我们得出:对于每个点 \(u\),选择其拓扑序较大的那种状态更优。
这里不需要再做一遍拓扑排序求拓扑序,在 \(\text{Tarjan}\) 求强连通分量中就求出了每个点的逆拓扑序,每个点的强连通分量编号也就是逆拓扑序。
四.例题讲解
P4782 【模板】2-SAT 问题
\(\text{2-SAT}\) 模板题。
按照上文建边,求解即可。
代码如下:
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
#include <stack>
using namespace std;
const int N = 2000005;
struct EDGE {
int nxt;
int to;
}e[N];
int cnt = 1, sjc = 0, dfn[N], low[N], col[N], co = 0, head[N], edge_num = 0;
int sta[N], top = 0;
stack<int> s;
bool insta[N];
inline void add_edge (int x, int y) {
e[++edge_num].nxt = head[x];
e[edge_num].to = y;
head[x] = edge_num;
}
inline void tarjan (int u) {
sta[++top] = u;
low[u] = dfn[u] = ++sjc;
insta[u] = true;
for (int i = head[u] ;~i ; i = e[i].nxt) {
int v = e[i].to;
if (!dfn[v]) {
tarjan (v);
low[u] = min (low[u], low[v]);
}
else if (insta[v]) {
low[u] = min (low[u], dfn[v]);
}
}
if (low[u] == dfn[u]) {
cnt ++;
int tp;
do {
tp = sta[top--];
col[tp] = cnt;
insta[tp] = false;
}while (tp != u);
}
}//Tarjan求强连通分量。
int main() {
memset (head, -1, sizeof (head));
int n, m;
scanf ("%d%d", &n, &m);
while (m --) {
int i, a, j, b;
scanf ("%d%d%d%d", &i, &a, &j, &b);
if (a == 0 && b == 0) {//a=0或b=0
add_edge (i + n, j);//i真j假
add_edge (j + n, i);//j真i假
}
if (a == 0 && b == 1) {//a=0或b=1
add_edge (i + n, j + n);//i真j假
add_edge (j, i);//j假i真
}
if (a == 1 && b == 0) {//a=1或b=0
add_edge (i, j);//i假j假
add_edge (j + n, i + n);//j真i真
}
if (a == 1 && b == 1) {//a=1或b=1
add_edge (i, j + n);//i假j真
add_edge (j, i + n);//j假i真
}
}
for (int i = 1; i <= (n << 1); i ++) {
if (!dfn[i]) {
tarjan (i);
}
}
for (int i = 1; i <= n; i ++) {
if (col[i] == col[i + n]) {//无解状态。
puts ("IMPOSSIBLE");
return 0;
}
}
puts ("POSSIBLE");
for (int i = 1; i <= n; i ++) {
printf ("%d ", col[i] > col[i + n] ? 1 : 0);
//选择拓扑序大的。
}
return 0;
}
P4171 [JSOI2010] 满汉全席
设汉式的点编号为 \(1到n\),满式的点编号为 \(n+1到2n\)。
那么就按照模板建边解决 \(\text{2-SAT}\) 问题即可。
UVA11294 Wedding
题意:有 \(n\) 对夫妻分别坐在新郎与新娘两侧,有两个要求:
\(1.\) 一对夫妻不能坐在同一侧;
\(2.\) 给出 \(m\) 个特殊关系,有特殊关系的两个人不能同时坐在新郎一侧,也就是说可以分别坐两侧,或者同时坐在新娘一侧。
我们设女方的 \(0\) 状态为 \(1到n\),男方的 \(0\) 状态为 \(n+1到2n\),女方的 \(1\) 状态为 \(2n+1到3n\),男方的 \(1\) 状态为 \(3n+1到4n\)。
同时设 \(0\) 状态在左边,也就是新娘一侧。\(1\) 状态在右边,也就是新郎一侧。
这样就可以建边了解决 \(\text{2-SAT}\) 问题了。
建边代码如下:
Add(1 + 2 * n, 1);//强制新娘在左边
Add(1 + n, 1 + n + 2 * n);//强制新郎在右边
for (int i = 2; i <= n; ++i) {
int x = i, y = i + n; //x女方0状态,y男方0状态
Add(x, y + 2 * n);
Add(y + 2 * n, x);
Add(x + 2 * n, y);
Add(y, x + 2 * n);
//双方0状态与1状态互相连边
}
while (m --) {
int x, y;
char a, b;
scanf("%d%c%d%c", &x, &a, &y, &b);
x ++, y ++;//调整编号为1-n
if (a == 'h') {
x += n;
}
if (b == 'h') {
y += n;
}
//男方要加一个n
Add(x + 2 * n, y);
Add(y + 2 * n, x);
//双方不在同一侧
}
P3007 [USACO11JAN] The Continental Cowngress G
也是模板。
拆点后建边跑 \(\text{Tarjan}\),然后用 \(\text{dfs}\) 判断点的可行情况即可。