Live2D

只有2-SAT不是NPC呢——2-SAT学习笔记

\[{\Huge\text{2-SAT学习笔记}} \]


什么是2-SAT?

我们举一个简单的例子:

机房里有三位大佬小s,小l,小m和一个蒟蒻tqr,他们刷题时有不同的要求(因为蒟蒻什么题都不会做,故不列举):

大佬/要求 小s 小l 小m
要求1 不包含数论知识(¬a) 包含数论知识(a) 不包含数论知识(¬a)
要求2 包含几何知识(b) 包含几何知识(b) 不包含几何知识(¬b)
要求3 不包含图论知识(¬c) 不包含图论知识(¬c) 包含图论知识(c)

在此之前,复习一下前置数学知识:

对于一个命题:如果a,那么b

其逆命题为:如果b,那么a

其否命题为:如果¬a,那么¬b

其逆否命题为:如果¬b,那么¬a

∧表示与,∨表示或,¬表示非

原命题与逆否命题等值。逆命题与否命题等值

原命题与逆命题以及逆否命题与否命题之间没有关系

我们继续上面的情景,如果我们要出一道题,使这道题能同时满足上面三位dalao的喜好,该如何做?

经过数学分析,满足的题目一定符合以下条件(¬a∨b∨¬c)∧(a∨b∨¬c)∧(¬a∨¬b∨c)

因此我们要做的就是给每个变量赋值,使上式值为true

是的,这就是SAT问题,但需要注意的是,上面的情况中,每个同学对题目都有三个限制,因此是3-SAT问题

可证明的,3(及以上)-SAT问题都是NPC问题(即不可使用算法解决,唯一的方法是暴力枚举)

因此,2-SAT问题是算法能解决的极限,那么我们将限制改一改:

经过一段时间的学习后,除了tqr,其他人都完全掌握了图论知识……

一波操作过后,限制条件变成了(¬a∨b)∧(a∨b)∧(¬a∨¬b),这就是我们要学习的2-SAT了

那我们就开始吧……

如何解决2-SAT?

首先,我们需要将几个互相有限制的点赋值,那就把它们丢到图里面!

建立两个点,分别是a和¬a(储存时,可以存到编号分别为 \(i\)\(i+maxn\) 的节点上,就像并查集对称点一样)

那么每个点之间的关系是什么?

(a∨b)可以理解为若a为真,则b为假,反之b为真

于是我们可以建出图形:

¬a→ b∧¬b →a

由图形可以发现,a与¬b,以及¬a与b都在同一个强连通分量里,于是我们可以得出结论:

2-SAT问题同一个强连通分量中的所有元素值相同

很显然的,如果a与¬a(或者x与¬x,x代表任意条件)在同一个强连通分量里,即他们的值相等,那么出现了矛盾,可判断问题无解(就像1=-1一样,矛盾的等式)

解决一个实际问题

题目描述

有n个布尔变量 \(x_1\)~\(x_n\),另有 \(m\) 个需要满足的条件,每个条件的形式都是 \(x_i\)true/false\(x_j\)true/false。你的mubiao给每个变量赋值使得所有条件得到满足。

输入格式

第一行两个整数\(n\)\(m\),意义如题面所述

接下来\(m\)行每行\(4\)个整数\(i,a,j,b\),表示\(x_i\)\(a\)\(x_j\)\(b\)\((a,b∈\{0,1\})\)

输出格式

如无解,输出\(IMPOSSIBLE\);否则输出\(POSSIBLE\),

下一行\(n\)个整数\(x_1\)~\(x_n\)\((x_i∈\{0,1\})\),表示构造出的解。

很显然地,我们需要按照上述所说的来建边

read(n);read(m);
for(register int i=1;i<=m;++i)
{
	read(a),read(va),read(b),read(vb);
	add_edge(a+!va*n,b+vb*n);
	add_edge(b+!vb*n,a+va*n);
}

然后tarjan找强连通分量(color数组是拓扑序)

void tarjan(int u)
{
	low[u]=dfn[u]=++idx;
	sta.push(u);
	vis[u]=1;
	for(register int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		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])
	{
		for(tms++;!sta.empty();)
		{
			int x=sta.top();
			sta.pop();
			vis[x]=0;
			color[x]=tms;
			if(x==u) break;
		}
	}
}

for(register int i=1;i<=2*n;++i) if(!dfn[i]) tarjan(i);

如果强连通分量 \(x_0\), \(x_1\) 在缩点后 \(DAG\) 的底图中连通,那么我们选择的一定是拓扑序较大的

\(Tarjan\) 求强连通分量得到的强连通分量编号是遵循拓扑序逆序的,所以如果 \(x_0\) 的编号更小,输出 \(0\),否则输出 \(1\)

for(register int i=1;i<=n;++i)
    if(color[i]==color[i+n])
        {puts("IMPOSSIBLE");return 0;}	puts("POSSIBLE");
for(register int i=1;i<=n;++i)
	printf(color[i]>color[i+n]?"1 ":"0 ");

这道题就很轻松地解决了,下面是完整代码

#include<bits/stdc++.h>
using namespace std;

int n,m,a,va,b,vb;

struct Edge
{
	int next,to;
}edge[2000005];
int cnt=0,head[2000005];

inline void add_edge(int from,int to)
{
	edge[++cnt].next=head[from];
	edge[cnt].to=to;
	head[from]=cnt;
}

int low[2000005],dfn[2000005],color[2000005],tms,idx;
bool vis[2000005];
stack<int> sta;
void tarjan(int u)
{
	low[u]=dfn[u]=++idx;
	sta.push(u);
	vis[u]=1;
	for(register int i=head[u];i;i=edge[i].next)
	{
		int v=edge[i].to;
		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])
	{
		for(tms++;!sta.empty();)
		{
			int x=sta.top();
			sta.pop();
			vis[x]=0;
			color[x]=tms;
			if(x==u) break;
		}
	}
}

template<class T>inline void read(T &res)
{
	T flag=1;char c;
	while((c=getchar())<'0'||c>'9')if(c=='-')flag=-1;res=c-'0';
	while((c=getchar())>='0'&&c<='9')res=(res<<1)+(res<<3)+c-'0';res*=flag;
}

int main()
{
	read(n);read(m);
	for(register int i=1;i<=m;++i)
	{
		read(a),read(va),read(b),read(vb);
		add_edge(a+!va*n,b+vb*n);
		add_edge(b+!vb*n,a+va*n);
	}
	for(register int i=1;i<=2*n;++i) if(!dfn[i]) tarjan(i);
	for(register int i=1;i<=n;++i)
		if(color[i]==color[i+n]) {puts("IMPOSSIBLE");return 0;}
	puts("POSSIBLE");
	for(register int i=1;i<=n;++i)
		printf(color[i]>color[i+n]?"1 ":"0 ");
	return 0;
}

有什么不懂的可以发qq问我,没了

posted @ 2019-07-15 20:12  tqr06  阅读(1164)  评论(3编辑  收藏  举报