2-SAT

2-SAT 问题

和网络流一样,很多问题都可以建模为 2-SAT 问题,只要会求解 2-SAT,就可以解决许多问题。

洛谷博客
github
vercel

k-SAT

首先引入 k-SAT 问题。

k-SAT 问题说的就是有 nn 个布尔值 aia_imm 组约束条件,其中每组约束都是 aakk 个值进行逻辑运算后等于一个给定值 (0011) 的形式,求一组满足约束的 aia_i(或者输出无解)。

比如

{(a0anda1)or(nota2)anda3=1(a0xor(nota1))anda2=0(a0ora1)xor(nota2)=1\begin{cases} (a_0 \operatorname{and} a_1) \operatorname{or} (\operatorname{not} a_2)\operatorname{and} a_3=1\\ (a_0 \operatorname{xor} (\operatorname{not}a_1)) \operatorname{and} a_2 =0\\ (a_0 \operatorname{or} a_1) \operatorname{xor} (\operatorname{not} a_2)=1 \end{cases}

是一个 3-SAT 问题,它的一个解为

{a0=0a1=0a2=0a3=1\begin{cases} a_0=0\\ a_1=0\\ a_2=0\\ a_3=1 \end{cases}

可以证明 k-SATk3k\ge3 时是没有多项式解法的,但是 k2k\le2 时有。

2-SAT

2-SAT 问题是 k-SAT 问题在 k=2k=2 时的特例。

每一个约束条件都可以表示为 「aia_ixxaja_jyy」的形式。和网络流一样,很多问题都可以建模为 2-SAT 问题,只要会求解 2-SAT,就可以解决许多问题。建模当然比原算法难哈哈哈

接下来是重点:如何求解 2-SAT 问题。

1. 图论建膜

先建立膜型 stO CYB Orz

我们把每个变量拆成两个点,一个表示该变量为真,另一个表示该变量为假。我们约定 <i,x>\left<i,x\right>ai=xa_i=x 代表的节点,如 <10,1>\left<10,1\right> 表示 a10=1a_{10}=1 代表的节点。因此一个节点代表了一个命题。既然这样,我们就可以自然地定义节点 <i,x>\left<i,x\right> 的非节点 ¬<i,x>\neg\left<i,x\right> 定义为 <i,¬x>\left<i,\neg x\right>,表示 aixa_i\not= x 也就是 ai=¬xa_i=\neg x

边表示蕴含关系,即 <i,x>\left<i,x\right><j,y>\left<j,y\right> 间有一条有向边表示如果 ai=xa_i=xaj=ya_j=y。注意连边时要把这条边的逆否边连起来,<i,x><j,y>\left<i,x\right> \to \left<j,y\right><j,¬y><i,¬x>\left<j,\neg y\right> \to \left<i,\neg x\right>。为了方便,我们把 <i,x>\left<i,x\right> 能走到 <j,y>\left<j,y\right> 记为 <i,x><j,y>\left<i,x\right> \Rightarrow \left<j,y\right>,走不到记为 <i,x>⇏<j,y>\left<i,x\right> \not\Rightarrow \left<j,y\right>

则原问题可化为:在约束对应的图上,选择 nn 个点,使表示某变量为真与为假的点有且只有一个被选择,每个点若被选择,它能走到的节点也被选择。

2. 求解

怎么解呢?当然要请出我们的 爆搜 Tarjan 大佬。

首先我们发现,如果一个节点与它的非节点互相连通,那么问题肯定无解,因为如果 ai=1ai=0a_i=1\Rightarrow a_i=0ai=0ai=1a_i=0\Rightarrow a_i=1,那么就形成了逻辑矛盾。

然后我们发现,如果 <i,x><i,¬x>\left<i,x\right> \Rightarrow \left<i,\neg x\right><i,¬x>⇏<i,x>\left<i,\neg x\right> \not\Rightarrow \left<i,x\right>ai=¬xa_i=\neg x。(其实也就是反证法)

如果 <i,x>\left<i,x\right><i,¬x>\left<i,\neg x\right> 没有直接或间接关系,则 aia_i 可以随便取值。

因此我们考虑将图用 tarjan 把强连通分量缩点,得到一张 DAG。如果一个点和它的非节点在同一个强连通分量中,则问题无解,否则,aia_i 的取值就是 <i,1>\left<i,1\right><i,0>\left<i,0\right> 中缩点后拓扑序较小的节点,因为拓扑序较大的节点不可能走回拓扑序小的节点。

于是,我们得到了一组可行解。

code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// by juruo999
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>

using namespace std;

namespace sat{

const int maxn=1000006;
vector<int> e[maxn*2];

int n;
inline int rho(int i,bool x){ // <i,x>
return i*2-x;
}
void add(int i,bool a,int j,bool b){ // x[i]==a => x[j]==b
e[rho(i,a)].push_back(rho(j,b));
e[rho(j,b^1)].push_back(rho(i,a^1));
}

int dfn[maxn*2],low[maxn*2],vis[maxn*2],cnt=0;
int scc[maxn*2],sccn=0,stk[maxn*2],tp=0;
void dfs(int u){
low[u]=dfn[u]=++cnt;
stk[++tp]=u;
vis[u]=1;
for(int i=e[u].size()-1;i>=0;i--){
int v=e[u][i];
if(!dfn[v]){
dfs(v);
low[u]=min(low[u],low[v]);
}else if(vis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(low[u]==dfn[u]){
sccn++;
while(stk[tp]!=u){
vis[stk[tp]]=0;
scc[stk[tp]]=sccn;
tp--;
}
vis[stk[tp]]=0;
scc[stk[tp]]=sccn;
tp--;
}
}

bool x[maxn];
bool solve(){
for(int i=1;i<=2*n;i++){
if(!dfn[i]){ dfs(i); }
}
for(int i=1;i<=n;i++){
if(scc[rho(i,0)]==scc[rho(i,1)]){
return 0;
}else{
x[i]=scc[rho(i,0)]>scc[rho(i,1)];
}
}
return 1;
}

bool& get(int i){
return x[i];
}

}

using sat::e;
using sat::rho;

int n,m;

int main(){

scanf("%d%d",&n,&m);

sat::n=n;
for(int i=1;i<=m;i++){
int x,a,y,b;
scanf("%d%d%d%d",&x,&a,&y,&b);
sat::add(x,a^1,y,b);
}

if(sat::solve()){
puts("POSSIBLE");
for(int i=1;i<=n;i++){
printf("%d%c",sat::x[i]," \n"[i==n]);
}
}else{
puts("IMPOSSIBLE");
}

return 0;

}

例题

模板题
双倍经验
NOI2017 游戏

第三题有一些难度,我做这道题的时候一开始建模建错了,结果不能保证每个地图都能分配到赛车……


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!