SAT问题总述
SAT是适定性(Satisfiability)问题的简称 。一般形式称为k-适定性问题,简称 k-SAT。 形式化地描述如下:
设
A={a1,a2,⋯,an}
A
=
{
a
1
,
a
2
,
⋯
,
a
n
}
为一个有限个布尔变量所构成的集合,
A^={a1,a2,⋯,an,¬a1,¬a2,⋯,¬an}
A
^
=
{
a
1
,
a
2
,
⋯
,
a
n
,
¬
a
1
,
¬
a
2
,
⋯
,
¬
a
n
}
。取
S
S
为 的子集,定义
∨S=s1∨s2∨⋯∨sk (si∈S)
∨
S
=
s
1
∨
s
2
∨
⋯
∨
s
k
(
s
i
∈
S
)
。求一个
A
A
使得给定一组 满足
(∨S1)∨(∨S2)∨⋯∨(∨Sm)=1
(
∨
S
1
)
∨
(
∨
S
2
)
∨
⋯
∨
(
∨
S
m
)
=
1
。
特别地,若
max{|Si| ∣ i∈[1,m]}≤k
max
{
|
S
i
|
∣
i
∈
[
1
,
m
]
}
≤
k
,则称这个问题为
k-sat
k-sat
问题。
已经有前人证明了当
k≥3
k
≥
3
时,这是一个
NP-complete
NP-complete
问题,因此我们此处只考虑
k=2
k
=
2
时的 k-适应性问题,也称之为 2-SAT (Two-SAT)问题。
2-SAT问题
在2-SAT问题中,变量的限制性只有两种情况:
1
1
. 单个的布尔变量 ,它没有任何限制。
2
2
. 两个变量的相互限制,例如 。
由于单个变量随意取值均符合条件,因此我们只考虑有限制的情况,即第二种情况。
问题分析
为简化问题,我们可以构造一个有向图
G
G
,其包含 个顶点,代表
A^
A
^
中的
2n
2
n
个元素。这样一来我们就可以把问题转化为从图
G
G
中选出 个节点,使其满足限制条件。显然,我们不能同时选
x
x
与 这样的节点(为方便,用
x
x
或者是 都表示图中代表它们的节点,下同)。
那么
x∨y=1
x
∨
y
=
1
在图中代表着什么呢?
我们知道
x∨y=¬ (¬x∧¬y)
x
∨
y
=
¬
(
¬
x
∧
¬
y
)
,这就意味着:如果我们选中
¬x
¬
x
,我们就必须选择
y
y
;如果我们选中 ,我们就必须选中
x
x
。因此对于 ,我们可以在图中添加有向边
(¬x,y)
(
¬
x
,
y
)
,
(¬y,x)
(
¬
y
,
x
)
。
解法
老规矩,先想想朴素的解法是什么。显然我们如果钦定一个变量
x
x
为 或
1
1
,那么与这个变量联通的所有均要取,那么若是在这个过程中我们把一个点的两个状态 与
¬y
¬
y
都取了,这个取法便是不符合条件的,即是我们钦定的
x
x
取值就是错的。对于一个变量 如果其的两个取值均不合法,那么这个
2−SAT
2
−
S
A
T
问题无解。
此处应该注意的是,这个取值过程是不存在回溯的,也就是说我们当前的
x
x
两个取值均不合法的话,不应回溯到先前钦定的那个点,而是直接判断无解。
优化
显然之前那个做法是很不优秀的,我们考虑优化这个做法。对于联通性问题,我们很容易想到缩强联通分量这个算法,这样我们就可以一次性地将一个强联通分量里所有的节点全部取出。就是说如果我们选中强连通分量中的任何一点,那么该强连通分量中的所有其它的顶点也必须被选择。很明显地,如果 和
¬x
¬
x
属于同一个强连通分量,那么产生矛盾,该
2−SAT
2
−
S
A
T
问题无解。
如果该问题有解(即未出现矛盾)如果没有产生矛盾,我们就可以把处在同一个强连通分量中的点和边缩成一个点,得到新的有向图
G′
G
′
。然后,我们把
G'
G
′
中的所有弧反向,得到图
G''
G
′
′
。现在我们观察
G''
G
′
′
。由于已经进行了缩点的操作,因此
G''
G
′
′
中一定不存在环,也就是说,
G''
G
′
′
具有拓扑结构。我们把
G''
G
′
′
中所有顶点置为“未染色”。按照拓扑顺序重复下面的操作:
1
1
、选择拓扑序上的第一个“未染色”节点 ,将其改为黑色。
2
2
、把所有与 矛盾的节点
y
y
(如果存在 ,且
bi
b
i
属于
x
x
代表的强连通分量, 属于
y
y
代表的强连通分量,那么 和
y
y
就是互相矛盾的节点)及其后代全部全部染成白色。
、如果图中还有未染色的节点,则重复
1
1
, 操作。
这样一来,图
G′′
G
″
所被染成黑色的节点所对应的
A^
A
^
中的元素集合,则是该
2−SAT
2
−
S
A
T
问题的一组解。
证明
接下来我们证明这个算法能对于这个
2−SAT
2
−
S
A
T
问题找到一组合法解。
将这个证明分为三步:
命题
1
1
、我们得到的解不会同时染黑一组矛盾的节点。
命题 、我们得到的解不会同时染黑
ai
a
i
和
¬ai
¬
a
i
。
命题
3
3
、我们得到的解不会同时染白 和
¬ai
¬
a
i
。
证明命题 1 1 :
首先,假如我们选定了图 中的未染色节点
x
x
并将其染黑后,与 矛盾的所有其它节点及其后代均会被染成白色。因此,我们把一个节点
x
x
染黑时,任何一个和 矛盾的节点都不会是黑色。
其次,由于我们按照拓扑排序选点,并且把一个顶点染成白色的时候,立刻把它的所有子孙也染成白色。也就是说,如果一个顶点
x
x
不可选,那么所有直接或间接满足条件 “ 假如选择 就必须选择
x
x
” 的顶点也会被染成白色。这样一来, 中不存在有向边
(x,y)
(
x
,
y
)
,其中
x
x
为黑色而 为白色。因此我们得到的结论不可能和
Si
S
i
对应的条件矛盾。
综合这两条结论,我们就可以证明上面的操作不会选定一组矛盾的节点。证毕。
证明命题 2 2 :
首先,对于 和 ¬ai ¬ a i ,它们在图 G G 中一定属于不同的强连通分量(如果不满足,先前就被判定为无解了),因此在图 中被不同的节点所代表,不妨设为 x x 和 。显然 x x 与 是一对矛盾的节点,既然证明 1 1 已经证明了我们的算法不会选择一组矛盾的节点,所以我们不可能同时选中 与 y y 。证毕。
证明命题 :
此处我们采取反证法。
假设我们同时染白了
x
x
与 ,会出现两种情况。
第一种情况,
x
x
单独一个节点作为图 的节点(即图
G
G
的强联通分量),那么我们将 染为白色的可能性只有一种,就是我们将
¬x
¬
x
(或者其所在的强联通分量)染成了黑色,这样才会使得与
¬x
¬
x
矛盾的点
x
x
染白。
第二种情况, 不作为图
G′
G
′
里单独的一个节点,那么必然存在一个节点
y
y
,使得 与
x
x
处于图 中同一个强联通分量,且
¬y
¬
y
被染为黑色(意即,由于
¬y
¬
y
被染成黑色,导致
y
y
这个强联通分量被染白),那么必然存在路径 与路径
(y,x)
(
y
,
x
)
。又因为,根据我们的连边方式,
G
G
中如果存在一个有向边 ,则必然存在有向边
(¬a,¬b)
(
¬
a
,
¬
b
)
。所以我们有路径
(¬y,¬x)
(
¬
y
,
¬
x
)
与路径
(¬x,¬y)
(
¬
x
,
¬
y
)
,即
¬x
¬
x
与
¬y
¬
y
处于同一个强联通分量。那么既然
¬y
¬
y
被染为黑色了,这样
¬x
¬
x
必然也会被染成黑色,与我们同时染白了
x
x
与 的假设矛盾,因此不可能出现同时染白
x
x
与 的情况。证毕。
这样我们就证明了,对于每一组 ai,¬ai a i , ¬ a i ,我们都只会选定 ai a i 与 ¬ai ¬ a i 中的一个,因此我们证明了这个算法的正确性。
复杂度
首先我们寻找强联通分量的、拓扑排序的时间复杂度都是 O(v+e) O ( v + e ) ,而染色操作的复杂度是 O(v+e) O ( v + e ) ,对于每一个限制,我们都会连 2 2 条边,因此 是 O(m) O ( m ) 的,对于每一个变量,我们都会建两个点,因此 v v 是 的。因此对于这个 2−SAT 2 − S A T 问题,我们的复杂度是 O(n+m) O ( n + m ) 的 。
简便的写法
我们知道在使用
Tarjan
T
a
r
j
a
n
算法时,强联通分量的编号就是按照图
G′
G
′
的拓扑排序的逆序给出的,即是以图
G′′
G
″
的拓扑排序给出的,所以我们可以直接按照强联通分量的编号来判断
A
A
中每个元素的取值。
我们记 为点
i
i
所属的强联通分量,则如果 ,我们对于元素
ai
a
i
取
0
0
值,否则取 值。
模板题以及代码
2-sat问题
有
n
n
个布尔变量 ,编号从
0
0
开始。给出一些条件,每个条件用四个整数表示:,表示要求满足
(xu=uval)∨(xv=vval)
(
x
u
=
u
v
a
l
)
∨
(
x
v
=
v
v
a
l
)
。
你要判断,是否存在一组布尔变量满足上述所有条件,如果存在,你需要找出一组变量的值。
#define R register
#define LL long long
template<class TT>inline TT Max(R TT a,R TT b){return a<b?b:a;}
template<class TT>inline TT Min(R TT a,R TT b){return a<b?a:b;}
using namespace std;
template<class TT>inline void read(R TT &x){
x=0;R bool f=false;R char c=getchar();
for(;c<48||c>57;c=getchar())f|=(c=='-');
for(;c>47&&c<58;c=getchar())x=(x<<1)+(x<<3)+(c^48);
(f)&&(x=-x);
}
#define maxn 200010
#define maxm 1000010
//Graph
struct Edge{
int to;
Edge *next;
}*head[maxn];
inline void add(R int u,R int v){
static Edge E[maxm],*e=E;
*e=(Edge){v,head[u]};head[u]=e++;
}
//end
//find strongly connected component
stack<int> stk;
int dfs_clo,scc,dfn[maxn],low[maxn],bel[maxn],ins[maxn];
void dfs(R int u){
dfn[u]=low[u]=++dfs_clo;
stk.push(u);ins[u]=1;
R int v;
for(R Edge *i=head[u];i;i=i->next){
if(!dfn[v=i->to]){
dfs(v);
low[u]=Min(low[v],low[u]);
}else if(ins[v]){
low[u]=Min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u]){
scc++;
do{
v=stk.top();
stk.pop();
ins[v]=0;
bel[v]=scc;
}while(v!=u);
}
}
//end
int n,m;
int main(){
read(n);read(m);
for(R int i=1,a,b,c,d;i<=m;++i){
read(a);read(b);read(c);read(d);
add(a+n*(1-b),c+n*d);
add(c+n*(1-d),a+n*b);
}
for(R int i=0;i<(n<<1);++i){
if(!dfn[i])dfs(i);
}
for(R int i=0;i<n;++i){
if(bel[i]==bel[i+n]){
puts("No");
return 0;
}
}
puts("Yes");
for(R int i=0;i<n;++i){
if(bel[i]<bel[i+n])putchar(48);
else putchar(49);
putchar(' ');
}
return 0;
}