2-SAT—学习笔记

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 表示Ai,0,结点 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+pn i + p ∗ n j+qn 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到ab+n到b
(比较玄学)

3. a and b=1 a   a n d   b = 1
这里要求必须a=b=1
连边a到a+nb到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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值