2-SAT(POJ3683)

前置技能

Tarjan

定义

K-SAT:给你许多个集合,每个集合都有k个元素。同时又有一些限制(如取x则不能取y),你必须在所有集合中取且仅能取一个元素,求合法方案。
2-SAT:当K取2时的情况。

为什么只考虑K=2的情况呢?
因为当K>2时,已经被证明是一个NPC问题。

算法实现

几个前提

建x->y的边表示选了x就必须选y。在同一集合的元素编号为x,x^1。
建图时,如果x与y有冲突,那么就建x->y^1和y->x^1的边
对称性:如果x能到y,则y^1必然能到x^1。

暴力求解

我们可以对每一个元素进行枚举判断,如果出现了既选x又选x^1的情况,那么就是不合法的。一直枚举到出现解为止。
这种方法可以解决求字典序最小解的情况,时间复杂度为 O(nm)

例题及题解:HDU1841

2-SAT

不难发现,当一个点被选择,这个点所在的强连通分量全部要被选择。因此我们可以进行Tarjan缩点。如果发现x和x^1在一个强连通分量内,则说明无解。

现在我们需要证明的就是:当没有上述情况出现时,问题一定有解。

我们把原图(已经缩过点)的反图进行拓扑排序,就可以得到反图中每个点所在强连通分量的拓扑序。
那么如果原图中x所在的强连通分量有指向x^1所在的强连通分量的边,我们就不能选x。在反图中边的方向正好相反。而这在反图拓扑序中的体现就是:x的拓扑序>x^1的拓扑序。而由于Tarjan的缩点编号方法,强连通分量的编号就是反图的拓扑序。因此,我们只需要在x和x^1的所在的强连通分量中取编号小的那一个即可。

上述情况成立仅当2-SAT满足对称性的时候成立。当2-SAT不满足对称性的时候,就会出现x->y,x^1->y^1这种情况。这时当我们取y和x^1时,显然是矛盾的情况。

乱口胡证毕

模板

求任意解:POJ3683

题意:有n场婚礼,每场婚礼时间上下限s和t,持续的时间为lst。所有婚礼必须在s–s+lst或t-lst–t这两个时间段举行。同一时间只能举办一场婚礼。求一种方案使得所有婚礼都举行。

我们把每一场婚礼看成一个集合,两个元素为s–s+lst和t-lst–t。把有冲突的婚礼建边,跑一边2-SAT即可。

代码:

#include<cstdio>
#include<cctype>
#include<cstring>
#include<algorithm>
#define N 2005
#define il inline 
using namespace std;
struct edge{ int next,to; }ed[N*N];
int n,m,k,nd,p,h[N],st[N],lt[N];
int tp,num[N],dfn[N],low[N],stk[N];
bool ans[N],f[N];
il int _read(){
    int x=0,f=1; char ch=getchar();
    while (!isdigit(ch)) { if (ch=='-') f=-1; ch=getchar(); }
    while (isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    return x*f;
}
il bool jd(int x,int y){ return lt[x]>st[y]&&lt[y]>st[x]; }
il void addedge(int x,int y){ ed[++k].next=h[x],ed[k].to=y,h[x]=k; }
void tarjan(int x){
    dfn[x]=low[x]=++p,stk[++tp]=x,f[x]=true;
    for (int i=h[x],v=ed[i].to;i;i=ed[i].next,v=ed[i].to)
        if (!dfn[v]) tarjan(v),low[x]=min(low[x],low[v]);
        else if (f[v]) low[x]=min(low[x],dfn[v]);
    if (low[x]==dfn[x]){
        f[x]=false,num[x]=++nd;int y;
        do y=stk[tp--],f[y]=false,num[y]=nd; while (y!=x);
    }
}
int main(){
    n=_read();
    for (int i=0;i<(n<<1);i+=2){
        int x=_read(),y=_read(),s=x*60+y,lst,t;
        x=_read(),y=_read(),t=x*60+y,lst=_read();
        if (s+lst>t) return printf("NO\n"),0;
        st[i]=s,lt[i]=s+lst,st[i^1]=t-lst,lt[i^1]=t;
    }
    for (int i=0;i<(n<<1)-2;i+=2) for (int p=0;p<=1;p++)
    for (int j=i+2;j<(n<<1);j+=2) for (int q=0;q<=1;q++)
        if (jd(i^p,j^q)) addedge(i^p,j^q^1),addedge(j^q,i^p^1);
    for (int i=0;i<(n<<1);i++) if (!dfn[i]) tarjan(i);
    for (int i=0;i<(n<<1);i+=2)
        if (num[i]==num[i^1]) return printf("NO\n"),0;
        else if (num[i]<num[i^1]) ans[i]=true; else ans[i^1]=true;
    printf("YES\n");
    for (int i=0;i<(n<<1);i++)
        if (ans[i]) printf("%02d:%02d %02d:%02d\n",st[i]/60,st[i]%60,lt[i]/60,lt[i]%60);
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值