2-SAT 学习笔记
一、SAT 问题简介
在日常的学习生活中,我们经常遇到如下问题:
在一场同学聚会上,你打算邀请 \(n\) 对 \(cp\) 参加,第 \(i\) 对 \(cp\) 的编号为 \((i,i+n)\)。但是,有一些人可能有冲突,有冲突的人不能同时参加聚会。你在每对 \(cp\) 都要挑选恰好一人参加聚会,判断是否有方案能满足上述所有条件。
对于这类问题,我们可以将 \(x,y\) 两人有冲突,理解为布尔方程“\(x\) 为假 \(y\) 为假”。假如一个布尔方程中有 \(k\) 个要求,那么就称他为 \(k-SAT\) 问题。假如像刚才举出的例子一样,只有两个要求,那就称他为 \(2-SAT\) 问题。\(k>2\) 的情况下,已经证明该问题为 \(NP\) 完全问题。因此通常只讨论 \(k=2\) 的情况。
二、2-SAT 问题
考虑上述布尔方程的逻辑推理关系。
例如,“\(x\) 为假或 \(y\) 为假”这个布尔方程,他等价于“若 \(x\) 为真,则 \(y\) 为假;若 \(y\) 为真,则 \(x\) 为假”。这感觉跟建了条边似的,所以我们建出 \(x\to !y,y\to !x\) 这两条边。其中 \(x\) 表示“\(x\) 为真”,\(!x\) 表示“\(x\) 为假”。这样,假如有一个状态 \(a\),它可以通过走有向边走到状态 \(b\),那就意味着若最终答案中有状态 \(a\),则必有状态 \(b\)。
那么,这张有向图该如何解决上述问题呢?
在这个问题中,我们可以将邀请 \(i\) 作为真,将邀请 \(i+n\) 作为假,这样就可以根据上述布尔方程将这张图 \(G\) 建立出来。
根据反证法,只要一对 \(cp\) 不会出现“\(i\) 参加则 \(i+n\) 也参加,\(i+n\) 参加则 \(i\) 也参加”这种情况,那么我们就一定能找出一个满足要求的解。而出现上述情况,当且仅当 \(i\) 和 \(i+n\) 处在同一个强连通分量中。因此我们直接对 \(G\) 进行 \(SCC\) 缩点即可。
三、2-SAT 问题求特解
当然,\(2-SAT\) 问题的可玩性远不止于此,我们还可以根据 \(tarjan\) 的过程量求出一组特解。
考虑两点 \(i,i+n\) 所在的强连通分量缩成的点 \(x,y\),一共只有两种位置关系:
- \(x\) 能到达 \(y\) 或 \(y\) 能到达 \(x\)。不妨设情况为第一种,这种情况下,若选择 \(x\),则可以根据 \(x\) 推出 \(y\)。所以这种情况下,一定选能被到达的点,而被到达的点的拓扑序一定更大。
- 两点彼此不能到达。那么二者互不影响,选谁都行。
而在一般的 \(tarjan\) 中,拓扑序越大的强连通分量编号越小,所以我们只需要在 \(x,y\) 中,选择编号更小的就可以了。
下给出模板题代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e6+5;
int n,m,cnt,id,dfn[N],low[N];
int tp,st[N],vist[N],idx[N];
vector<int>g[N];int ans[N];
void tarjan(int x){
st[++tp]=x,vist[x]=1;
dfn[x]=low[x]=++id;
for(auto y:g[x]){
if(!dfn[y]) tarjan(y),low[x]=min(low[x],low[y]);
else if(vist[y]) low[x]=min(low[x],dfn[y]);
}if(dfn[x]!=low[x]) return;
while(st[tp+1]!=x){
idx[st[tp]]=cnt;
vist[st[tp--]]=0;
}++cnt;
}void two_sat(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int x,a,y,b;cin>>x>>a>>y>>b;
g[x+a*n].push_back(y+(b^1)*n);
g[y+b*n].push_back(x+(a^1)*n);
}for(int i=1;i<=n*2;i++)
if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++){
if(idx[i]==idx[i+n])
return cout<<"IMPOSSIBLE",void();
ans[i]=(idx[i]<idx[i+n]);
}cout<<"POSSIBLE\n";
for(int i=1;i<=n;i++)
cout<<ans[i]<<" ";
}signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
return two_sat(),0;
}
四、2-SAT 问题求选择一状态后导致结果
这类题最经典的譬如 [JSOI2019] 精准预测。题目可能会问你:假如我第 \(x\) 个值选了 \(0/1\),那么第 \(y\) 个值会选啥?
这一类问题就要充分利用 \(SCC\) 缩点后的 \(DAG\) 图了。我们可以在这张图上进行(类)\(dp\) 操作(你问我为啥是类 \(dp\)?因为你还可以合并 \(bitset\) 这类数据结构……)。这样就可以直接查询了。
五、常用小 \(trick\)
- 可以将某些多要求的布尔方程通过暴力枚举转化成多种 \(2-SAT\) 情况([NOI2017] 游戏)。
- 假如用 \(2-SAT\) 分出两个集合,可以选择求出特解后通过交换元素的方式得到其他解([POI2011] Conspiracy)。

浙公网安备 33010602011771号