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; }