2-SAT
1 概念
有这样一类问题,给出 \(n\) 个布尔变量 \(a_i\),并且给出若干限制条件,每一个限制条件形如 \((\bigvee\limits_{i=1}^k a_{p_i}=x_i)=\text{true}\)。问是否存在一种可行解。这被称之为 k-SAT 问题,当 \(k>2\) 时这个问题是一个 NPC 问题,而 2-SAT 却是一个可以用多项式复杂度解决的问题。
2 求解过程
2-SAT,也就是给定若干变量以及若干二元限制,形如 \(x_i\) 为 \(a\) 或 \(x_j\) 为 \(b\),要我们求一种可行解。
考虑将这个问题转化成一个图论模型,将每个点拆成两个点,分别表示其取值为 \(0,1\) 时的方案。那么如何满足上面的限制就是我们需要考虑的内容。考虑给图上的有向边 \((x,a)\to (y,b)\) 赋予一个意义,表示当 \(x\) 取 \(a\) 时 \(y\) 必须取 \(b\)。那么上面的限制实际上可以转化为两个:\(x_i\) 取 \(\lnot a\) 时 \(x_j\) 必须取 \(b\)、\(x_j\) 取 \(\lnot b\) 时 \(x_i\) 必须取 \(a\)。所以我们连两条边即可限制变量的取值。
接下来考虑判断有无解以及构造可行解。有无解很好判断,假如一个变量的两种取值都能互相走到,说明这个变量产生了矛盾,故无解。此时我们不难发现,这个变量的两种取值一定在同一个强联通分量内,所以跑一遍 Tarjan 缩点即可判断有无解。
构造可行解也很简单,对于一个变量的两种取值,我们看它们所属的强连通分量的拓扑序,显然如果满足拓扑序靠前的取值则另一种拓扑序靠后的取值也要满足,矛盾。所以我们只能满足拓扑序更靠后的取值。当然我们不用真的缩点然后跑拓扑排序,因为 Tarjan 缩点时给强联通分量赋上的实际上就是倒过来的拓扑序,所以改一下符号即可。
模板题:【模板】2-SAT,代码如下:
#include <bits/stdc++.h>
using namespace std;
const int Maxn = 2e6 + 5;
const int Inf = 2e9;
int n, m;
int head[Maxn], edgenum;
struct node {
int nxt, to;
}edge[Maxn];
void add(int u, int v) {
edge[++edgenum] = {head[u], v};
head[u] = edgenum;
}
int V(int x, int a) {return a ? x : x + n;}
int dfn[Maxn], low[Maxn], ind, st[Maxn], top, ins[Maxn], bel[Maxn], scc;
void tarjan(int x) {
dfn[x] = low[x] = ++ind;
st[++top] = x;
ins[x] = 1;
for(int i = head[x]; i; i = edge[i].nxt) {
int to = edge[i].to;
if(!dfn[to]) {
tarjan(to);
low[x] = min(low[x], low[to]);
}
else if(ins[to]) low[x] = min(low[x], dfn[to]);
}
if(dfn[x] == low[x]) {
scc++;
while(1) {
int v = st[top--];
ins[v] = 0;
bel[v] = scc;
if(v == x) break;
}
}
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int x, a, y, b;
cin >> x >> a >> y >> b;
add(V(x, a ^ 1), V(y, b));
add(V(y, b ^ 1), V(x, a));//连边
}
for(int i = 1; i <= (n << 1); i++) {
if(!dfn[i]) tarjan(i);
}
for(int i = 1; i <= n; i++) {
if(bel[i] == bel[i + n]) {//两种取值位于同一强连通分量内
cout << "IMPOSSIBLE\n"; return 0;
}
}
cout << "POSSIBLE\n";
for(int i = 1; i <= n; i++) {
if(bel[i] < bel[i + n]) cout << "1 ";//Tarjan 上拓扑序靠前对应实际拓扑序靠后
else cout << "0 ";
}
return 0;
}
显然解决 2-SAT 问题的核心要点与其他图论问题一致,都是建出合适的图论模型然后套板子。
这里需要强调的一点是,2-SAT 的连边表示的是若 \(a\) 则 \(b\),但是如果题目中出现了若 \(a\) 则 \(b\) 的时候不能只连这个命题的一条边 \(a\to b\),其逆否命题的那条边 \(\lnot b\to \lnot a\) 也要连上,因为原命题和逆否命题是等价的。
3 例题
例 1 [NEERC 2016] Binary Code
首先每个字符串最多只有一个 ?,所以实际上一个字符串只有两种取值,不妨记作 \((s,0)\) 和 \((s,1)\)。然后互相之间不能有前缀关系就是 2-SAT 中的限制。现在的问题就是怎样快速的找出这些限制。
考虑处理前缀关系的算法,显然可以想到 Trie 树。考虑将每个字符串的两种取值直接插入 Trie 树中,那么对于树上的两个终止节点,如果它们有祖先关系,那么它们都应该向对方的相反取值连边表示限制。但是这样做的时间和空间复杂度都难以支撑,考虑优化建图。
对于本题,最合适的优化方法就是前缀和优化建图。我们新建两个 Trie 树,一棵上每个点向父亲及当前节点上的相反取值连边,另一棵上每个点向儿子及当前节点上的相反取值连边。这样,我们对于每个取值,其对应终止节点往第一棵树上的父亲连边、往第二棵树上的儿子连边即可满足限制,且点边数均为 \(O(n)\)。
但是剩下的还有一点,就是同一个节点上我们可能也会有多个终止节点,这些节点中的每个点都要向其他点对应的相反取值连边,这个可以继续用前后缀优化建图。
最后就是固定的字符串了,我们可以假定其一位为 ?,然后强制其必须选原先的那一位,即连边 \((s,\lnot a)\to (s,a)\),其中 \(a\) 为那一位取值。然后跑 2-SAT 即可得出答案。
例 2 [JSOI2019] 精准预测
建图方式本身并不难想到,将每个点对时间拆点,然后两种取值分别表示活和死。那么按照题目中所给的限制条件连边即可。为了满足死后不能复活的要求,我们还要对死点向其后面的死点连边,活点向其前面的活点连边。
现在的问题是我们的点数太多,有 \(O(Tn)\) 个,难以通过。发现这些点中有一些点只起到了传递死亡不能复活这个限制的作用,那他们完全可以直接舍弃掉,直接将有用的点连起来即可。而有用的点只有 \(m\) 个限制条件中给出的点,所以点数可以控制到 \(O(m)\) 范围。
然后考虑如何统计答案,我们应该从每个点在 \(T+1\) 时刻活这个点出发遍历整张图,不过我们的建图是没有办法让我们从谁活推向谁活的,不过我们可以推出谁死。那么从这个点开始遍历,如果走到了一个点在 \(T+1\) 时刻死那它一定没有贡献,用 \(n-1\) 减去这样的点即可。
现在的问题就转化为了一个有向图上可达性问题。进一步观察发现,我们连出来的图实际上是一个 DAG,所以可以直接拓扑排序 + bitset 解决这个问题。不过计算后发现这样做空间会炸,那么考虑经典的 bitset 分块,我们每一次只处理 \(B\) 个死点的可达性,取 \(B=10^4\) 即可保证时间和空间复杂度。
最后还有一点就是如果一个点活可以推向这个点死,那么这个点无论如何都会死,它一定没有贡献且它自身的答案一定为 \(0\),需要特判。
例 3 [NOI2017] 游戏
看到这个限制条件就可以想到 2-SAT,接下来我们发现,如果不管 x 地图,那么题目就是一个标准的 2-SAT,因为每张地图只有两种取值、限制条件是二元的。
如果加上 x 那么就不是很标准了,发现 \(d\) 很小,考虑直接暴力枚举。如果直接枚举 x 地图跑什么车的话复杂度是 \(O(3^d (n+m))\) 的,不够优秀。实际上我们不必要枚举 x 的具体取值,而是可以借助题目中给出的地图进行枚举。我们可以枚举每一个 x 作为 a 或 b 时的答案,这样显然 x 处的所有取值都会被考虑到,正确性得到保证。复杂度是 \(O(2^d(n+m))\) 的,可以通过。
例 4 [POI2011] 同谋者 Conspiracy
题目中的限制显然可以转化为 2-SAT 问题。每个人只有两种取值:后勤或同谋。对每个人的后勤点向其不认识的人的同谋点连边,每个人的同谋点向其认识的人的后勤点连边即可。边数是标准的 \(O(n^2)\)。
然后难点在于求出这个 2-SAT 解的个数。这个问题在一般条件下是不可解决的,但是这个题有一个特殊性质:对于一个方案,每一组中最多挑出一个人换到另一个组才可能得到另一个合法方案。证明显然,如果同时挑了两个人的话这两个人一定不可能满足另一组的限制。
那么我们先跑一边 2-SAT 得出一个合法方案,然后开始调整得到所有方案。具体的,我们只有两种可能:将一个人换组或将两个人交换。对于前者,需要要求这个人换组后没有冲突且该组还有剩余的人;对于后者,要么两个人换组后没有冲突,要么换组后冲突的人只有一个且就是对方。直接累加合法答案即可,复杂度 \(O(n^2)\)。

浙公网安备 33010602011771号