Q:
2-SAT的问题描述为
有n个变量,每个变量可取值0或1
给定m个限定条件
求是否存在一组合法的赋值方案
A:
为简化描述,记
Ai,0,Ai,1
A
i
,
0
,
A
i
,
1
分别表示第i个变量取值0和1
在2-SAT问题中
我们可以将所有限制条件转化为统一形式
若有
Ai
A
i
赋值为
Ai,p
A
i
,
p
,则变量
Aj
A
j
必须赋值为
Aj,q
A
j
,
q
对每个条件进行相应转化后
我们就可以通过图论解决上述问题
对每个变量
Ai
A
i
,我们把它拆成两个点
结点
i
i
表示,结点
i+n
i
+
n
表示
Ai,1
A
i
,
1
对每个条件若有
Ai
A
i
赋值为
Ai,p
A
i
,
p
,则变量
Aj
A
j
必须赋值为
Aj,q
A
j
,
q
我们由
i+p∗n
i
+
p
∗
n
向
j+q∗n
j
+
q
∗
n
连边
建完图后我们用tarjan求出所有强连通分量
若由
Ai,0
A
i
,
0
和
Ai,1
A
i
,
1
属于同一个强连通分量
表示若有
Ai
A
i
赋值为0,则变量
Ai
A
i
必须赋值为1
显然产生矛盾
说明在该组限制条件下不存在合法赋值方案
若不存在上述情况则可判定存在合法方案
上述是2-SAT的基本思想
那么对于各种情况的限制条件要如何进行转化呢
我们来看下面这道例题
POJ - 3678 Katu Puzzle
这道题中的限制条件都是形如
xa op xb=c
x
a
o
p
x
b
=
c
的算式
c取值0或1,op为OR或AND或XOR
1.
a or b=1
a
o
r
b
=
1
连边a到b+n,表示若a=0,则b必须等于1
连边b到a+n,表示若b=0,则a必须等于1
2.
a or b=0
a
o
r
b
=
0
该条件比较特殊,要求必须a=b=0
我们连边a+n到a和b+n到b
(比较玄学)
3.
a and b=1
a
a
n
d
b
=
1
这里要求必须a=b=1
连边a到a+n和b到b+n
4.
a and b=0
a
a
n
d
b
=
0
连边a+n到b,表示若a=1,则b必须等于0
连边b+n到a,表示若b=1,则a必须等于0
5.
a xor b=1
a
x
o
r
b
=
1
连边a到b+n,表示若a=0,则b必须等于1
连边b到a+n,表示若b=0,则a必须等于1
连边b+n到a,表示若b=1,则a必须等于0
连边a+n到b,表示若a=1,则b必须等于0
6.
a xor b=0
a
x
o
r
b
=
0
连边a到b,表示若a=0,则b必须等于0
连边b到a,表示若b=0,则a必须等于0
连边b+n到a+n,表示若b=1,则a必须等于1
连边a+n到b+n,表示若a=1,则b必须等于1
这里已经包括了所有限制条件可能的形式了
任何形式的条件都可以通过一定转换得到上述模型
#include<iostream>
#include<stack>
#include<algorithm>
#include<queue>
#include<cstring>
#include<cstdio>
using namespace std;
typedef long long lt;
lt read()
{
lt f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
const int maxn=4000;
int n,m;
struct node{int v,nxt;}E[4000010];
int head[maxn],tot;
int low[maxn],dfn[maxn],cnt;
int col[maxn],ins[maxn],colnum;
stack<int> st;
char ss[10];
void add(int u,int v)
{
E[++tot].nxt=head[u];
E[tot].v=v;
head[u]=tot;
}
void tarjan(int u)
{
low[u]=dfn[u]=++cnt;
st.push(u); ins[u]=1;
for(int i=head[u];i;i=E[i].nxt)
{
int v=E[i].v;
if(!dfn[v]){ tarjan(v); low[u]=min(low[u],low[v]);}
else if(ins[v])
low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
int v; colnum++;
do{
v=st.top();
st.pop(); ins[v]=0;
col[v]=colnum;
}
while(v!=u);
}
}
int main()
{
n=read();m=read();
for(int i=1;i<=m;++i)
{
int u=read()+1,v=read()+1,c=read();
scanf("%s",&ss);
if(ss[0]=='A')
{
if(c==0)add(u+n,v),add(v+n,u);
else if(c==1)add(u,u+n),add(v,v+n);
}
else if(ss[0]=='O')
{
if(c==1)add(u,v+n),add(v,u+n);
else if(c==0)add(u+n,u),add(v+n,v);
}
else if(ss[0]=='X')
{
if(c==1)add(u,v+n),add(v,u+n),add(v+n,u),add(u+n,v);
else if(c==0)add(u,v),add(v,u),add(u+n,v+n),add(v+n,u+n);
}
}
for(int i=1;i<=n<<1;++i)
if(!dfn[i])tarjan(i);
for(int i=1;i<=n;++i)
if(col[i]==col[i+n]){printf("NO");return 0;}
printf("YES");
return 0;
}
最后一个要讨论的问题是如何解决方案输出
tarjan求出强连通分量后
实际上得到的SCC编号是SCC缩点后得到新图的反向拓扑序
对于每个
i∈[1,n]
i
∈
[
1
,
n
]
我们只需比较
col[i],col[i+n]
c
o
l
[
i
]
,
c
o
l
[
i
+
n
]
的拓扑序大小
给i赋值为拓扑序较大的那个
for(int i=1;i<=n;++i)
printf("%d ",col[i]>col[i+n]);
特别注意tarjan求SCC后得到的是反向拓扑序
要比较的是拓扑序
可以拿这道模板题练练手
洛谷 P4782 【模板】2-SAT 问题
#include<iostream>
#include<stack>
#include<algorithm>
#include<queue>
#include<cstring>
#include<cstdio>
using namespace std;
typedef long long lt;
lt read()
{
lt f=1,x=0;
char ss=getchar();
while(ss<'0'||ss>'9'){if(ss=='-')f=-1;ss=getchar();}
while(ss>='0'&&ss<='9'){x=x*10+ss-'0';ss=getchar();}
return f*x;
}
const int maxn=2000010;
int n,m;
struct node{int v,nxt;}E[maxn<<1];
int head[maxn],tot;
int low[maxn],dfn[maxn],cnt;
int col[maxn],ins[maxn],colnum;
stack<int> st;
void add(int u,int v)
{
E[++tot].nxt=head[u];
E[tot].v=v;
head[u]=tot;
}
void tarjan(int u)
{
low[u]=dfn[u]=++cnt;
st.push(u); ins[u]=1;
for(int i=head[u];i;i=E[i].nxt)
{
int v=E[i].v;
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(ins[v])
low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
int v; colnum++;
do
{
v=st.top();
st.pop(); ins[v]=0;
col[v]=colnum;
}
while(v!=u);
}
}
int main()
{
n=read();m=read();
for(int i=1;i<=m;++i)
{
int x1=read(),v1=read(),x2=read(),v2=read();
add(x1+(v1^1)*n,x2+v2*n);
add(x2+(v2^1)*n,x1+v1*n);
}
for(int i=1;i<=n<<1;++i)
if(!dfn[i])tarjan(i);
for(int i=1;i<=n;++i)
if(col[i]==col[i+n]){printf("IMPOSSIBLE");return 0;}
printf("POSSIBLE\n");
for(int i=1;i<=n;++i)
printf("%d ",col[i]>col[i+n]);
return 0;
}