2-SAT问题

传送门

什么是2-SAT问题呢?

我们先说一下SAT问题。给定一个布尔方程,判断是否存在一组布尔变量的取值方案,使得整个方程式的值为真,这种问题被称为布尔方程的可满足性问题(SAT)。SAT问题被证明是NP完全的,当k  > 2的时候我们无法在多项式时间之内求解,但是对于一些特殊的SAT(比如2-SAT)我们可以有效求解。

注:因为笔者不会打与,或,非(否定)的数学符号,所以下文中,“与”使用&&代替,“或”使用||代替,“非”使用!代替。

布尔变量只有两个取值,0和1.

我们称下面这种布尔方程为合取范式:

(a || b || c || ……)^ (d || e || f || g ……) ^ ……

其中a,b等等称为文字,是一个布尔变量或其否定。使用 || 连接的部分称为子句。如果合取范式的每个子句中的文字个数不超过两个,那么对应的SAT问题称为2-SAT问题。

解法:

首先,因为子句都是由不超过两个文字组成的,用|| 表示的关系不是很好理解,我们其实要求的是满足所有条件的一组解,也就是说其实求的是交集(&&),所以我们首先要把“a || b”的形式转换为使用&&表示。

它的等价形式是(!a => b)&& (!b => a),其中=>是蕴含,我们可以把它理解为“推出”的意思。

它的意思是,如果a为真,那么b必然为真,否则如果a为假,那么b可以为真或者为假。

为什么呢?我们可以用现实中推理事情的过程类比一下。如果我们已知A事件,我们用A能够推出B,那么如果A是真的,那么B必然也是真的。如果A是假的话,说明你使用了一种错误的方法去推B,这样的话B的正确性就是不能确定的,所以真或者假都可以。

于是我们就可以将一个合取范式转化为全部由“&&”连接的形式。

对于上文的转换可以自行推一下,只要不是两个0都是成立的。

这是对于每个子句都有两个文字的情况,但是如果只有一个文字呢?

这里首先要说一条重要的结论:如果由!a能推出a,那么a必然为真。

我们假设一下,首先假设a为假,那么!a就是真,然后由于推出的性质,我们又能推出a为真,这与假设a为假是矛盾的。而假设a为真的话,他就是成立的。所以结论是成立的。

既然如此,对于一个单独的文字组成的合取范式,比如(a),那就把它的否定连向它,(!a)也是一样的。

然后对于每一个子句我们是需要建两条边的。因为,一个子句是两者互相限制的,上文(a || b)的限制比较宽松,但是如果我们建立一个小型的布尔方程,我们就能看出来如果少建一条边对于结果的影响。

比如:(b && !a && (a || !b))

对于最后一个合取范式,如果你只建一条边的话,他就是合法的,建立两条就是不合法的。因为如果只建一条的话,那么我们就少了一种限制,所以答案就会被影响。

不过对于一个只有一个文字的情况,我们只需要建一条边。因为,仔细观察的话两边是互相对应的。但是只有一个文字的时候,它对应的反向边仍然是他自己,所以只用建立一条即可。

这样的话,我们把合取范式转化为全部由&&连接的形式,之后我们以=>建立有向边,那么我们就有了一张有向图。因为推出是具有传递性的,所以如果在这个有向图中a能到达b,那就说明a为真的时候b一定为真。所以,在一个图中同一个强连通分量所含的所有变量的布尔值均相同。(要么全部为真要么全部为假,否则就违反了上文)

之后我们就很容易看出,如果对于某个布尔变量x,x和!x同时存在于一个强连通分量中,那么这个布尔方程就是无解的,否则的话,我们只要先缩点,使之成为一个DAG,之后我们求出它的一个拓扑序,用上面重要的一条结论:如果由!a能推出a,那么a必然为真。来解决问题。只要在缩点之后x所在的强连通分量的拓扑序在!x所在的强连通分量的拓扑序之后,那么x即为真。

(拓扑序在前面能推出拓扑序在后面的强连通分量)

不过因为tarjan求强连通分量的时候,强连通分量的编号实际是与拓扑序相反的,所以我们不用真的缩点,直接比较一下两个点所在的强连通分量的编号即可。

所以我们就知道了2-SAT问题的一般解法:

1.将给定的合取范式变为&&连接的表示形式。

2.以=>为关系建图。

3.跑一遍tarjan,求出所有的布尔变量所在的强连通分量的编号。

4.首先比较每个变量所在强连通分量编号,如果自己和自己的否定在一个里面就不合法。

5.否则的话存在一组解,我们只要再比较一下每个变量自己和自己的否定的拓扑序,对其赋值即可。

我们来看一下代码。

 

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
#include<cmath>
#include<set>
#include<queue>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')

using namespace std;
typedef long long ll;
const int M = 1000005;
const int INF = 1000000009;

int read()
{
    int ans = 0,op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
    if(ch == '-') op = -1;
    ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
    ans *= 10;
    ans += ch - '0';
    ch = getchar();
    }
    return ans * op;
}

struct edge
{
    int next,to,from;
}e[M<<1];

int n,m,a,b,x,y,dfn[M<<1],low[M<<1],scc[M<<1],ecnt,idx,cnt,stack[M<<1],top,head[M<<1];
bool vis[M<<1];

void add(int x,int y)
{
    e[++ecnt].to = y;
    e[ecnt].from = x;
    e[ecnt].next = head[x];
    head[x] = ecnt;
}

int rev(int x)
{
    return x > n ? x - n : x + n;
}

void tarjan(int x)
{
    low[x] = dfn[x] = ++idx;
    vis[x] = 1,stack[++top] = x;
    for(int i = head[x];i;i = e[i].next)
    {
    if(!dfn[e[i].to]) tarjan(e[i].to),low[x] = min(low[x],low[e[i].to]);
    else if(vis[e[i].to]) low[x] = min(low[x],dfn[e[i].to]);
    }
    if(dfn[x] == low[x])
    {
    int p;
    cnt++;
    while(p = stack[top--])
    {
        scc[p] = cnt,vis[p] = 0;
        if(p == x) break;
    }
    }
}

int main()
{
    n = read(),m = read();
    rep(i,1,m)
    {
    a = read(),x = read(),b = read(),y = read();
    if(x) a += n;
    if(y) b += n;
    add(rev(a),b),add(rev(b),a);
    }
    rep(i,1,n<<1) if(!dfn[i]) tarjan(i);
    rep(i,1,n)
    {
    if(scc[i] == scc[i+n])
    {
        printf("IMPOSSIBLE\n");
        return 0;
    }
    }
    printf("POSSIBLE\n");
    rep(i,1,n) (scc[i] < scc[i+n]) ? printf("0 ") : printf("1 ");enter;
    return 0;
}

 

posted @ 2018-10-09 15:23  CaptainLi  阅读(2675)  评论(0编辑  收藏  举报