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;
}
posted @ 2025-02-21 20:46  hm2ns  阅读(24)  评论(0)    收藏  举报