2-SAT 算法
前言
似乎是很牛的算法,也可能是因为题目太难了
前置芝士:有向图中的 Tarjan 缩点
1. 是什么
SAT 问题是这样的:给你 \(n\) 个变量 \(a_i\),每个变量可以有 \(k\) 种取值。再给你一些限制,诸如:
- 如果 \(a_i=1\),那么 \(a_j\) 必须 \(\ne 2\)。
- 如果 \(a_k=3\),那么 \(a_l\) 必须 \(= 4\)。
- \(a_i=1\) 和 \(a_j=0\) 不能同时成立。
- \(a_x\) 必须是 \(5\)。
- \(a_y\) 必须不是 \(6\)。
给完这些限制后,问你存不存在一种对于每一个变量的赋值,使得它们都能满足这些限制。
你可能第一个想到的是搜索,没错,对于 \(k>2\) 的情况,只能搜索。因为这样的 SAT 问题被证明是 NP 完全的,什么是 NP 完全?
但是当 \(k=2\) 时,我们就可以通过转化成图论问题解决它。这是的 SAT 问题我们叫它 2-SAT。
\(k=1\)?那还用算吗
由于 \(k=2\),每个变量只能有两个取值,我们当然可以用 \(0,1\) 代表这两个取值。
2. 怎么解
首先我们把这些变量抽象成点,这 \(n\) 个点要拆成两份,(以下是作者自己的定义)其中一份我们叫它 正点,表示这些变量取到 \(1\) 的情况;另一份叫做 反点,表示这些变量取到 \(0\) 的情况。我们使用下标 \(1\le i \le n\) 表示变量 \(a_i\) 的正点,用 \(n+1\le j \le n\times 2\) 表示 \(a_i\) 的反点 \(a_j\)。根据我们的定义可以得出 \(a_i\) 和 \(a_{i+n}\) 分别表示变量 \(a_i\) 的正点和反点。
接下来是建图,我们使用有向边表示两个变量之间的关系:
如果关系是形如 "如果 \(a_i=1\),那么 \(a_j\) 必须 \(= 0\)",我们首先从表示 \(a_i=1\) 的点(也就是正点)连一条边到表示 \(a_k=0\) 的点(也就是反点)。代表如果选择让 \(a_i=1\) 那么 \(a_j\) 的取值也跟着确定为 \(0\)。
同理我们还要从 表示 \(a_j=1\) 的点连一条有向边到表示 \(a_i=0\) 的点。代表如果 \(a_j\) 选择值为 \(1\) 那么 \(a_i\) 就不可以再选 \(1\) 只能选 \(0\)。
如果关系是形如 "如果 \(a_i=1\),那么 \(a_j\) 必须 \(\ne 0\)",很显然可以转换成 "如果 \(a_i=1\),那么 \(a_j\) 必须 \(=1\)" 然后用相同的方法建边即可。
如果关系是形如 "\(a_i=1\) 和 \(a_j=0\) 不能同时成立",此时如果让 \(a_i=1\) 那么 \(a_j\) 跟着确定为 \(1\),相当于从 \(a_i=1\) 点连有向边到 \(a_j=1\) 点。反过来如果让 \(a_j=0\) 那么 \(a_i\) 确定为 \(0\),同理连边即可。
这样建完边以后,我们进行缩点。
很明显,缩点完成后在同一个强连通分量内的点的值一定是 “必须选” 的。
那么什么情况下会出现无解的情况?就是一个点的正点和反点同时 “必须选”,即在同一个 SCC 中。
上几张图可能会更好理解:
判断两个点是否在同一个 SCC 中很简单,我们把每个 SCC 给予一个颜色,把里面的点都染成这个颜色,判断颜色就可以了。
那么如何输出答案呢?
这个比较抽象,总而言之就是如果一个正点的 SCC 编号大于反点的,那么这个点是 \(1\),否则是 \(0\)。
由于只使用了一次 Tarjan 算法,时间复杂度 \(O(N+M)\)
3. 代码时间
代码可通过模板题 P4782 【模板】2-SAT。
#define rd read()
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll read(){
ll x=0,f=1;
char c=getchar();
while(c>'9'||c<'0'){if(c=='-') f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
const int N=2e6+5;
vector<int> edge[N];
int dfn[N],low[N],idx;
stack<int> dfr;
bool vis[N];
int col[N];
int scc;
vector<int> news[N];
void tarjan(int u){
dfn[u]=low[u]=++idx;
dfr.push(u);
vis[u]=1;
for(int i=0;i<edge[u].size();i++){
int v=edge[u][i];
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v]){
low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u]){
scc++;
while(dfr.top()!=u){
news[scc].push_back(dfr.top());
col[dfr.top()]=scc;
vis[dfr.top()]=0;
dfr.pop();
}
news[scc].push_back(dfr.top());
col[dfr.top()]=scc;
vis[dfr.top()]=0;
dfr.pop();
}
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v;
bool a,b;
cin>>u>>a>>v>>b;
if(a==0&&b==0){
edge[u+n].push_back(v);
edge[v+n].push_back(u);
}
else if(a==1&&b==0){
edge[u].push_back(v);
edge[v+n].push_back(u+n);
}
else if(a==0&&b==1){
edge[u+n].push_back(v+n);
edge[v].push_back(u);
}
else{//a==1 b==1
edge[u].push_back(v+n);
edge[v].push_back(u+n);
}
}
for(int i=1;i<=n*2;i++){
if(!dfn[i])
tarjan(i);
}
for(int i=1;i<=n;i++){
if(col[i]==col[i+n]){
cout<<"IMPOSSIBLE";
return 0;
}
}
cout<<"POSSIBLE"<<'\n';
for(int i=1;i<=n;i++){
if(col[i]>col[i+n]){
cout<<1<<' ';
}
else cout<<0<<' ';
}
return 0;
}