2-SAT 图论算法:
理解:
给定一个布尔方程,判断是否存在一组布尔变量的取值方案,使得整个方程值为真的问题,被称为布尔方程的可满足性问题(SAT) 。SAT 问题是NP 完全的,但对于一些特殊形式的SAT 问题我们可以有效求解。
我们将下面这种布尔方程称为合取范式:
其中称为文字,它是一个布尔变量或其否定。像这样用连接的部分称为子句。如果合取范式的每个子句中的文字个数都不超过两个,那么对应的SAT 问题又称为2-SAT 问题。
思想:
对于给定的2-SAT ,首先将每个子句 (a || b) = 1 改写成等价形式(!a => b || !b => a) 我们可以简单的理解为,若a为不为真,则b一定为假。对于每个变量x, 构造两个顶点分别代表x, !x,用推出的关系为边建立有向图, 如果图中的a能到达b,则当a为真时,b一定为真。因此我们可以认为该图中同一个强连通分量中所含的所有变量的bool 值均相同。
若存在某个变量x, x 和 !x 的两个顶点在同一个强连通分量中,原bool 表达式的值就不能为真,反之,若没有这样的情况,我们可以进行强连通分量的缩点,构新图,显然,新图是一个dag(有向无环图,即拓扑图),我们求出它的一个拓扑序。那么对于每个变量x,存在。x所在的强连通分量(新图)的拓扑序在 !x 所在的强连通分量之后ó x为真。 这就是使得当前bool表达式成立的一组bool变量取值。
算法优化:
如果有用心学 tarjan, 你应该能够发现一点,其实
tarjan 求SCC 就是按照头拓扑序逆序来求的。
证明:其实具体的证明应该不是太简单,但是我有一个形象的说明方法,考虑这么一个问题,我们进行tarjan-dfs 时,我们是根据边的指向不断层数加深的,那么,我们先找到的一定是递归层数最深的,也就是拓扑序最大的了。所以事情就变得简单了许多,我们可以用 scc_id[u] < scc_id[v]ó top[u] > top[v] 来等价。
时间复杂度:
若bool变量的个数为n, 子句个数为m, 那么用tarjan 求解的时间复杂度就是O(n + m)。
建图规则:
常见的2-SAT 建图中的bool子句一般就是用三种符号连接,&(and), |(or), ^(xor), 下面我们来分别讨论。
1、(a & b) = true
ð a = true, b = true (强制为true)
ð !a -> a, !b -> b
解释:显然我们要强制要求a, b 均为true, 所以如果我们想达成这个目的我们就需要在即使选中!a 为真的情况下,也要让a 为真,所以直接!a -> a( 由!a 到a 连边, 表示当!a = true è a = true)。
2、(a & b) = false
ð a = false or b = false or a, b = false
ð a = true -> b = false, b = true -> a = false
ð a -> !b, b -> !a
解释:显然我们要强制要求a, b 中有一个为false, 也就是说选择了一个为true 后, 另一个一定为false, 所以由 a -> !b, b -> !a 连边。
3、(a | b) = true
ð a = true or b = true or a, b = true
ð a = false -> b = true, b = false -> a = true
ð !a -> b, !b -> a
解释:显然我们要强制要求a, b 中有一个为true, 也就是说选择了一个为flase 后, 另一个一定为true, 所以由 !a -> b, !b -> a 连边。
4、(a | b) = false
ð a = false ans b = false (强制为false)
ð a -> !a, b -> !b
解释:显然因为我们要强制要求a, b 均为false, 所以如果我们想达成这个目的我们就需要在即使选中a为真的情况下,也要让!a为真,所以直接a -> !a( 由a 到!a 连边, 表示当a = true è !a = true)。
5、(a ^ b) = true
ð a != b
ð a = true -> b = false, a = false -> b = true, b = true -> a = false, b = false -> a = true
ð a -> !b, !a -> b, b -> !a, !b -> a
解释:显然我们要强制要求a, b 不相同, 所以如果我们想达成这个目的我们就需要其中一个确定之后,确定另一个与它相反,所以直接a -> !b, !a -> b, b -> !a, !b -> a 连边即可。
6、(a ^ b) = false
ð a == b
ð a = true -> b = true, a = false -> b = false, b = true -> a = true, b = false -> a = false
ð a -> b, b -> a, !a -> !b, !b -> !a
解释:显然我们要强制要求a, b 相同, 所以如果我们想达成这个目的我们就需要其中一个确定之后,确定另一个与它相同,所以直接!a -> !b, a -> b, !b -> !a, b -> a 连边即可。
适用例题:
题目大意 : 给出一堆bool 表达式, 询问是否有满足全部的可行解,有就输出YES ,没有就输出NO 。
分析:显然这就是一道2-SAT 裸题(毫不掩饰),直接上就好,只不过呢,这道题有一个好处就是可以锻炼建图。
Source:
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std; inline void R(int &v) { char c = 0; bool p = true; v = 0; while(!isdigit(c)) { if(c == '-') p = false; c = getchar(); } while(isdigit(c)) { v = (v << 3) + (v << 1) + (c ^ '0'); c = getchar(); } if(!p) v = -v; } const int MAXN = 1000 + 10; const int MAXM = 5000000 + 10; stack
s; int first[MAXN << 1], low[MAXN << 1], num[MAXN << 1], scc[MAXN << 1]; int n, m, x, y, z, cnt, tot, ind; bool exist[MAXN << 1]; char s1[10]; struct node { int next, to; } edge[MAXM]; inline void create(int x, int y) { tot++, edge[tot].next = first[x], first[x] = tot, edge[tot].to = y; } void build() { R(n), R(m); for(int i = 1; i <= m; ++i) { R(x), R(y), R(z); scanf("%s", s1); /*建图规则*/ if(s1[0] == 'A') { if(z) create(x + n, x), create(y + n, y); else create(x, y + n), create(y, x + n); } if(s1[0] == 'O') { if(z) create(x + n, y), create(y + n, x); else create(x, x + n), create(y, y + n); } if(s1[0] == 'X') { if(z) create(x, y + n), create(y, x + n), create(x + n, y), create(y + n, x); else create(x, y), create(y, x), create(x + n, y + n), create(y + n, x + n); } } } void dfs(int cur) { num[cur] = low[cur] = ++ind; s.push(cur), exist[cur] = true; for(int p = first[cur]; p; p = edge[p].next) { if(!num[edge[p].to]) dfs(edge[p].to), low[cur] = min(low[edge[p].to], low[cur]); else if(exist[edge[p].to]) low[cur] = min(num[edge[p].to], low[cur]); } if(num[cur] == low[cur]) { int o = s.top(); cnt++; while(o != cur) exist[o] = false, scc[o] = cnt, s.pop(), o = s.top(); exist[o] = false, scc[o] = cnt, s.pop(); } } void work() { for(int i = 0; i <= ((n - 1) << 1); ++i) if(!num[i]) dfs(i); for(int i = 0; i < n; ++i) if(scc[i] == scc[i + n]) cout << "NO", exit(0); cout << "YES"; } int main() { build(); work(); return 0; }