1. \(\rm 2-SAT\) 问题简述

有 \(n\) 个变量,每个变量有只有 \(2\) 种取值,还有 \(m\) 个约束条件,每个条件都是对 \(k\) 个变量的约束。问这 \(n\)​​ 个变量有没有一种取值方法,能满足这 \(m\) 个条件,这个问题就是 \(\rm k-SAT\) 问题,其中 \(\text{SAT}\) 是 \(\text{satisfiability}\) 的缩写,意为“满足性”​。

当 \(k>2\) 时,\(\rm k-SAT\) 问题为 \(\rm NP\) 完全问题,只能用暴力;当 \(k=2\) 时,我们可以通过 强连通分量(我用了 \(\rm Tarjan\)) 实现 \(\operatorname{O}(n+m)\)​ 解决。

举个栗子:

现在举行了一场 \(\left\lceil 数据删除\right\rfloor\)​​ 的比赛,有 \(3\)​​​ 位候选人和 \(3\)​​​ 位评委,每位评委要满足条件之一:

  1. yzh 评委:
  • cxr 进入决赛;
  • wsy 进入决赛。
  1. xhj 评委:
  • wsy 进入决赛;
  • zlq 不进入决赛。
  1. sid 评委:
  • zlq 进入决赛;
  • cxr 进入决赛。

那么我们可以找到一组方案:cxr 不进入决赛,wsy 进入决赛,zlq 进入决赛(完了我又要被揍了啊 /fad)。

2. \(\rm 2-SAT\)​ 问题解决

P4782 【模板】2-SAT 问题

题意

有 \(n\) 个变量 \(x_1\sim x_n(x_i\in\{0,1\})\),另有 \(m\) 个需要满足的条件,每个条件给出 \(i,a,j,b\),表示 \(\lceil x_i\) 为 \(a\) 或 \(x_j\) 为 \(b\rfloor\)。给每个变量赋值使得所有条件得到满足,若无解,输出 ​​IMPOSSIBLE​​,否则输出 ​​POSSIBLE​​ 并构造一组解。

思路

先建立有 \(2n\) 个节点的有向图,第 \(i\) 号节点意味着 \(x_i=0\),第 \(i+n\) 号节点意味着 \(x_i=1\)。

对于一个约束条件:

  1. 若 \(a=0,b=0\)​​​​​​,则向 \(i+n\to j\)​​​​​​ 连边,\(j+n\to i\)​​​​​​ 连边,说明当 \(x_i=1\)​​​​​​ 时 \(x_j\)​​​​​​ 必须取 \(0\)​​​​​​,\(x_j=1\) 时 \(x_i\) 必须取 \(0\)​​​​​​​​;
  2. 若 \(a=0,b=1\)​​​​​​,则向 \(i+n\to j+n\)​​​​​​ 连边,\(j\to i\)​​​​​​ 连边,说明当 \(x_i=1\)​​​​​​ 时 \(x_j\)​​​​​​ 必须取 \(1\)​​​​​​,\(x_j=0\) 时 \(x_i\) 必须取 \(0\)​​​​​​​;
  3. 若 \(a=1,b=0\)​​​​​​​​,则向 \(i\to j\)​​​​​ 连边,\(j+n\to i+n\)​​​​​ 连边,说明当 \(x_i=0\)​​​​​ 时 \(x_j\)​​​​​ 必须取 \(0\)​​​​​,\(x_j=1\) 时 \(x_i\) 必须取 \(1\)​​​​​​​​​;
  4. 若 \(a=1,b=1\)​​​​​​​​​,则向 \(i\to j+n\)​​​​​​​​​ 连边,\(j\to i+n\)​​​​​​​​​ 连边,说明当 \(x_i=0\)​​​​​​​​​ 时 \(x_j\)​​​​​​​​​ 必须取 \(1\)​​​​​​​​​,\(x_j=0\)​​​​​​ 时 \(x_i\)​​​​​​ 必须取 \(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\) 所在的强连通的编号为 \(c_i\),遍历 \(i=1\to n\),然后判断:若 \(c_i=c_{i+n}\):说明若 \(x_i\) 取 \(0/1\),则对应的,\(x_i\) 必须取 \(1/0\)​​???炸了,所以我们推出了矛盾,即无解。

否则说明有解,那么我们要怎么构造解呢?

其实直接取所在强连通的编号更小的那个即可,原因如下:

在用 \(\rm Tarjan\) 求强连通时,由于是往下搜,所以实际上更晚访问的强连通会被先标记,即该强连通的编号更小。

对于一个节点 \(i\),假设它对应的是取 \(0\),则取 \(1\) 的是 \(i+n\),若有这样一条路


\[i\to j\to i+n\]


那么 \(i+n\) 所在的强连通编号更小。当我们取 \(i\)​ 时同时会取到 \(i+n\),就不行了,所以我们只能取 \(i+n\)​​ 所在的强连通,即编号更小的。

for (int i = 1; i <= n; i++)
{
printf("%d ", c[i] < c[i + n]);
}


\(\text{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;
}