2-SAT
概念
SAT是适定性(Satisfiability)问题的简称 。一般形式为k-适定性问题,简称 k − S A T k-SAT k−SAT。
可以证明,当 k > 2 k>2 k>2时, k − S A T k-SAT k−SAT是NP完全的。因此一般讨论的是 k = 2 k=2 k=2的情况,即 2 − S A T 2-SAT 2−SAT问题。
我们通俗的说,就是给你 n n n个变量 a i a_i ai,每个变量能且只能取 0 / 1 0/1 0/1的值。同时给出若干条件,形式诸如( n o t ) a i o p t ( n o t ) a j = 0 / 1 not)a_iopt(not) a_j=0/1 not)aiopt(not)aj=0/1,其中 o p t opt opt表示 a n d , o r , x o r and,or,xor and,or,xor中的一种
而求解2-SAT的解就是求出满足所有限制的一组 a a a
解法
首先我们考虑将 2 − S A T 2-SAT 2−SAT问题往图论的方向靠,我们发现每个点要么取 0 0 0,要么取 1 1 1。因此对于 a i a_i ai,我们建两个点 2 i − 1 2i−1 2i−1与 2 i 2i 2i分别表示 a i a_i ai取 0 0 0和 1 1 1
然后我们考虑建边来表示这些关系,我们令一条有向边的意义: x → y x→y x→y表示如果选择了 x x x就必须选 y y y
那么我们可以举一些简单的例子来总结下连边的规律(用 i ′ i′ i′表示 i i i的反面):
i
,
j
i,j
i,j不能同时选:选了
i
i
i就要选
j
′
j′
j′,选
j
j
j就要选
i
′
i′
i′。故
i
→
j
′
,
j
→
i
′
i→j′,j→i′
i→j′,j→i′。一般操作即为
a
i
x
o
r
a
j
=
1
a_i xor a_j=1
aixoraj=1
i
,
j
i,j
i,j必须同时选:选了
i
i
i就要选
j
j
j,选
j
j
j就要选
i
i
i。故
i
→
j
,
j
→
i
i→j,j→i
i→j,j→i。一般操作即为
a
i
x
o
r
a
j
=
0
a_ixora_j=0
aixoraj=0
i
,
j
i,j
i,j任选(但至少选一个)选一个:选了
i
i
i就要选
j
′
j′
j′,选
j
j
j就要选
i
′
i′
i′,选
i
′
i′
i′就要选
j
j
j,选
j
′
j′
j′就要选
i
i
i。故
i
→
j
′
,
j
→
i
′
,
i
′
→
j
,
j
′
→
i
i→j′,j→i′,i′→j,j′→i
i→j′,j→i′,i′→j,j′→i一般操作即为
a
i
o
r
a
j
=
1
a_iora_j=1
aioraj=1
i必须选:直接i′→i,可以保证无论怎样都选
i
i
i。一般操作为给出的
a
i
=
1
a_i=1
ai=1或
a
i
a
n
d
a
j
=
1
a_ianda_j=1
aiandaj=1
建好图然后就是考虑怎么用图论的方式解决
2
−
S
A
T
2-SAT
2−SAT了。
一,显然,
D
F
S
DFS
DFS;
1,对于每个当前不确定的变量
a
i
a_i
ai,令
a
i
=
0
a_i=0
ai=0然后沿着边
D
F
S
DFS
DFS访问相连的点。
2,检查如果会导致任意一个j与j′都被选,那么撤销。否则令
a
i
=
0
a_i=0
ai=0
3,否则令
a
i
=
1
a_i=1
ai=1,重复
2
2
2。如果还不行就无解。
4,继续考虑下一个不确定的变量
这样的话正确性显然,由于这里的
D
F
S
DFS
DFS涉及到全局,因此复杂度是
O
(
n
(
n
+
m
)
)
O(n(n+m))
O(n(n+m))的。
一般情况下已经很优秀了,而且还可以改进:
只需要在DFS之前判断i′能否走到i就可以省略撤销标记的过程,所以我们可以bitset优化传递闭包做到
O
(
n
3
/
w
)
O({n^3}/{w)}
O(n3/w)预处理,然后就可以
O
(
n
+
m
)
O(n+m)
O(n+m)的DFS了。
这种做法还可以保证解的字典序,有时不失为一种不错的方法。
二,连通分量
考虑我们上面的判断有无解的情况,我们想到完全可以借助SCC来判断两个点是否互相到达。
那么我们先缩点,如果
i
i
i与
i
′
i′
i′在同一SCC里那么显然无解。
否则选择
i
i
i与
i
′
i′
i′中拓扑序较大的一个就可以得到一组可行解。
不是很常用(主要是一般题目的数据范围都不需要),用来判可行性比较好。
代码实现(模板题:SCC)
#include<bits/stdc++.h>
#define M 4000009
using namespace std;
int nxt[M],first[M],to[M],_first[M],_nxt[M],_to[M],tot;
int n,m,vis[M],turn[M],cnt,bel[M];
int read(){
char ch;
int f=1,re=0;
for(ch=getchar();!isdigit(ch)&&ch!='-';ch=getchar());
if(ch=='-'){f=-1,ch=getchar();}
for(;isdigit(ch);ch=getchar()) re=(re<<3)+(re<<1)+ch-'0';
return re*f;
}
void add(int x,int y){
nxt[++tot]=first[x],first[x]=tot,to[tot]=y;
_nxt[++tot]=_first[y],_first[y]=tot,_to[tot]=x;
}
void dfs1(int u){
vis[u]=1;
for(int i=first[u];i;i=nxt[i]){
int v=to[i];
if(!vis[v]) dfs1(v);
}turn[++cnt]=u;
}
void dfs2(int u){
bel[u]=cnt,vis[u]=1;
for(int i=_first[u];i;i=_nxt[i]){
int v=_to[i];
if(!vis[v]) dfs2(v);
}
}
void solve(){//Kosaraju算法:一种找强连通分量的算法
for(int i=1;i<=n*2;i++) if(!vis[i]) dfs1(i);
memset(vis,0,sizeof(vis)),cnt=0;
for(int i=2*n;i>=1;i--) if(!vis[turn[i]]) cnt++,dfs2(turn[i]);
}
int main(){
int x,y,fx,fy;
n=read(),m=read();
for(int i=1;i<=m;i++){
x=read(),fx=read(),y=read(),fy=read();
add(x+n*(fx&1),y+n*(fy^1));
add(y+n*(fy&1),x+n*(fx^1));
}solve();
for(int i=1;i<=n;i++) if(bel[i]==bel[i+n]){printf("IMPOSSIBLE\n");return 0;}
printf("POSSIBLE\n");
for(int i=1;i<=n;i++) printf("%d ",bel[i]>bel[i+n]);
return 0;
}
//Kosaraju算法:一种找强连通分量的算法
例题
HDU3062
HDU1814
HDU3062
HDU1814
洛谷P3007