前置知识
a ∧ b a\land b a∧b:表示 a a a 命题和 b b b 命题是否同时满足,也就是计算机里面的与运算。
a ∨ b a\lor b a∨b:表示 a a a 命题和 b b b 命题中是否至少有一个满足,也就是计算机里面的或运算。
¬ a \lnot a ¬a:表示 a a a 命题的反命题,也就是按位取反运算。
什么是 2-SAT 问题
在这个问题之前,我们先来解决另一个问题:SAT 问题是什么。
SAT 是英文单词 Satisfiability(可满足性)的简写,而 k-SAT 问题就是指有 m m m 个式子,每个式子包含 k k k 个布尔变量,是否能构造出一组布尔变量使得这 m m m 个式子同时满足。说得通俗一点:就是说如果有 n n n 个命题 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,…,an,能否构造出一组解使得每个 a i a_i ai 取 0 0 0 和 1 1 1 中的一个,并且保证 ( a i 1 , 1 op 1 a i 1 , 2 op 1 … op 1 a i 1 , k ) ∧ ( a i 2 , 1 op 2 a i 2 , 2 op 2 … op 2 a i 2 , n ) ∧ ⋯ ∧ ( a i m , 1 op m a i m , 2 op m … op m a i m , k ) (a_{i_{1,1}}\operatorname{op}_1a_{i_{1,2}}\operatorname{op}_1\dots\operatorname{op}_1a_{i_{1,k}})\land(a_{i_{2,1}}\operatorname{op}_2a_{i_{2,2}}\operatorname{op}_2\dots\operatorname{op}_2a_{i_{2,n}})\land\dots\land(a_{i_{m,1}}\operatorname{op}_ma_{i_{m,2}}\operatorname{op}_m\dots\operatorname{op}_ma_{i_{m,k}}) (ai1,1op1ai1,2op1…op1ai1,k)∧(ai2,1op2ai2,2op2…op2ai2,n)∧⋯∧(aim,1opmaim,2opm…opmaim,k) 成立。其中 op i \operatorname{op}_i opi 可以是与、或、异或甚至更多操作中的任意一种。
不过我们已经严格证明了:当 k > 2 k>2 k>2 时,这个问题是 NP 完全的,也就是说只能使用暴力求解。因此我们通常研究 k = 2 k=2 k=2 时的情况,也就是 2-SAT 问题。
上面的了解一下就行,接下来我们正式进入正题。
2-SAT 问题指已知 n n n 个布尔变量 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,…,an 和 m m m 个式子,每个式子形如 a i op a j a_i\operatorname{op}a_j aiopaj,问你能否构造出一组解,使得这 m m m 个式子的最终都能成立。有时它还会让你输出一组解。
如何求解 2-SAT 问题
要想求解 2-SAT 问题,我们需要用到建图的思想。
以一道模版题为例:
P4782 【模板】2-SAT
题目描述
有 n n n 个布尔变量 x 1 ∼ x n x_1\sim x_n x1∼xn,另有 m m m 个需要满足的条件,每个条件的形式都是 「 x i x_i xi 为
true/false或 x j x_j xj 为true/false」。比如 「 x 1 x_1 x1 为真或 x 3 x_3 x3 为假」、「 x 7 x_7 x7 为假或 x 2 x_2 x2 为假」。2-SAT 问题的目标是给每个变量赋值使得所有条件得到满足。
输入格式
第一行两个整数 n n n 和 m m m,意义如题面所述。
接下来 m m m 行每行 4 4 4 个整数 i i i, a a a, j j j, b b b,表示 「 x i x_i xi 为 a a a 或 x j x_j xj 为 b b b」( a , b ∈ { 0 , 1 } a, b\in \{0,1\} a,b∈{0,1})
输出格式
如无解,输出
IMPOSSIBLE;否则输出
POSSIBLE,下一行 n n n 个整数 x 1 ∼ x n x_1\sim x_n x1∼xn( x i ∈ { 0 , 1 } x_i\in\{0,1\} xi∈{0,1}),表示构造出的解。输入输出样例 #1
输入 #1
3 1 1 1 3 0输出 #1
POSSIBLE 0 0 0说明/提示
1 ≤ n , m ≤ 10 6 1\leq n, m\leq 10^6 1≤n,m≤106 , 前 3 3 3 个点卡小错误,后面 5 5 5 个点卡效率。
由于数据随机生成,可能会含有 10 0 10 0 之类的坑,但按照最常规写法的写的标程没有出错,各个数据点卡什么的提示在标程里。
说白了,就是要做或运算。每一次输入其实就是要求 ( x i = a ) ∨ ( x j = b ) (x_i=a)\lor(x_j=b) (xi=a)∨(xj=b) 为真。
我们思考这样一件事:如果 x i ≠ a x_i\not=a xi=a,那么 x j = b x_j=b xj=b 就是一定的了。如果 x j ≠ b x_j\not=b xj=b,那么 x i = a x_i=a xi=a 就是一定的了。
换句话说就是: x i = ¬ a → x j = b x_i=\lnot a\to x_j=b xi=¬a→xj=b,同样的 x j = ¬ b → x i = a x_j=\lnot b\to x_i=a xj=¬b→xi=a。因此我们可以从 x i = ¬ a x_i=\lnot a xi=¬a 这个命题向 x j = b x_j=b xj=b 这个命题建边,从 x j = ¬ b x_j=\lnot b xj=¬b 这个命题向 x i = a x_i=a xi=a 这个命题建边。注意是命题之间建边,而不是值之间建边。
为了方便,我们把 x i = 1 x_i=1 xi=1 这个命题的编号编为 i i i, x i = 0 x_i=0 xi=0 这个命题的编号编为 i + n i+n i+n。
现在我们建好了边,就要考虑哪些命题是真的了。
我们首先思考这样一件事:建边的含义是什么?其实不难理解,就是说如果一个命题成立了,那么它指向的所有命题都必须成立,再往后一直递推下去。
那么我们就不难想到无解究竟是什么情况了:如果命题 x x x 与它的反命题 ¬ x \lnot x ¬x 能互相推导,也就是说命题 x x x 为真就能推出命题 ¬ x \lnot x ¬x 为真,而命题 ¬ x \lnot x ¬x 为真又能推出命题 x x x 为真,那么这个命题不论为真还是假就都不成立。那么这个问题也就无解了。
因此我们可以考虑使用缩点,如果说命题 x x x 与它的反命题 ¬ x \lnot x ¬x 在同一个 SCC 里面,那么就无解,否则就有解。
现在我们考虑怎么解开。
首先给出结论:在命题 x x x 和 ¬ x \lnot x ¬x 中,保证拓扑序最大的那个为真,那么就一定有合法解。现在我们来证明一下(证明是我自己证的,如果有错误的地方还请指正)。
证明:
考虑命题 x x x 与 ¬ x \lnot x ¬x。
如果说在保证合法的情况下,命题 x x x 为真能推出命题 ¬ x \lnot x ¬x 为真,那么当命题 x x x 为假时后面的就不需要推了(因为是或运算,只需要满足命题 x x x 为真且后面的所有命题为真或命题 x x x 为假就行),此时 ¬ x \lnot x ¬x 为真且 ¬ x \lnot x ¬x 比 x x x 的拓扑序大,且取 x x x 为假时成立,因此得证。
没看懂的可以看图:


如果在合法的情况下命题 ¬ x \lnot x ¬x 为真能推出命题 x x x 为真,则当命题 ¬ x \lnot x ¬x 为假时后面均不用考虑,此时命题 x x x 为真,且命题 x x x 比命题 ¬ x \lnot x ¬x 的拓扑序大,因此得证。这一块和上面那一块没有什么区别。
在考虑完所有能从原命题推出反命题或从反命题推出原命题的情况之后,剩下的均为原命题与反命题没有关系的命题。
首先我们需要知道一个引理:
引理:如果 x → y x\to y x→y 有边,那么 ¬ y → ¬ x \lnot y\to\lnot x ¬y→¬x 也有边。
这个引理很好证明,从建图的时候就能看出来: ¬ a → b \lnot a\to b ¬a→b 建边且 ¬ b → a \lnot b\to a ¬b→a 建边。所以我们就证明了这个引理。
现在我们假设一个命题 y y y 与它的反命题 ¬ y \lnot y ¬y 互相都不能推导,有一个命题 x x x 能推导到它的反命题 ¬ x \lnot x ¬x,但是不能反推回来,并且 y y y在 x x x 与 ¬ x \lnot x ¬x 之间,也就是长这样:

那么根据我们上面提出的引理,我们可以知道 ¬ y \lnot y ¬y 肯定也在 x x x 与 ¬ x \lnot x ¬x 之间:

这个的证明很容易,这里不细讲。
那么我们知道:命题 x x x 肯定是取的假,那么命题 y y y 是真是假就并不重要了,因此我们也可以选择让命题 y y y 为真,也就满足了我们的方法。得证。
如果 y y y 能推导到 x x x,那么根据引理 ¬ x \lnot x ¬x 就能推导到 ¬ y \lnot y ¬y,因为 x x x 能推导到 ¬ x \lnot x ¬x,所以 y y y 也能推导到 ¬ y \lnot y ¬y,与我们的定义不符。
如果 ¬ x \lnot x ¬x 能推导到 y y y,那么 ¬ y \lnot y ¬y 就能推导到 x x x,因为 x x x 能推导到 ¬ x \lnot x ¬x,所以 ¬ y \lnot y ¬y 就能推导到 y y y,与我们的定义不符。
因此最后一种情况就是:命题 y y y 和其他与它定义相同的命题连在了一起,根据引理,必然会有另外一个连通块与 y y y 所在的连通块完全相反,而两个连通块之间没有任何关系,有一种方法是让两个连通块的最靠后的那一部分都是 1 1 1 就行。那怎么做呢?很简单:根据我们的引理,如果 y y y 所在的连通块的第一个为假,那么 ¬ y \lnot y ¬y 所在的连通块的最后一个就为真,因此我们只需要从前到后把 y y y 所在的连通块的点全部赋值为 0 0 0。相应的,当过了整个的一半的时候,后面的命题全部赋值为 1 1 1 就行。而且这个规则依然在我们的结论里,所以得证。
看图:

其中通过 y y y 与 ¬ y \lnot y ¬y 的关系可以看出每个原命题与反命题之间的对应关系。
证毕。
至于拓扑序怎么求,这个就是别的问题了。
代码:
#include<bits/stdc++.h>
#define int long long
#define code using
#define by namespace
#define plh std
code by plh;
namespace fastio
{
inline int read()
{
int z=0,f=1;
char c=getchar();
if(c==EOF)
{
exit(0);
}
while(c<'0'||c>'9')
{
if(c==EOF)
{
exit(0);
}
if(c=='-')
{
f=-1;
}
c=getchar();
}
while(c>='0'&&c<='9')
{
z=z*10+c-'0';
c=getchar();
}
return z*f;
}
inline void read(string &s)
{
s="";
char c=getchar();
while(c!=' '&&c!='\n')
{
s+=c;
}
}
inline void write(int x)
{
if(x<0)
{
putchar('-');
x=-x;
}
static int top=0,stk[106];
while(x)
{
stk[++top]=x%10;
x/=10;
}
if(!top)
{
stk[++top]=0;
}
while(top)
{
putchar(char(stk[top--]+'0'));
}
}
inline void write(string s)
{
for(auto i:s)
{
putchar(i);
}
}
}
using namespace fastio;
int n,m,cnt,ti,dfn[2000006],low[2000006],bel[2000006];
bool ins[2000006];
vector<int>v[2000006];
stack<int>st;
void tarjan(int x)
{
dfn[x]=low[x]=++ti;
ins[x]=1;
st.push(x);
for(auto i:v[x])
{
if(!dfn[i])
{
tarjan(i);
low[x]=min(low[x],low[i]);
}
else if(ins[i])
{
low[x]=min(low[x],dfn[i]);
}
}
if(dfn[x]==low[x])
{
cnt++;
while(1)
{
int u=st.top();
st.pop();
bel[u]=cnt;
ins[u]=0;
if(u==x)
{
break;
}
}
}
}
signed main()
{
n=read(),m=read();
for(int i=1,x,xx,y,yy;i<=m;i++)
{
x=read(),xx=read(),y=read(),yy=read();
if(xx&&yy)
{
v[x+n].push_back(y);
v[y+n].push_back(x);
}
else if(!xx&&yy)
{
v[x].push_back(y);
v[y+n].push_back(x+n);
}
else if(xx&&!yy)
{
v[x+n].push_back(y+n);
v[y].push_back(x);
}
else
{
v[x].push_back(y+n);
v[y].push_back(x+n);
}
}
for(int i=1;i<=2*n;i++)
{
if(!dfn[i])
{
tarjan(i);
}
}
for(int i=1;i<=n;i++)
{
if(bel[i]==bel[i+n])
{
write("IMPOSSIBLE");
return 0;
}
}
puts("POSSIBLE");
for(int i=1;i<=n;i++)
{
write(bel[i]<bel[i+n]);//缩点完后的顺序就是拓扑序的倒序
putchar(' ');
}
return 0;
}
习题
提供一道经典习题:https://www.luogu.com.cn/problem/P10969。
后话
其实 2-SAT 的重点部分就在于拓扑序那里,很多文章都没有证明那一步,不过我为了完整性,自己证明了一下,如果有任何错误的地方,麻烦私信指出。


3541

被折叠的 条评论
为什么被折叠?



