前置知识
强连通分量
例题引入
先来看一个问题:
给定\(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\))。
所以得到上述例题的做法:
- 编号\(i\)代表\(a_i\)取\(0\),\(i+n\)代表\(a_i\)取\(1\)
- 对于若\(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\)连边。 ( \(~\) 代表取反) - 求强连通分量,记录每个节点属于哪一个强连通分量。
- 很显然,属于同一个强连通分量的值都相同,\(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\)。
模板
对于六种情况分别讨论,
特别的,如果是\(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
通过时间是否重叠建图
这题有两种方法:
第一种是用\(0\)和\(1\)代表这个位置是\(h\)还是\(w\)。
第二种是让每个人都有两个取值\(0\)和\(1\),表示坐新郎这边还是坐新娘那边。
除去\(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;
}