2-SAT
1 2-SAT 问题
1.1 什么是 2-SAT
\(n\) 个 bool 变量,有 \(m\) 个限制,形如:“第 \(i\) 个变量的真假性为 \(s\),那么第 \(j\) 个变量的真假性需要为 \(t\)”,其中 \(i, j \in [1, n], s, t \in \{0, 1\}\)。求这些变量的一组合法取值,使得满足所有限制。
1.2 求解一组给定 2-SAT 的解
这里把限制简化为 \((\neg) i \rightarrow (\neg) j\)。建立 \(2n\) 个点,意义类似并查集。对应进行连边。注意对于限制,要给其逆否命题也连一条边。
我们做一个观察,首先对于能够到达的点,可以看成其直接相邻。然后考虑这样一个事实:如果 \(x \rightarrow \neg x\) 并且 \(\neg x \rightarrow x\) 那么 \(x\) 取 \(0, 1\) 都不对。这种情况是无解。如果 \(x \rightarrow \neg x\) 那么 \(x\) 只可以取 \(0\)。
于是我们想到按照拓扑序来搞。就是如果发现 \(x \rightarrow \neg x\) 那么令 \(x=0\)。\(x \rightarrow \neg x\) 说明一定 \(x\) 点的拓扑序在 \(\neg x\) 的前面,因此我们取拓扑序后面的那个点。
但是这图有很多环怎么办呢?考虑强连通分量的拓扑序。对于在同一个连通分量内的点,是等价的。缩点之后是一个 DAG。所以这是自然的。
首先证明一下这个对所有图上的限制都不会违反。违反限制,也就是 \(x \rightsquigarrow y\) 满足 \(x\) 选了但是 \(y\) 没选。因为 \(x\) 的拓扑序在 \(y\) 前面,\(\neg x\) 在 \(\neg y\) 后面,所以如果选了 \(x\),那么 \(\neg y \le \neg x \le x \le y\),一定是 \(y\) 选了。
其次证明建出的图包含了所有限制。也就是说,如果不存在一条边使得起点满足并且终点不满足,那么就是合法的。通过真值表我们可以发现是对的,只有原命题和逆否命题有用。
于是我们只需要建图,kosaraju 判 scc 上有没有对边,然后拓扑排序即可。
考虑 kosaraju 算法对连通分量的标号。可以发现它其实标出了拓扑序(这个和 tarjan 不一样,但是我是 kosaraju 党,记住这个就好了),所以不需要单独再进行一次拓扑排序,直接使用标号即可。
kosaraju 怎么写:
int cnt=0, dfn[1620], rnk[1620];bool vis[1620];int col,yse[1620];
void dfs1(int now) {
vis[now]=1;
for(int i:g[now]){
if(!vis[i]){
dfs1(i);
}
}
dfn[now]=++cnt;rnk[cnt]=now;
}
void dfs2(int now) {
yse[now]=col;
for(int i:f[now]){
if(!yse[i]){
dfs2(i);
}
}
}
bool kosaraju() {
//dfs并且在回溯的时候赋一个cnt
//对反图进行最大标号顶点开始的dfs,然后走到的就是强连通分量。
f(i,1,8*k)vis[i]=0,yse[i]=0,cnt=0;
f(i,1,8*k){
if(!vis[i]){dfs1(i);}
}
for(int i=8*k;i>=1;i--){
if(!yse[rnk[i]]){col++;dfs2(rnk[i]);}
}
f(i,1,4*k)if(yse[i]==yse[i+4*k])return 0;
return 1;
}
需要注意的地方是,如果多次进行 kosaraju 算法,那么 cnt 一定要每次都赋 0,否则 rnk 会寄。
1.3 另一种角度观察
考虑一开始所有解的集合是 \(\{2^n\}\)。
加入一个限制,例如 \(a_i \or !a_j\),其实就是把解集合里面 \(a_i = 0\) 并且 \(a_j = 1\) 的所有解全部去掉。
最后剩下来的一个集合就是 2-SAT 的解集。
这个系统可以用一个大小 \(4n^2\) 的数组 \(ok\) 表示,\(ok_{i,j,0/1,0/1}\) 表示是否能够有 \(i\) 的取值为 \(0/1\) 并且 \(j\) 的取值为 \(0/1\)。而求解所有解的过程(是一个 NPC)的一个 dfs 算法就是,从前到后依次确认某一个字符等于 \(0/1\) 的时候是否前 \(i\) 个字符均满足系统的限制。如果满足,继续向前走,否则直接返回。
时间复杂度为 \(O(m^2|S|)\),其中 \(m\) 是变量个数,\(|S|\) 是解集大小。
注意 2-SAT 里面很多 bool 类型,我们会尝试利用 bitset 优化它。但是注意只有一个长度为 \(n\) 的 bitset 做整体位运算的复杂度是 \(O(\cfrac{n}{w})\),而不是对 bitset 做任意操作都是。
豆知識:bitset 的 any() 函数的返回值是:\(1\),如果 bitset 里面有值。比 count() 快很多。
又一个豆知識:除的 \(w\) 是字长的意思,不知道谁开始以讹传讹写成 \(\omega\) 的。
ucup 14 G. LaLa and Divination Magic
【题意】
给定一个大小为 \(m\) 的 2-SAT 系统的所有 \(n\) 个解,构造一个 2-SAT 系统使得其解集等于题目给的解集,这个系统必须包含不超过 \(2m^2\) 个限制。如果没有任何一个 2-SAT 系统的解集是题目里给的,那么输出 \(-1\)。
\(n, m \le 2000\)
【分析】
首先根据上面“删解”的想法,自然地想到可以枚举所有 \(i,j\),如果没有一个解满足 \(i=x\) 且 \(j=y\),那么令 \(ok_{i,j,x,y} = 0\)(加入一个限制)
发现去掉 \(i=j, x\neq y\) 的没有用的限制之后正好 \(2m^2\) 个。
那么问题变成了,这些限制下是否还有其他解?
考虑和上面说的一样 dfs,两部分的时间都是 \(O(nm^2)\)。
我们需要将时间降到 \(O(\cfrac{nm^2}{w} )\)。这也是个大工程。
考虑第一部分,我们需要判断是否存在一个 \(i=x\) 并且 \(j=y\) 的解。怎么搞呢?考虑 bitset 记录所有 \(i=x\) 的位置,两个 bitset 取 &,没了!
考虑第二部分,我们需要判断是否对于每一个 \(i \le step\) 都满足 \(ok_{i, step, a_i, a_{step}} = 1\)。这个有点麻烦。我们考虑预处理 \(step = a_{step}\) 的时候,前面所有数有几种取值。
对于每一个 \(i\):
- 有 \(0\) 种取值。这时候意味着我们根本不能选择 \(step = a_{step}\)。
- 有 \(1\) 种取值:0/1。
- 有 \(2\) 种取值:选啥都行。
这里我考虑的是,对于每一个 \(step, 0/1\),记录两个数组,对于 \(i\),如果 \(0\) 种取值直接记录不能这么选;如果 \(1\) 种取值那么两个数组这一位都是这种取值;如果 \(2\) 种取值那么两个数组这一位分别是 \(0\) 和 \(1\)。
最后数一下 \(a\) 数组和这两个数组同或的结果的 count 之和。应该到达的数是,\(1\) 种取值的数的个数 \(\times 2\) 加上 \(2\) 种取值的数的个数。
最后时间复杂度 \(O(\cfrac{nm^2}{w})\)。
PS:这题怎么造数据啊,只会造 \(n=1\),但是很弱。
PPS:额,其实想说的是怎么除 \(w\)。大概是拿出你想判的东西,把它压到 bitset 里做整体操作。(一位位判的地方就是笨蛋的地方,要考虑优化了)