【学习笔记】2-SAT 小结
2-SAT 小结
从SAT说起
布尔可满足性问题(Boolean satisfiability problem;SAT))属于决定性问题,也是第一个被证明属于NP完全的问题。 此问题在计算机科学上许多领域的皆相当重要,包括计算机科学基础理论、算法、人工智能、硬件设计等等。---摘自某百科
如:有三个bool变量,三个人要求的取值如下
A:101
B:110
C:111
如果三个人要被满足至少一个取值,求方案。
虽然SAT是NP完全问题,不过今天介绍的\(2-SAT\)是特殊的,其可以在\(O(n+m)\)的时间完成。
前置知识:tarjan(强联通分量),一定的逻辑思维
部分定义:
取值:本文中一个变量的取值有且只有\(0\)和\(1\),即\(bool\)变量
\(tarjan\)编号:指求强联通分量时,求出的强联通分量编号。
热身
- 请思考一下,如果给一条类似"a选1或b选0"这种约束,那么能推出什么?
请注意:这里的或指的是逻辑或,即两者至少满足一条,而非二选一。
显然,我们可以得出
1:如果a选0,则b必须选1
2:如果b选1,则a必须选1
- 如果是"a如果选1则b必须选0呢"?
1:如果a选1,则b必须选0
2:如果b选1,则a必须选0 - 如果是"a必须选1"呢?
这个留给读者自行思考
建边
我们可以通过建边将其表达出来。\((u,v)\)的意义是,假设选择了\(u\),则必须选\(v\)。那么我们就可以按照上面所述进行建边。
我们将每个变量拆成两点,对于\(1 \le i \le n\),\(i\)的意义是第\(i\)个变量选\(0\),\(i+n\)为选\(1\)。
那么,我们就得到一张完整的有向图了。
我们可以发现:对于一个环,只要选定一个点,那么其他点也随之确定。所以,我们可以通过强联通分量,使其成为一张有向无环图!
求解
假设\(i\)与\(i+n\)在同一个强联通分量内,则表示“假设第\(i\)个变量选0,那么就要选\(1\)”(或反过来),这时一定无解。
(那么此时上面的谜底也出来了,如果\(i\)必须为\(0\),则建边\((i+n,i)\),如果必须选\(i=1\),则无解)
那么我们怎么输出解呢?
这时候我们请出拓扑序。如果对于两个点\(u,v\),假设\(u\)的拓扑序小于\(v\),那么没有\(v\)没有到\(u\)的路径。证明略。
所以,假设对于\(i\),选拓扑序小的可能会出问题,所以应选拓扑序大的。
Q:拓扑好麻烦啊,能否不拓扑啊qwq
A:可以!考虑直接使用\(tarjan\)寻强联通分量时留下来的编号,因为\(tarjan\)寻环是\(dfs\)下去的,与拓扑恰恰相反,所以,我们应选\(tarjan\)编号小的。
Q:讲了这么多,上代码吧~
代码
这题只有一个约束,即“或”约束,按上文所写即可。
#include<bits/stdc++.h>
using namespace std;
int ltk,cc,to[2000500],net[2000500],fr[2005000],dfn[2000500],low[2005000],snack[2005000],color[2005000];
int df,p,u,v,u1,v1,n,m;bool vis[2000500];
void addedge(int u,int v)
{
cc++;
to[cc]=v;net[cc]=fr[u];fr[u]=cc;
}
void tarjan(int x)
{
dfn[x]=low[x]=++df;
snack[++p]=x;vis[x]=true;
for (int i=fr[x];i;i=net[i])
{
int y=to[i];
if (!dfn[y])
{
tarjan(y);
low[x]=min(low[x],low[y]);
}
else
if (vis[y]) low[x]=min(low[x],dfn[y]);
}
if (dfn[x]==low[x])
{
ltk++;
while (snack[p]!=x)
{
color[snack[p]]=ltk;vis[snack[p]]=false;
p--;
}
color[x]=ltk;vis[snack[p]]=false;
p--;
}
}//tarjan
int main()
{
cin>>n>>m;
for (int i=0;i<m;i++)
{
scanf("%d%d%d%d",&u,&u1,&v,&v1);
addedge(u+(u1^1)*n,v+v1*n);//位运算,意义与上文相同
addedge(v+(v1^1)*n,u+u1*n);
}
for (int i =1;i<=2*n;i++)
{
p=0;
if (!dfn[i])
tarjan(i);
}
for (int i=1;i<=n;i++)
{
if (color[i]==color[i+n])
{
cout<<"IMPOSSIBLE";
return 0;
}
}
cout<<"POSSIBLE\n";
for (int i=1;i<=n;i++)
{
if (color[i]<color[i+n])
{
cout<<0<<" ";
}
else
{
cout<<1<<" ";
}
}
return 0;
}
参考资料:
[1]
[2]LuoguP4782题解