(笔记)2-SAT
前言:调死我了
2-SAT
算法内容
题意介绍
2-SAT,简单的说就是给出 \(n\) 个集合,每个集合有两个元素,已知若干个 \(\langle a,b \rangle\),表示 \(a\) 与 \(b\) 矛盾(其中 \(a\) 与 \(b\) 属于不同的集合)。然后从每个集合选择一个元素,判断能否一共选 \(n\) 个两两不矛盾的元素。显然可能有多种选择方案,一般题中只需要求出一种即可。
——摘自OI-wiki
与其说是一种算法,不如说是一种策略。2-SAT 问题是一类逻辑模型,即将逻辑关系建图可视化的过程。
基本策略
上文已经暗示过应建图解决,那么首先我们在图中引入如下概念:
命题\(p\),它的否定命题记为 \(\neg p\)。
逻辑关系\(p\rightarrow q\),即 \(p\) 推出 \(q\)。这里 \(p\) 是 \(q\) 的充分条件,\(q\) 是 \(p\) 的必要条件。
强联通分量\(A\),在建图时,我们将点作为命题,每个点有与之对应的一个否定命题,而用有向边表示命题之间的推导关系。强联通分量即一组两两之间可以互相到达的点集。
有了如上信息,我们很容易得到几个性质:
-
同一强联通分量的值相同,此值记录着当前命题是否成立。
-
一个命题 \(p\) 不可以和 \(\neg p\) 在同一强联通分量中,否则由 \(1\) 可得这个命题既真又假。
如题,模板中给出的限制是 \(x_i=a\space or\space y_i=b\),考虑将这样形式的限制记为一个命题,令 \(p\) 表示 \(x_i=a\),\(q\) 表示 \(y_i=b\) 。
题目给出的逻辑关系是或,因此不难想到,可以令 \(\neg p\rightarrow q\),\(\neg q\rightarrow p\),即当一个命题不成立时,另外一个命题必须至少是对的,这样就满足了所有限制。
输出方案方面,由于 Tarjan 缩点已经将强联通分量自动编号为逆拓扑序,所以只需要每个点判断其所在强联通分量编号是否大于否定命题所在编号即可。
代码贴贴
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5,M=2e6+5;
int n,m;
int head[N],idx;
struct Edge{
int v,next;
}adj[M<<1];
void ins(int x,int y){
adj[++idx].v=y;
adj[idx].next=head[x];
head[x]=idx;
}
int scc_cnt,dfn[N],sccno[N],low[N],tim;
int stk[N],tp;
void tarjan(int x){
stk[++tp]=x;
dfn[x]=low[x]=++tim;
for(int i=head[x];i;i=adj[i].next){
int v=adj[i].v;
if(!dfn[v]){
tarjan(v);
low[x]=min(low[x],low[v]);
}
else if(!sccno[v]){
low[x]=min(low[x],dfn[v]);
}
}
if(dfn[x]==low[x]){
scc_cnt++;
int tem=-1;
while(x!=tem){
tem=stk[tp--];
sccno[tem]=scc_cnt;
}
}
}
bool check(){
for(int i=1;i<=2*n;i++){
if(!dfn[i])tarjan(i);
}
for(int i=1;i<=n;i++)
if(sccno[i]==sccno[i+n])
return false;
return true;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
for(int k=1;k<=m;k++){
int i,a,j,b;
cin>>i>>a>>j>>b;
int nota=a^1,notb=b^1;
ins(i+nota*n,j+b*n);ins(j+notb*n,i+a*n);
}
if(check()){
cout<<"POSSIBLE\n";
for(int i=1;i<=n;i++){
cout<<(sccno[i]>sccno[i+n])<<' ';
}
}
else cout<<"IMPOSSIBLE";
return 0;
}
算法应用与技巧
-
构造问题,普遍比较灵活,需要自己构造命题,必要时加入枚举等完善。典型即P3825 [NOI2017] 游戏。
-
较为明显的限制性问题,一般为命题或问题或命题且问题,容易直接建模得出答案。
-
前后缀优化建图,这个很重要,值得拎出来单独讲一讲。
由于有时建图边数会多得不可思议,所以必须采用一定方法避免超空间与时间,而如果建这些边是一个点对同一点集内其他所有点连边,那就可以用到这个实用的小技巧。
令一组命题 \(p_1\),\(p_2\)……\(p_n\),且题目规定这组命题中必须恰有一个命题成立,假设 \(pre_i\) 表示前 \(i\) 个命题中是否有成立的,将 \(pre_i\) 作为虚点加入图中。
有几项显然的逻辑关系:
-
\(\neg pre_i\rightarrow \neg pre_{i-1}\),\(pre_{i-1}\rightarrow pre_i\)
-
\(p_i\rightarrow pre_i\),\(\neg p_i\rightarrow \neg pre_i\)
-
\(p_i\rightarrow \neg pre_{i-1}\),\(pre_{i-1}\rightarrow \neg p_i\)
通过在新图上跑 Tarjan,然后判断真点与其否定命题是否在同一强联通分量内,可以判断有无解。

浙公网安备 33010602011771号