2-SAT 学习笔记

djwj233

2021-08-19 21:49:02

Personal

### 2-SAT 是什么 2-SAT 的全称是 2-Satisfiability Problem,中文直译为「2 - 可满足性问题」。 一般地,2-SAT 的解法用以解决如下问题: > 已知 $n$ 个二元组和 $m$ 组二元关系 $(u_i,v_i)$。 > > 现在要在每个二元组中选出**恰好一个**元素构成一个集合,使得所有的 $(u_i,v_i)$ **不同时在集合中出现**。 > > 尝试构造一组合法选择。 2-SAT 可以被形式化地表述为一个更具普适性的问题: > 有 $n$ 个变量 $x_{1\cdots n}$。其中,一个变量 $x_i$ 的可能取值有且仅有 $x_{i,0}$ 和 $x_{i,1}$ 两种。 > > 有 $m$ 条约束条件,每条约束条件可被描述为: > > 若 $x_i$ 被赋值为 $x_{i,p}$,则 $x_j$ 必须被赋值为 $x_{j,q}$。 > > 求判断合法的值是否存在,若存在则构造一组合法的值。 事实上,2-SAT 可以被推广为 **$k$ - SAT** 问题($k\geq 2$)。在 $k$ - SAT中,每个变量的可能取值有 $k$ 种。 - 3-SAT 是 Karp 的 21 个 NPC 问题之一,一般可以用于对一个问题是 NP-Hard 的证明; - 而对一般的 $k>2$,$k$ - SAT 都是一个 NPC 问题; - SAT 问题是人类第一个已知的 NPC 问题。 ### 2-SAT 求解的主过程 有些地方提到可以用 爆搜 + 剪枝 的方式解决,不过个人觉得挺扯淡的。 不过注意:如果题目要求解**字典序最小**则必须用 DFS。 一般做法是使用 Tarjan 缩点的方式。 模板:[P4782 【模板】2-SAT 问题](https://www.luogu.com.cn/problem/P4782) 我们考虑建一张含有 $2n$ 个点的图,每个点表示一种赋值。 对于每个条件 $i,a,j,b$: - 从 $(i,\lnot a)$ 向 $(j,b)$ 连边; - 从 $(j,\lnot b)$ 向 $(i,a)$ 连边。 其中,连一条边 $u\to v$ 的意义是**如果选了 $u$ 就必须选 $v$**。 然后跑一遍 Tarjan 求出所有的 SCC,并检查所有的点: - 如果存在一个点 $x$ 使 $(x,0)$ 和 $(x,1)$ 在同一个 SCC 中则说明无解。 - 否则,**按在 Tarjan 中被求出的顺序**枚举所有的 SCC,若当前的 SCC 中**没有一个点的否定**被选择,则选择这个 SCC 中的所有点。 为什么要按这个顺序呢?实际上,Tarjan 中 SCC 被求出的顺序可以看作是一个拓扑序的倒序。 根据上面我们的建图,一个没有出边的点的否定一定有出边,而有了出边就会对别的点造成影响。 因此,我们应该尽可能地选择没有出边的点。依次类推可以归纳证明**选择一个拓扑序更大的点一定更优**。 检查部分的实现可以写得较为简单: ```c++ fo(i,1,n) if(col[i]==col[i+n]) { puts("IMPOSSIBLE"); exit(0); } puts("POSSIBLE"); fo(i,1,n) printf("%d ",col[i]>col[i+n]); puts(""); ``` 完整代码: ```c++ #include <bits/stdc++.h> using namespace std; #define fo(v,a,b) for(int v=a;v<=b;v++) #define fr(v,a,b) for(int v=a;v>=b;v--) #define il inline const int N=2000010,M=2000010; int n,m; int head[N],Next[M],ver[M],tot; void add_edge(int x,int y) { ver[++tot]=y; Next[tot]=head[x], head[x]=tot; } int dfn[N],low[N],cnt; int st[N],top; bool ins[N]; int col[N],cur; void tarjan(int x) { dfn[x]=low[x]=++cnt; st[++top]=x, ins[x]=true; for(int i=head[x];i;i=Next[i]) { int y=ver[i]; if(!dfn[y]) { tarjan(y); low[x]=min(low[x],low[y]); } else if(ins[y]) low[x]=min(low[x],low[y]); } if(low[x]==dfn[x]) { cur++; while(st[top]!=x) col[st[top]]=cur, ins[st[top]]=false, top--; col[st[top]]=cur, ins[st[top]]=false, top--; } } int main() { cin>>n>>m; fo(i,1,m) { int x,y,a,b; scanf("%d%d%d%d",&x,&a,&y,&b); add_edge(x+(a^1)*n,y+b*n); add_edge(y+(b^1)*n,x+a*n); } fo(i,1,2*n) if(!dfn[i]) tarjan(i); fo(i,1,n) if(col[i]==col[i+n]) { puts("IMPOSSIBLE"); exit(0); } puts("POSSIBLE"); fo(i,1,n) printf("%d ",col[i]>col[i+n]); puts(""); return 0; } ``` ### 例题 - [2018-2019 ACM-ICPC Asia Seoul Regional](https://codeforces.com/gym/101987) K TV Show Time > 简要题面:(摘自 OI-Wiki) > > 有 $k$ 盏灯,每盏灯是红色或者蓝色,但是初始的时候不知道灯的颜色。 > > 有 $n$ 个人,每个人选择 $3$ 盏灯并猜灯的颜色,一个人猜对两盏灯或以上的颜色就可以获得奖品。 > > 判断是否存在一个灯的着色方案使得每个人都能领奖,若有则输出一种灯的着色方案。 看到有多个变量,每个变量有 $2$ 种赋值,且赋值有约束时可以考虑 2-SAT。 为保证一个人领奖,我们需要在他猜错其中一盏灯时钦定他别的两盏灯必须猜对。 这样就可以 2-SAT 了。[Code](https://codeforces.com/gym/101987/submission/128734803) - [P3825 [NOI2017] 游戏](https://www.luogu.com.cn/problem/P3825) 我们注意到大多数的地图都只能使用 $2$ 种赛车,这启发我们使用 2-SAT。 但是还有 $d$ 幅所有赛车都能使用的地图,如果暴力枚举这 $d$ 幅地图的状态,复杂度将会达到 $\Theta(3^d(n+m))$,无法通过。 怎么优化呢?实际上我们对每幅 $x$ 地图只需枚举它是 $a$ 型或者 $b$ 型就可以了,这两种涵盖了所有三种赛车。 复杂度 $\Theta(2^d(n+m))$,可以通过,不过细节有点多。[Code](https://www.luogu.com.cn/record/58018165) 核心思想就是**不要尝试硬刚 NPC 问题**。