SAT问题总述
SAT是适定性(Satisfiability)问题的简称 。一般形式称为k-适定性问题,简称 k-SAT。 形式化地描述如下:
设 A={a1,a2,⋯,an}A={a1,a2,⋯,an} 为一个有限个布尔变量所构成的集合,A^={a1,a2,⋯,an,¬a1,¬a2,⋯,¬an}A^={a1,a2,⋯,an,¬a1,¬a2,⋯,¬an} 。取 SS 为 的子集,定义 ∨S=s1∨s2∨⋯∨sk (si∈S)∨S=s1∨s2∨⋯∨sk (si∈S) 。求一个 AA 使得给定一组 满足 (∨S1)∨(∨S2)∨⋯∨(∨Sm)=1(∨S1)∨(∨S2)∨⋯∨(∨Sm)=1 。
特别地,若 max{|Si| ∣ i∈[1,m]}≤kmax{|Si| ∣ i∈[1,m]}≤k ,则称这个问题为 k-satk-sat 问题。
已经有前人证明了当 k≥3k≥3 时,这是一个 NP-completeNP-complete 问题,因此我们此处只考虑 k=2k=2 时的 k-适应性问题,也称之为 2-SAT (Two-SAT)问题。
2-SAT问题
在2-SAT问题中,变量的限制性只有两种情况:
11. 单个的布尔变量 ,它没有任何限制。
22. 两个变量的相互限制,例如 。
由于单个变量随意取值均符合条件,因此我们只考虑有限制的情况,即第二种情况。
问题分析
为简化问题,我们可以构造一个有向图 GG,其包含 个顶点,代表 A^A^ 中的 2n2n 个元素。这样一来我们就可以把问题转化为从图 GG 中选出 个节点,使其满足限制条件。显然,我们不能同时选 xx 与 这样的节点(为方便,用 xx 或者是 都表示图中代表它们的节点,下同)。
那么 x∨y=1x∨y=1 在图中代表着什么呢?
我们知道 x∨y=¬ (¬x∧¬y)x∨y=¬ (¬x∧¬y) ,这就意味着:如果我们选中 ¬x¬x ,我们就必须选择 yy ;如果我们选中 ,我们就必须选中 xx。因此对于 ,我们可以在图中添加有向边 (¬x,y)(¬x,y),(¬y,x)(¬y,x)。
解法
老规矩,先想想朴素的解法是什么。显然我们如果钦定一个变量 xx 为 或 11 ,那么与这个变量联通的所有均要取,那么若是在这个过程中我们把一个点的两个状态 与 ¬y¬y 都取了,这个取法便是不符合条件的,即是我们钦定的 xx 取值就是错的。对于一个变量 如果其的两个取值均不合法,那么这个 2−SAT2−SAT 问题无解。
此处应该注意的是,这个取值过程是不存在回溯的,也就是说我们当前的 xx 两个取值均不合法的话,不应回溯到先前钦定的那个点,而是直接判断无解。
优化
显然之前那个做法是很不优秀的,我们考虑优化这个做法。对于联通性问题,我们很容易想到缩强联通分量这个算法,这样我们就可以一次性地将一个强联通分量里所有的节点全部取出。就是说如果我们选中强连通分量中的任何一点,那么该强连通分量中的所有其它的顶点也必须被选择。很明显地,如果 和 ¬x¬x 属于同一个强连通分量,那么产生矛盾,该 2−SAT2−SAT 问题无解。
如果该问题有解(即未出现矛盾)如果没有产生矛盾,我们就可以把处在同一个强连通分量中的点和边缩成一个点,得到新的有向图 G′G′ 。然后,我们把 G'G′ 中的所有弧反向,得到图 G''G′′ 。现在我们观察 G''G′′ 。由于已经进行了缩点的操作,因此 G''G′′ 中一定不存在环,也就是说,G''G′′ 具有拓扑结构。我们把 G''G′′ 中所有顶点置为“未染色”。按照拓扑顺序重复下面的操作:
11、选择拓扑序上的第一个“未染色”节点 ,将其改为黑色。
22、把所有与 矛盾的节点 yy(如果存在 ,且 bibi 属于 xx 代表的强连通分量, 属于 yy 代表的强连通分量,那么 和 yy 就是互相矛盾的节点)及其后代全部全部染成白色。
、如果图中还有未染色的节点,则重复 11, 操作。
这样一来,图 G′′G″ 所被染成黑色的节点所对应的 A^A^ 中的元素集合,则是该 2−SAT2−SAT 问题的一组解。
证明
接下来我们证明这个算法能对于这个 2−SAT2−SAT 问题找到一组合法解。
将这个证明分为三步:
命题11 、我们得到的解不会同时染黑一组矛盾的节点。
命题 、我们得到的解不会同时染黑 aiai 和 ¬ai¬ai。
命题33 、我们得到的解不会同时染白 和 ¬ai¬ai。
证明命题 11:
首先,假如我们选定了图 中的未染色节点 xx 并将其染黑后,与 矛盾的所有其它节点及其后代均会被染成白色。因此,我们把一个节点 xx 染黑时,任何一个和 矛盾的节点都不会是黑色。
其次,由于我们按照拓扑排序选点,并且把一个顶点染成白色的时候,立刻把它的所有子孙也染成白色。也就是说,如果一个顶点 xx 不可选,那么所有直接或间接满足条件 “ 假如选择 就必须选择 xx ” 的顶点也会被染成白色。这样一来, 中不存在有向边 (x,y)(x,y),其中 xx 为黑色而 为白色。因此我们得到的结论不可能和 SiSi 对应的条件矛盾。
综合这两条结论,我们就可以证明上面的操作不会选定一组矛盾的节点。证毕。
证明命题 22:
首先,对于 和 ¬ai¬ai,它们在图 GG 中一定属于不同的强连通分量(如果不满足,先前就被判定为无解了),因此在图 中被不同的节点所代表,不妨设为 xx 和 。显然 xx 与 是一对矛盾的节点,既然证明 11 已经证明了我们的算法不会选择一组矛盾的节点,所以我们不可能同时选中 与 yy 。证毕。
证明命题 :
此处我们采取反证法。
假设我们同时染白了 xx 与 ,会出现两种情况。
第一种情况, xx 单独一个节点作为图 的节点(即图 GG 的强联通分量),那么我们将 染为白色的可能性只有一种,就是我们将 ¬x¬x (或者其所在的强联通分量)染成了黑色,这样才会使得与 ¬x¬x 矛盾的点 xx 染白。
第二种情况, 不作为图 G′G′ 里单独的一个节点,那么必然存在一个节点 yy,使得 与 xx 处于图 中同一个强联通分量,且 ¬y¬y 被染为黑色(意即,由于 ¬y¬y 被染成黑色,导致 yy 这个强联通分量被染白),那么必然存在路径 与路径 (y,x)(y,x) 。又因为,根据我们的连边方式,GG 中如果存在一个有向边 ,则必然存在有向边 (¬a,¬b)(¬a,¬b) 。所以我们有路径 (¬y,¬x)(¬y,¬x) 与路径 (¬x,¬y)(¬x,¬y),即 ¬x¬x 与 ¬y¬y 处于同一个强联通分量。那么既然 ¬y¬y 被染为黑色了,这样 ¬x¬x 必然也会被染成黑色,与我们同时染白了 xx 与 的假设矛盾,因此不可能出现同时染白 xx 与 的情况。证毕。
这样我们就证明了,对于每一组 ai,¬aiai,¬ai,我们都只会选定 aiai 与 ¬ai¬ai 中的一个,因此我们证明了这个算法的正确性。
复杂度
首先我们寻找强联通分量的、拓扑排序的时间复杂度都是 O(v+e)O(v+e),而染色操作的复杂度是 O(v+e)O(v+e),对于每一个限制,我们都会连 22 条边,因此 是 O(m)O(m)的,对于每一个变量,我们都会建两个点,因此 vv 是 的。因此对于这个 2−SAT2−SAT 问题,我们的复杂度是 O(n+m)O(n+m) 的 。
简便的写法
我们知道在使用 TarjanTarjan 算法时,强联通分量的编号就是按照图 G′G′ 的拓扑排序的逆序给出的,即是以图 G′′G″ 的拓扑排序给出的,所以我们可以直接按照强联通分量的编号来判断 AA 中每个元素的取值。
我们记 为点 ii 所属的强联通分量,则如果 ,我们对于元素 aiai取00 值,否则取 值。
模板题以及代码
2-sat问题
有 nn 个布尔变量 ,编号从 00 开始。给出一些条件,每个条件用四个整数表示:,表示要求满足 (xu=uval)∨(xv=vval)(xu=uval)∨(xv=vval)。
你要判断,是否存在一组布尔变量满足上述所有条件,如果存在,你需要找出一组变量的值。
#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;
}