Night的算法杂谈-2SAT问题

SAT问题总述

SAT是适定性(Satisfiability)问题的简称 。一般形式称为k-适定性问题,简称 k-SAT。 形式化地描述如下:
A = { a 1 , a 2 , , a n } 为一个有限个布尔变量所构成的集合, A ^ = { a 1 , a 2 , , a n , ¬ a 1 , ¬ a 2 , , ¬ a n } 。取 S A ^ 的子集,定义 S = s 1 s 2 s k   ( s i S ) 。求一个 A 使得给定一组 S 1 , S 2 , , S m 满足 ( S 1 ) ( S 2 ) ( S m ) = 1
特别地,若 max { | S i |     i [ 1 , m ] } k ,则称这个问题为 k-sat 问题。
已经有前人证明了当 k 3 时,这是一个 NP-complete 问题,因此我们此处只考虑 k = 2 时的 k-适应性问题,也称之为 2-SAT (Two-SAT)问题。


2-SAT问题

在2-SAT问题中,变量的限制性只有两种情况:
1 . 单个的布尔变量 x ,它没有任何限制。
2 . 两个变量的相互限制,例如 x y = 1   (   x , y A ^   )
由于单个变量随意取值均符合条件,因此我们只考虑有限制的情况,即第二种情况。

问题分析

为简化问题,我们可以构造一个有向图 G ,其包含 2 n 个顶点,代表 A ^ 中的 2 n 个元素。这样一来我们就可以把问题转化为从图 G 中选出 n 个节点,使其满足限制条件。显然,我们不能同时选 x ¬ x 这样的节点(为方便,用 x 或者是 ¬ x 都表示图中代表它们的节点,下同)。
那么 x y = 1 在图中代表着什么呢?
我们知道 x y = ¬   ( ¬ x ¬ y ) ,这就意味着:如果我们选中 ¬ x ,我们就必须选择 y ;如果我们选中 ¬ y ,我们就必须选中 x 。因此对于 S i = x y ,我们可以在图中添加有向边 ( ¬ x , y ) ( ¬ y , x )

解法

老规矩,先想想朴素的解法是什么。显然我们如果钦定一个变量 x 0 1 ,那么与这个变量联通的所有均要取,那么若是在这个过程中我们把一个点的两个状态 y ¬ y 都取了,这个取法便是不符合条件的,即是我们钦定的 x 取值就是错的。对于一个变量 x 如果其的两个取值均不合法,那么这个 2 S A T 问题无解。
此处应该注意的是,这个取值过程是不存在回溯的,也就是说我们当前的 x 两个取值均不合法的话,不应回溯到先前钦定的那个点,而是直接判断无解。

优化

显然之前那个做法是很不优秀的,我们考虑优化这个做法。对于联通性问题,我们很容易想到缩强联通分量这个算法,这样我们就可以一次性地将一个强联通分量里所有的节点全部取出。就是说如果我们选中强连通分量中的任何一点,那么该强连通分量中的所有其它的顶点也必须被选择。很明显地,如果 x ¬ x 属于同一个强连通分量,那么产生矛盾,该 2 S A T 问题无解。
如果该问题有解(即未出现矛盾)如果没有产生矛盾,我们就可以把处在同一个强连通分量中的点和边缩成一个点,得到新的有向图 G 。然后,我们把 G 中的所有弧反向,得到图 G 。现在我们观察 G 。由于已经进行了缩点的操作,因此 G 中一定不存在环,也就是说, G 具有拓扑结构。我们把 G 中所有顶点置为“未染色”。按照拓扑顺序重复下面的操作:
1 、选择拓扑序上的第一个“未染色”节点 x ,将其改为黑色。
2 、把所有与 x 矛盾的节点 y (如果存在 a i , ¬ a i A ^ ,且 b i 属于 x 代表的强连通分量, ¬ b i 属于 y 代表的强连通分量,那么 x y 就是互相矛盾的节点)及其后代全部全部染成白色。
3 、如果图中还有未染色的节点,则重复 1 2 操作。
这样一来,图 G 所被染成黑色的节点所对应的 A ^ 中的元素集合,则是该 2 S A T 问题的一组解。

证明

接下来我们证明这个算法能对于这个 2 S A T 问题找到一组合法解。
将这个证明分为三步:
命题 1 、我们得到的解不会同时染黑一组矛盾的节点。
命题 2 、我们得到的解不会同时染黑 a i ¬ a i
命题 3 、我们得到的解不会同时染白 a i ¬ a i

证明命题 1

首先,假如我们选定了图 G 中的未染色节点 x 并将其染黑后,与 x 矛盾的所有其它节点及其后代均会被染成白色。因此,我们把一个节点 x 染黑时,任何一个和 x 矛盾的节点都不会是黑色。
其次,由于我们按照拓扑排序选点,并且把一个顶点染成白色的时候,立刻把它的所有子孙也染成白色。也就是说,如果一个顶点 x 不可选,那么所有直接或间接满足条件 “ 假如选择 y 就必须选择 x ” 的顶点也会被染成白色。这样一来, G 中不存在有向边 ( x , y ) ,其中 x 为黑色而 y 为白色。因此我们得到的结论不可能和 S i 对应的条件矛盾。
综合这两条结论,我们就可以证明上面的操作不会选定一组矛盾的节点。证毕。

证明命题 2

首先,对于 a i ¬ a i ,它们在图 G 中一定属于不同的强连通分量(如果不满足,先前就被判定为无解了),因此在图 G 中被不同的节点所代表,不妨设为 x y 。显然 x y 是一对矛盾的节点,既然证明 1 已经证明了我们的算法不会选择一组矛盾的节点,所以我们不可能同时选中 x y 。证毕。

证明命题 3

此处我们采取反证法。
假设我们同时染白了 x ¬ x ,会出现两种情况。
第一种情况, x 单独一个节点作为图 G 的节点(即图 G 的强联通分量),那么我们将 x 染为白色的可能性只有一种,就是我们将 ¬ x (或者其所在的强联通分量)染成了黑色,这样才会使得与 ¬ x 矛盾的点 x 染白。
第二种情况, x 不作为图 G 里单独的一个节点,那么必然存在一个节点 y ,使得 y x 处于图 G 中同一个强联通分量,且 ¬ y 被染为黑色(意即,由于 ¬ y 被染成黑色,导致 y 这个强联通分量被染白),那么必然存在路径 ( x , y ) 与路径 ( y , x ) 。又因为,根据我们的连边方式, G 中如果存在一个有向边 ( a , b ) ,则必然存在有向边 ( ¬ a , ¬ b ) 。所以我们有路径 ( ¬ y , ¬ x ) 与路径 ( ¬ x , ¬ y ) ,即 ¬ x ¬ y 处于同一个强联通分量。那么既然 ¬ y 被染为黑色了,这样 ¬ x 必然也会被染成黑色,与我们同时染白了 x ¬ x 的假设矛盾,因此不可能出现同时染白 x ¬ x 的情况。证毕。

这样我们就证明了,对于每一组 a i , ¬ a i ,我们都只会选定 a i ¬ a i 中的一个,因此我们证明了这个算法的正确性。

复杂度

首先我们寻找强联通分量的、拓扑排序的时间复杂度都是 O ( v + e ) ,而染色操作的复杂度是 O ( v + e ) ,对于每一个限制,我们都会连 2 条边,因此 e O ( m ) 的,对于每一个变量,我们都会建两个点,因此 v O ( n ) 的。因此对于这个 2 S A T 问题,我们的复杂度是 O ( n + m ) 的 。

简便的写法

我们知道在使用 T a r j a n 算法时,强联通分量的编号就是按照图 G 的拓扑排序的逆序给出的,即是以图 G 的拓扑排序给出的,所以我们可以直接按照强联通分量的编号来判断 A 中每个元素的取值。
我们记 b e l i 为点 i 所属的强联通分量,则如果 b e l i < b e l ¬ i ,我们对于元素 a i 0 值,否则取 1 值。


模板题以及代码

2-sat问题

n 个布尔变量 x i ,编号从 0 开始。给出一些条件,每个条件用四个整数表示: u , u v a l , v , v v a l ,表示要求满足 ( 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-u=uvald),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;
}

猜你喜欢

转载自blog.csdn.net/night2002/article/details/79732693