2-SAT学习笔记

前置知识

强连通分量

例题引入

先来看一个问题:
给定\(n\)个元素,分别是\(a_1,a_2,...,a_n\)
每个\(a_i\)只有\(0\)\(1\)两种取值,再给定\(m\)个约束条件,
条件的形式都是 "若\(a_x\)\(p\),则\(a_y\)\(q\)" 其中\(p,q\) \(\in\) \(\{ {0,1} \}\)
请问是否有一组合法的\(a\)的取值满足以上条件

这就是\(2-SAT\)问题的经典模型。
\(2-SAT\)中有个十分重要的结论:

  • \(A\)\(B\);可以推出:若非\(B\),则非\(A\)
    即每个命题的逆否命题一定成立

例如:若\(a_1\)\(1\),则\(a_2\)\(0\)
那么:若\(a_2\)\(1\)(不为\(0\)),则\(a_1\)\(0\)(不为\(1\))。

所以得到上述例题的做法:

  1. 编号\(i\)代表\(a_i\)\(0\),\(i+n\)代表\(a_i\)\(1\)
  2. 对于若\(a_x\)\(p\),则\(a_y\)\(q\),那么让\(x+p*n\)\(y+q*n\)连边。
    那么我们得出\(a_y\)不为 \(q\),则\(a_x\)不为 \(p\)(逆否命题),那么让\(y+(\)~\(q) \times n\)\(x+(\)~\(p) \times n\)连边。 ( \(~\) 代表取反)
  3. 求强连通分量,记录每个节点属于哪一个强连通分量。
  4. 很显然,属于同一个强连通分量的值都相同,\(i\)不可能与\(i+n\)处于同一个强连通分量
    假如我们求出来\(c\)数组代表每个节点属于哪个强连通分量,
    如果对于任意的\(c_i\)\(c_{i+n}\)都不相同,那么\(2-SAT\)问题有解,否则无解。

时间复杂度\(O(V+E)\) (\(V\)\(E\)是建完图后的点数和边数)

性质

通过上述例题,我们发现,因为最先提出的两个命题是成对出现的,
所以我们建出来的图也是具有对称性的,所以建边时必须注意这个性质。
值得一提的是,如果一个元素的值是确定的,那么我们让\(i+n\)\(i\)\(i\)\(i+n\)连边,以便直接产生矛盾。

如何求出一组解

大部分\(2-SAT\)题目都需要输出一组可行解,其中有两种\(2-SAT\)问题的构造方法。
第一种:
因为同一\(SCC\)中的元素值相同,所以其中一个元素的值确定,其他元素的值也是相同的。
我们考虑缩点,把每个\(SCC\)看成一个点,缩点后的图就是一个\(DAG\)
接下来用拓扑排序遍历整个图。
每次我们需要找出没有出度的点,防止对其他点产生影响。
而通常拓扑排序都是寻找没有入度的点,所以我们需要对原图的反图进行拓扑排序。
代码大致实现:

void topsort(){
	queue<int>q;
	memset(now,tot=0,sizeof(now));
	//建反图 
	for(int i=1;i<=Ed;i++){
		int X=E[i].x,Y=E[i].y;
		if(X!=Y)add(Y,X),deg[X]++;
	}
	//cnt:SCC的个数 
	for(int i=1;i<=cnt;i++)
		if(!deg[i])q.push(i);
	//拓扑排序 
	while(q.size()){
		int u=q.front();q.pop();
		//检查是否已经被赋值
		//opp[u] : 第u个SSC 与它不同的 SSC 
		if(!val[u]){
			val[u]=1;
			val[opp[u]]=2;
		} 
		for(int i=now[u];i;i=pre[i])
			if(!--deg[to[i]])q.push(v);
	}
	//输出一组解 
	for(int i=1;i<=n;i++)
		if(val[c[i]]==1)printf("1 ");
		else printf("0 ");
}

第二种:
事实上,\(tarjan\)算法求出来的每个\(SCC\)的编号就是缩点后图的拓扑序。
我们可以直接利用\(SCC\)编号的大小关系确定元素的值,这使得过程非常简单。
代码:

for(int i=1;i<=n;i++)
    if(c[i]<c[i+n])printf("1 ");
    else printf("0 ");

习题

模板

尝试转换成\(2-SAT\)的限制条件。
很显然,\(x\)\(p\)\(y\)\(q\) \(~~~\) 转换成 \(~~~\) \(y\)不为\(q\)\(x\)\(p\)

[POI2001]Peaceful Commission

模板

poj3678 Katu Puzzle

对于六种情况分别讨论,
特别的,如果是\(a\) \(and\) \(b\) \(=\) \(1\)\(a\) \(or\) \(b\) = \(0\),那么\(a,b\)的值是确定的。
那么需要让\(a+n\)\(a\)\(a\)\(a+n\)连边,\(b+n\)\(b\)\(b\)\(b+n\)连边。

poj3683 Priest John's Busiset Day

通过时间是否重叠建图

poj3648Weeding

这题有两种方法:
第一种是用\(0\)\(1\)代表这个位置是\(h\)还是\(w\)
第二种是让每个人都有两个取值\(0\)\(1\),表示坐新郎这边还是坐新娘那边。

NOI2017游戏

除去\(x\)类图,就是最简单的\(2-SAT\)
鉴于\(d \leqslant 8\)我们想到直接暴力枚举\(x\);
\(O(3^d)\)枚举每个 \(x\)类型仍然会超时。
我们可以\(O(2^d)\)枚举不适合哪两种车,那么三种车都能包含到。

模板

#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int n,m,cnt;
int pre[N],now[N],to[N],tot;
int sk[N],top;
int low[N],dfn[N],num,c[N];
bool vis[N];
void add(int x,int y){
	pre[++tot]=now[x];
	now[x]=tot;to[tot]=y;
}
void tarjan(int u){
	low[u]=dfn[u]=++num;
	vis[sk[++top]=u]=true;
	for(int i=now[u];i;i=pre[i]){
		int v=to[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]){
		int v;cnt++;
		do{
			v=sk[top--];
			vis[v]=false;
			c[v]=cnt;
		}while(u!=v);
	}return;
}
int main(){
	scanf("%d%d",&n,&m);
	while(m--){
		int i,j,a,b;
		scanf("%d%d%d%d",&i,&a,&j,&b);
		if(a&&b)add(i+n,j),add(j+n,i);
		if(!a&&!b)add(i,j+n),add(j,i+n);
		if(!a&&b)add(i,j),add(j+n,i+n);
		if(a&&!b)add(i+n,j+n),add(j,i);
	}
	for(int i=1;i<=2*n;i++)
		if(!dfn[i])tarjan(i);
	for(int i=1;i<=n;i++){
		if(c[i]==c[i+n]){
			puts("IMPOSSIBLE");
			return 0;
		}
	}
	puts("POSSIBLE");
	for(int i=1;i<=n;i++)
		printf("%d ",(c[i]<c[i+n])?1:0);
	return 0;
}
posted @ 2021-02-21 16:54  Isenthalpic  阅读(87)  评论(0编辑  收藏  举报