【2-SAT初学+模板题讲解】POJ3683 Priest John's Busiest Day

什么是2-SAT?

SAT是适定性(Satisfiability)问题的简称 。一般形式为k-适定性问题,简称 k-SAT。

可以证明,当k>2时,k-SAT是NP完全的。因此一般讨论的是k=2的情况,即2-SAT问题。

我们通俗的说,就是给你n个变量ai,每个变量能且只能取0/1的值。同时给出若干条件,形式诸如(not)ai opt (not)aj=0/1
其中opt表示and,or,xor中的一种

而求解2-SAT的解就是求出满足所有限制的一组a

通俗点理解:

1、存在n组的元素,每组两个元素。

2、每组元素中,选择了其中一个元素,另外一个元素就不能被选择,两个必须选择一个。这两个元素记为a和!a(两个即为对立面,互补关系)。

3、该模型中的元素之间存在一些关系,且这些关系是对称的。(除非是同一组元素中的关系,这些关系限定了“必须选择”该组中的某一个元素,可能单独出现)

满足上述条件,要求在满足给定关系的情况下在每组元素中选出一个元素,但是选出的元素又是相容的的问题称为2-SAT问题。问是否存在即2-SAT判定问题,当然也可以求出一组可行解。


实现思路:

首先我们考虑将2-SAT问题往图论的方向靠,我们发现每个点要么取0,要么取1。因此对于ai,我们建两个点2i与2i+1分别表示ai取0和1,然后我们考虑建边来表示这些关系,我们令一条有向边的意义:a→b表示如果选择了a就必须选b。

若a和b冲突,即选a时不能选b,那么选a时必须选!b(因为不选b就必须选!b,这是一个2-SAT问题必须满足的条件),那么我们就连边<a,!b>。同样的道理,如果选了b,那么就不能选a,必须选!a,所以连边<b,!a>。这样的连边,显然是对称的。

实现方法:

对原图求一次强连通分量,然后看每组中的两个点是否属于同一个强连通分量,如果存在这种情况,那么无解。

然后对于缩点后的图G',我们将G'中所有边转置。进行拓扑排序。对于缩点后的所有点,我们先预处理求出所有冲突顶点。
例如缩点后Ai所在强连通分支的ID为id[ Ai] ,同理~Ai在 id[ ~Ai ]
所以冲突顶点conflict[ id[Ai] ]=conflict[ id[~Ai] ]同理conflict[ id[~Ai] ]=conflict[ id[Ai] ]

设缩点后有Nscc个点。然后对拓扑序进行染色,初始化所有点color均为未着色,顺序遍历得到的拓扑序列,对于未着色的点x,将x染成红色,同时将所有与x矛盾的点conflic[x]染成蓝色。

2-SAT的一组解就等价于所有缩点后点颜色为红色的点,也就是color[ id[i] ]=RED的所有点

资源分享:

网上找的很好的PPT
链接:https://pan.baidu.com/s/1ysYQhtRI2qT4gXYm1tUJ6Q             提取码:da1y 

参考博客
https://blog.csdn.net/u010126535/article/details/24192565
https://www.cnblogs.com/cjjsb/p/9771868.html

例题POJ3683

题意:
有一个小镇上只有一个牧师。现在有 n 场婚礼需要牧师去主持,每场婚礼的举行时间为 [Si,Ti] ,牧师主持的时间为Di。
但是牧师只能在 [Si,Si+Di] 或者 [Ti-Di,Ti] 这两个区间主持,问牧师是否能够主持所有婚礼,即所有主持时间不冲突,如果可以输出每个婚礼的举行时间。

解题方法:
①先利用上面的方法将所有冲突的关系建立有向边
②利用Tanjar缩点
③记录每个组中两个点的缩点情况
④反向建边和记录入度
⑤拓扑排序染色
⑥输出答案

代码:
 

///#include<bits/stdc++.h>
///#include<unordered_map>
///#include<unordered_set>
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<cmath>
#include<queue>
#include<set>
#include<stack>
#include<map>
#include<new>
#include<vector>
#define MT(a,b) memset(a,b,sizeof(a));
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const double pai=acos(-1.0);
const double E=2.718281828459;
const int mod=1e9+7;
const int INF=0x3f3f3f3f;

int n;
///时刻区间
struct node
{
    int l;
    int r;
} time[10005];
int change_time(int h,int m)
{
    ///时刻换算为分钟
    return h*60+m;
}
bool judge(node a,node b)
{
    ///判断两个区间是否重叠
    ///如果不重叠返回false
    return !(a.r<=b.l||a.l>=b.r) ;
}
///跑Tanjan用的邻接表
struct edge
{
    int e;
    int p;
} load[4000005];
int head[10005],sign;
void add_edge(int s,int e)
{
    load[++sign]=edge{e,head[s]};
    head[s]=sign;
}

///Tanjan模板
int dfn[10005],low[10005],t;
int stack_[10005],instack[10005],top;
int belong[10005],cnt;
void tanjan(int s)
{
    dfn[s]=low[s]=++t;
    stack_[++top]=s;
    instack[s]=1;
    for(int i=head[s]; i!=-1; i=load[i].p)
    {
        int e=load[i].e;
        if(!dfn[e])
        {
            tanjan(e);
            low[s]=min(low[s],low[e]);
        }
        else
        {
            if(instack[e])
                low[s]=min(low[s],dfn[e]);
        }
    }
    int now;
    if(low[s]==dfn[s])
    {
        cnt++;
        do
        {
            now=stack_[top--];
            instack[now]=0;
            belong[now]=cnt;
        }
        while(now!=s);
    }
}

int both[2005];///记录每个点的对立点(比如a和!a)

void solve()
{
    printf("YES\n");
    int color[10005];///记录拓扑排序的染色情况
    int in[10005];///判断入度
    memset(in,0,sizeof(in));
    memset(color,-1,sizeof(color));
    vector<int>q[10005];
    ///缩点,重新建边
    for(int i=2; i<=(n<<1^1); i++)
    {
        int s=belong[i];
        for(int j=head[i]; j!=-1; j=load[j].p)
        {
            int e=belong[load[j].e];
            if(s!=e)
            {
                q[e].push_back(s);///DAG转置,建立反边
                in[s]++;
            }
        }
    }
    queue<int>Q;
    for(int i=1; i<=cnt; i++)
        if(!in[i])
            Q.push(i);
    while(!Q.empty())
    {
        int s=Q.front();
        Q.pop();
        if(color[s]==-1)///如果没有染色
        {
            color[s]=1;     ///当前点染色为1
            color[both[s]]=0;///对立点染色为0
        }
        for(int i=0; i<q[s].size(); i++)
        {
            int e=q[s][i];
            if(--in[e]==0)
                Q.push(e);
        }
    }
    for(int i=1; i<=n; i++)
    {
        int j=i<<1;
        if(color[belong[j]]!=1)///如果(i<<1)没有被染色为1,则选择的时他的对立面
            j^=1;
        printf("%02d:%02d %02d:%02d\n",time[j].l/60,time[j].l%60,time[j].r/60,time[j].r%60);
    }
    return ;
}

void init()///初始化
{
    sign=t=top=cnt=0;
    for(int i=1; i<=(n*2+3); i++)
    {
        head[i]=-1;
        dfn[i]=low[i]=0;
        instack[i]=0;
    }
}

int main()
{
    ///用i*2和i*2+1表示i的两种情况
    while(scanf("%d",&n)!=EOF)
    {
        init();     ///初始化
        int sx,sy,ex,ey,add;    ///起始时间和结束时间,牧师举行仪式的时间
        int lt,rt;
        for(int i=1; i<=n; i++)
        {
            scanf("%d:%d %d:%d %d",&sx,&sy,&ex,&ey,&add);
            ///时间换算为分钟
            lt=change_time(sx,sy);
            rt=change_time(ex,ey);
            time[i<<1]=node{lt,lt+add};
            time[i<<1^1]=node{rt-add,rt};
        }
        for(int i=1; i<=n; i++)
        {
            for(int j=1; j<=n; j++)
            {
                if(i==j)
                    continue;
                ///建立约束条件
                if(judge(time[i<<1],time[j<<1]))
                    add_edge(i<<1,j<<1^1);
                if(judge(time[i<<1],time[j<<1^1]))
                    add_edge(i<<1,j<<1);
                if(judge(time[i<<1^1],time[j<<1]))
                    add_edge(i<<1^1,j<<1^1);
                if(judge(time[i<<1^1],time[j<<1^1]))
                    add_edge(i<<1^1,j<<1);
            }
        }
        for(int i=2; i<=(n<<1^1); i++)
            if(!dfn[i])
                tanjan(i);
        int flag=1;
        for(int i=1; i<=n; i++)
        {
            ///如果两个对立点在同一个强连通分量里,则不存在答案
            if(belong[i<<1]==belong[i<<1^1])
            {
                printf("NO\n");
                flag=0;
            }
            ///记录每个点的对立点
            both[belong[i<<1]]=belong[i<<1^1];
            both[belong[i<<1^1]]=belong[i<<1];
        }
        if(flag)    solve();
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值