浅析2-sat

2-sat

简介:

  • sat问题(适定性(Sat**isfiability)问题)——给定一个布尔方程,求出这个方程的解**.
  • 若有 K K 个变量,则称为K-sat问题,故 K=2 K = 2 时,即为2-sat问题…
  • 关于它的解法,常用的有两种——(dfs,tarjan+拓扑).

常规操作:

  • 在满足布尔方程的性质——一个变量为1或0——下,我们可以建出一个满足对称性的类似于二分图的图.(具体的连边看条件)
  • 在审题中,你要准确定义出一个变量的1和0分别表示什么含义,并且满足这个两个定义也是满足对称性的,进而也能满足图的对称性.

建图模型:

模型一: (A,B) ( A , B ) 不能同时取.
对于 A A ,若被“选择”,则只能选 B B ′ ,即建边 A>B A − > B ′ ;
对于 B B ,若被“选择”,则只能选 A A ′ ,即建边 B>A B − > A ′ ;
模型二: (A,B) ( A , B ) 不能同时不取.
对于 A A ′ ,若被“选择”,则只能选 B B ,即建边 A>B A ′ − > B ;
对于 B B ′ ,若被“选择”,则只能选 A A ,即建边 B>A B ′ − > A ;
模型三: (A,B) ( A , B ) 要么都取,要么都不取.
对于 A A ,若被“选择”,则只能选 B B ,即建边 A>B A − > B ;
对于 B B ,若被“选择”,则只能选 A A ,即建边 B>A B − > A ;
模型四: (A,A) ( A , A ′ ) 必取A.
对于 A A ′ ,若被“选择”,则只能选 A A ,即建边 A>A A ′ − > A ;

解法1:dfs

概念&原理: 类似二分图的方式,一一去匹配,但这里要通过栈进行记录,在匹配失败后便于还原.故复杂度为 Θ(nm) Θ ( n ∗ m ) .

code(模板):


int qwq,head[N];
struct edge{
    int to,next;
}E[N<<1];
void addedge(int x,int y){E[qwq]=(edge){y,head[x]};head[x]=qwq++};

int stk[N],top;
bool vis[N];

bool dfs(int x){
    if(vis[x^1])return 0;//对立面已被选,匹配失败 
    if(vis[x])return 1;//已匹配过
    vis[x]=1;
    stk[top++]=x; 
    for(int i=head[x];~i;i=E[i].next) if(!dfs(E[i].to)) return 0;//匹配是败 
    return 1;//匹配成功 
}

bool check(){
    mcl(vis,0);
    SREP(i,0,n){
        if(vis[i<<1] || vis[i<<1|1])continue;
        top=0;
        if(!dfs(i<<1)){//当前这一面匹配失败 
            while(top)vis[stk[--top]]=0;//还原 
            if(!dfs(i<<1|1))return 0;//该面的对立面也匹配失败,故无解 
        }
    }
    return 1;
}

void Clear(){
    qwq=0;
    mcl(head,-1); 
}

int main(){

    scanf("%d%d",&n,&m);

    Clear();

    REP(i,1,m){//条件 
        int a,b;
        scanf("%d%d",&a,&b);
        //对立面 
        int x1=a<<1,x0=a<<1|1; 
        int y1=b<<1,y0=b<<1|1;
        /* 根据题意建边 addedge(x1,y0); addedge(y1,x0); */ 
    }

    if(!check())puts("-1");//无解 
    else SREP(i,0,n) printf("%d\n",vis[i<<1]?1:0);//vis即为方案(解)
    return 0;
}

解法2:tarjan+拓扑

概念&原理: 通过tarjan进行缩点,就是满足条件的点为集合,在通过建反图来跑拓扑求方案(解).
解释:

  1. 为什么建反图:考虑到算法传递标记的时候,“选择”标记是没有进行传递的,也就是说正向边是没有利用到的,传递的都是“不选择”标记,也就是说走的都是反边,故而建反图.故复杂度为 Θ(n) Θ ( n ) .
  2. 为什么用拓扑:若不运用拓扑来选择集合的话,当访问到集合 A A 时,发现其无标记,于是把它标记为可选择,然后把 A A ′ ( A A 的对立面)标记为不可选择,then进行标记传递,那么进行“选择”标记传递,则集合 B B B B ′ 将被标记为选择,若进行“不选择”标记传递,则B和B’将被标记为不选择,这就产生矛盾了.所以,正确的方法应该是按照拓扑顺序,从影响小的开始抉择,在转成反边后, A A ′ 会处在拓扑序列的队首,若将 A A ′ 标记为选择,则 A A 标记为不选择,都不能传递,再将 B B 标记为选择, B B ′ 标记为不选择,当然,将 B B 标记为不选择, B B ′ 标记为选择也是可以的.

code(模板):


int qwq,head[N];
struct edge{
    int to,next;
}E[N<<1];
void addedge(int x,int y){E[qwq]=(edge){y,head[x]};head[x]=qwq++};

int dfn[N],low[N],tim;
int stk[N],top;
int Id[N],tot;
bool vis[N];

vector<int>G[N];
int In[N],mark[N],rev[N];
queue<int>Q;

void tarjan(int x){
    dfn[x]=low[x]=++tim;
    stk[++top]=x;
    vis[x]=1;

    for(int i=head[x];~i;i=E[i].next){
        int y=E[i].to;
        if(!dfn[y]){
            tarjan(y);
            chkmi(low[x],low[y]);
        }
        else if(vis[y]) chmin(low[x],dfn[y]);
    }

    if(dfn[x]==low[x]){//缩点 
        tot++;
        do{
            Id[stk[top]]=tot;
            vis[stk[top]]=0; 
        }while(x!=stk[top--]);
    }
}

void Build(){
    SREP(x,0,n<<1){
        for(int i=head[x];~i;i=E[i].next){
            int y=E[i].to;
            if(Id[x]!=Id[y])G[Id[y]].pb(Id[x]),In[Id[x]]++;//建反边 
        }
    }
    SREP(i,0,n) rev[Id[i<<1]]=Id[i<<1|1],rev[Id[i<<1|1]]=Id[i<<1];//记录对立面 
}

void Tuopu(){
    while(!Q.empty())Q.pop();
    REP(i,1,tot) if(!In[i]) Q.push(i);

    while(!Q.empty()){
        int x=Q.front();Q.pop();
        if(!mark[x])mark[x]=1,mark[rev[x]]=2;//解 
        SREP(i,0,G[x].size()){
            int y=G[x][i];
            In[y]--;
            if(!In[y])Q.push(y);
        } 
    }
}

void Clear(){
    qwq=0;
    mcl(head,-1);

    tot=top=tim=0;
    mcl(dfn,0);
    SREP(i,0,n<<1)G[i].clear();
    mcl(In,0);
    mcl(mark,0);
}

int main(){

    scanf("%d%d",&n,&m);

    Clear(); 

    REP(i,1,m){//条件 
        int a,b;
        scanf("%d%d",&a,&b);
        //对立面 
        int x1=a<<1,x0=a<<1|1; 
        int y1=b<<1,y0=b<<1|1;
        /* 根据题意建边 addedge(x1,y0); addedge(y1,x0); */ 
    }

    SREP(i,0,n<<1) if(!dfn[i]) tarjan(i);

    bool f=1;
    SREP(i,0,n) if(Id[i<<1]==Id[i<<1|1]){f=0;break;}

    if(!f)puts("-1");//无解
    else {
        Build();//建反图 
        Tuopu();//跑拓扑 
        SREP(i,0,n) printf("%d\n",mark[Id[i<<1]]==1?0:1);
    } 

    return 0;
}

总结:

2-sat一般的问题都是求解,一般也是二分答案再建图来判断是否存在解.

点赞