轻松处理条件矛盾——浅析 2-SAT

轻松处理条件矛盾——浅析 2-SAT

模板题链接 【模板】2-SAT 问题 - 洛谷

首先,什么是 2-SAT ?

我们考虑一个问题。(题意转化自[JSOI2010] 满汉全席

你是一位厨师,现在你的餐厅来了m个客人,你有n种材料,每种材料都可以做出两种菜系的美食。每个客人都有一定的需求,他们都点了两道菜品,并且必须吃到一样符合的菜品才会满足。为了满足客人,你需要把n种材料做成不同的美食,但是你嫌麻烦,不想反复做菜,于是做成了一大锅菜,导致每种材料只能做一种菜品。那么有没有一种方案能在能偷懒的条件满足所有的客人呢?

假设第一种菜系为H,第二种菜系为M。

假设你有三种材料,来了四个客人,现在第一个客人点了第一种材做的M系菜品(简称M1)与第二种材料做的M系菜品(简称M2),第二个客人点了M3和H1,第三个客人点了H1和H3,第四个点了H3和M2。

如何偷懒呢?选材料一做H菜系,这样就满足了二三号顾客的需求,再用材料二做M菜系,这样就满足了一四号顾客的需求,这样我们既完成了任务,又成功偷懒了。

以上的问题,就是一个经典的 2-SAT 问题。

2-SAT,即two-satisfiability,通俗来说,就是给出n个集合,每个集合有两个元素,已知若干个<a,b>,表示a与b矛盾(其中a与b属于不同的集合)。然后从每个集合选择一个元素,判断能否一共选n个两两不矛盾的元素。显然可能有多种选择方案,一般题中只需要求出一种即可。(定义摘自oi-wiki)

那么如何解决这个问题呢?

这里我介绍的是 Tarjan-缩点 算法。

为什么会想到建边跑强联通呢?

我们考虑对于一个条件,如果要满足这个条件,则必须满足另一个条件。

即选 X —> Y,如果我们当成路径来看,走X这条路,必然会走Y这条路,这不就是是一个有向边吗?

如果选了 X 后不能选 Z,那么X这条路径上,一定不能经过Z,如果经过了Z,那么就不符合题意了。所以我们就想到,要判断X与Z是否存在同一个集合中,若存在,在不符合,不存在则符合。

这样的问题,不就是一个强联通分量的问题吗?所以我们缩点,缩完后检测X与Z是否被缩成了一个点,就可以判断是否存在方案了。

关于强联通缩点问题,可以去看【模板】缩点 - 洛谷,缩点的模板。看完之后再回来做这类题。

//一般的tarjan
void tarjan(int x){
    s[++top]=x;
    vis[x]=true;
    dfn[x]=low[x]=++cnt;
    for(int i=head[x];i;i=e[i].nex){
        int v=e[i].v;
        if(!dfn[v]){
            tarjan(v);
            low[x]=min(low[x],low[v]);
        }
        else if(vis[v]){
            low[x]=min(low[x],dfn[v]);
        }
    }
    if(dfn[x]==low[x]){//一般的缩点
        int c;cnts++;
        do{
            c=s[top--],vis[c]=0;
            bel[c]=cnts;
        }while(c^x);
    }
}

P4782的输入,有点奇怪,这里直接附上代码。

scanf("%d%d%d%d",&x,&x1,&y,&y1);
if(x1&&y1)add(x+n,y),add(y+n,x);    
if(!x1&&y1)add(x,y),add(y+n,x+n);   
if(x1&&!y1)add(x+n,y+n),add(y,x);   
if(!x1&&!y1)add(x,y+n),add(y,x+n);

其实可以去看第一篇题解理解题意。对于不同的 2-SAT 问题,其建图的方式也有所不同,这点可以自己下来刷题体会一下。

P4782完整代码:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=2e6+10;
struct node{
    int v,nex;
}e[N];
int head[N],tote=0;
void add(int u,int v){
    e[++tote].v=v,e[tote].nex=head[u],head[u]=tote;
}
int bel[N];
int top=0,cnt=0,cnts=0;
int dfn[N],low[N];
bool vis[N];
int s[N];
void tarjan(int x){
    s[++top]=x;
    vis[x]=true;
    dfn[x]=low[x]=++cnt;
    for(int i=head[x];i;i=e[i].nex){
        int v=e[i].v;
        if(!dfn[v]){
            tarjan(v);
            low[x]=min(low[x],low[v]);
        }
        else if(vis[v]){
            low[x]=min(low[x],dfn[v]);
        }
    }
    if(dfn[x]==low[x]){
        int c;cnts++;
        do{
            c=s[top--],vis[c]=0;
            bel[c]=cnts;
        }while(c^x);
    }
}
int n,m;
int x,y,x1,y1; 
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        scanf("%d%d%d%d",&x,&x1,&y,&y1);
        if(x1&&y1)add(x+n,y),add(y+n,x);    
        if(!x1&&y1)add(x,y),add(y+n,x+n);   
        if(x1&&!y1)add(x+n,y+n),add(y,x);   
        if(!x1&&!y1)add(x,y+n),add(y,x+n);  
    }
    for(int i=1;i<=2*n;i++)if(!dfn[i])tarjan(i);//记住是2*n!!!!!
    for(int i=1;i<=n;i++){
        if(bel[i]==bel[i+n]){
            puts("IMPOSSIBLE");
            return 0;
        }
    }
    puts("POSSIBLE");
    for(int i=1;i<=n;i++){
        if(bel[i]<bel[i+n])printf("1 ");
        else printf("0 ");
    }
    return 0;
} 

例题:

满汉全席

和平委员

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值