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
";
for (int i=1;i<=n;i++)
{
if (color[i]<color[i+n])
{
cout<<0<<" ";
}
else
{
cout<<1<<" ";
}
}
return 0;
}
参考资料:
[1]
[2]LuoguP4782题解