1. 2 − S A T \rm 2-SAT 2−SAT 问题简述
有 n n n 个变量,每个变量有只有 2 2 2 种取值,还有 m m m 个约束条件,每个条件都是对 k k k 个变量的约束。问这 n n n 个变量有没有一种取值方法,能满足这 m m m 个条件,这个问题就是 k − S A T \rm k-SAT k−SAT 问题,其中 SAT \text{SAT} SAT 是 satisfiability \text{satisfiability} satisfiability 的缩写,意为“满足性”。
当 k > 2 k>2 k>2 时, k − S A T \rm k-SAT k−SAT 问题为 N P \rm NP NP 完全问题,只能用暴力;当 k = 2 k=2 k=2 时,我们可以通过 强连通分量(我用了 T a r j a n \rm Tarjan Tarjan) 实现 O ( n + m ) \operatorname{O}(n+m) O(n+m) 解决。
举个栗子:
现在举行了一场 ⌈ 数 据 删 除 ⌋ \left\lceil 数据删除\right\rfloor ⌈数据删除⌋ 的比赛,有 3 3 3 位候选人和 3 3 3 位评委,每位评委要满足条件之一:
- yzh 评委:
- cxr 进入决赛;
- wsy 进入决赛。
- xhj 评委:
- wsy 进入决赛;
- zlq 不进入决赛。
- sid 评委:
- zlq 进入决赛;
- cxr 进入决赛。
那么我们可以找到一组方案:cxr 不进入决赛,wsy 进入决赛,zlq 进入决赛(完了我又要被揍了啊 /fad)。
2. 2 − S A T \rm 2-SAT 2−SAT 问题解决
题意
有
n
n
n 个变量
x
1
∼
x
n
(
x
i
∈
{
0
,
1
}
)
x_1\sim x_n(x_i\in\{0,1\})
x1∼xn(xi∈{0,1}),另有
m
m
m 个需要满足的条件,每个条件给出
i
,
a
,
j
,
b
i,a,j,b
i,a,j,b,表示
⌈
x
i
\lceil x_i
⌈xi 为
a
a
a 或
x
j
x_j
xj 为
b
⌋
b\rfloor
b⌋。给每个变量赋值使得所有条件得到满足,若无解,输出 IMPOSSIBLE
,否则输出 POSSIBLE
并构造一组解。
思路
先建立有 2 n 2n 2n 个节点的有向图,第 i i i 号节点意味着 x i = 0 x_i=0 xi=0,第 i + n i+n i+n 号节点意味着 x i = 1 x_i=1 xi=1。
对于一个约束条件:
- 若 a = 0 , b = 0 a=0,b=0 a=0,b=0,则向 i + n → j i+n\to j i+n→j 连边, j + n → i j+n\to i j+n→i 连边,说明当 x i = 1 x_i=1 xi=1 时 x j x_j xj 必须取 0 0 0, x j = 1 x_j=1 xj=1 时 x i x_i xi 必须取 0 0 0;
- 若 a = 0 , b = 1 a=0,b=1 a=0,b=1,则向 i + n → j + n i+n\to j+n i+n→j+n 连边, j → i j\to i j→i 连边,说明当 x i = 1 x_i=1 xi=1 时 x j x_j xj 必须取 1 1 1, x j = 0 x_j=0 xj=0 时 x i x_i xi 必须取 0 0 0;
- 若 a = 1 , b = 0 a=1,b=0 a=1,b=0,则向 i → j i\to j i→j 连边, j + n → i + n j+n\to i+n j+n→i+n 连边,说明当 x i = 0 x_i=0 xi=0 时 x j x_j xj 必须取 0 0 0, x j = 1 x_j=1 xj=1 时 x i x_i xi 必须取 1 1 1;
- 若 a = 1 , b = 1 a=1,b=1 a=1,b=1,则向 i → j + n i\to j+n i→j+n 连边, j → i + n j\to i+n j→i+n 连边,说明当 x i = 0 x_i=0 xi=0 时 x j x_j xj 必须取 1 1 1, x j = 0 x_j=0 xj=0 时 x i x_i xi 必须取 1 1 1。
建图代码:
while (m--)
{
int i, a, j, b;
scanf("%d%d%d%d", &i, &a, &j, &b);
if (a == 0)
{
if (b == 0)
{
add(i + n, j);
add(j + n, i);
}
else
{
add(i + n, j + n);
add(j, i);
}
}
else
{
if (b == 0)
{
add(i, j);
add(j + n, i + n);
}
else
{
add(i, j + n);
add(j, i + n);
}
}
}
当然,我们可以简化一下:
while (m--)
{
int i, a, j, b;
scanf("%d%d%d%d", &i, &a, &j, &b);
add(i + a * n, j + (1 - b) * n);
add(j + b * n, i + (1 - a) * n);
}
建图后,我们求一遍强连通,设点 i i i 所在的强连通的编号为 c i c_i ci,遍历 i = 1 → n i=1\to n i=1→n,然后判断:若 c i = c i + n c_i=c_{i+n} ci=ci+n:说明若 x i x_i xi 取 0 / 1 0/1 0/1,则对应的, x i x_i xi 必须取 1 / 0 1/0 1/0???炸了,所以我们推出了矛盾,即无解。
否则说明有解,那么我们要怎么构造解呢?
其实直接取所在强连通的编号更小的那个即可,原因如下:
在用 T a r j a n \rm Tarjan Tarjan 求强连通时,由于是往下搜,所以实际上更晚访问的强连通会被先标记,即该强连通的编号更小。
对于一个节点 i i i,假设它对应的是取 0 0 0,则取 1 1 1 的是 i + n i+n i+n,若有这样一条路
i → j → i + n i\to j\to i+n i→j→i+n
那么 i + n i+n i+n 所在的强连通编号更小。当我们取 i i i 时同时会取到 i + n i+n i+n,就不行了,所以我们只能取 i + n i+n i+n 所在的强连通,即编号更小的。
for (int i = 1; i <= n; i++)
{
printf("%d ", c[i] < c[i + n]);
}
Code \text{Code} Code
#include <iostream>
#include <cstdio>
#include <stack>
using namespace std;
const int MAXN = 2e6 + 5;
int cnt, Time, scc;
int head[MAXN], dfn[MAXN], low[MAXN], c[MAXN];
bool ins[MAXN];
stack<int> s;
struct edge
{
int to, nxt;
}e[MAXN << 1];
void add(int u, int v)
{
e[++cnt] = edge{v, head[u]};
head[u] = cnt;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++Time;
s.push(u);
ins[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 (ins[v])
{
low[u] = min(low[u], dfn[v]);
}
}
if (dfn[u] == low[u])
{
scc++;
int v = 0;
while (v != u)
{
v = s.top();
s.pop();
c[v] = scc;
ins[v] = false;
}
}
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++)
{
int u, a, v, b;
scanf("%d%d%d%d", &u, &a, &v, &b);
add(u + a * n, v + (1 - b) * n);
add(v + b * n, u + (1 - a) * n);
}
for (int i = 1; i <= (n << 1); i++)
{
if (!dfn[i])
{
tarjan(i);
}
}
for (int i = 1; i <= n; i++)
{
if (c[i] == c[i + n])
{
puts("IMPOSSIBLE");
return 0;
}
}
puts("POSSIBLE");
for (int i = 1; i <= n; i++)
{
printf("%d ", c[i] < c[i + n]);
}
return 0;
}