学习笔记:2-sat

概念

s a t sat sat问题是求满足一些形如 a 1   o r   a 2   o r   a 3 . . . a_1\ or\ a_2\ or\ a_3... a1 or a2 or a3...的表达式成立的一组解的问题。其中每一个命题 a i a_i ai可以是 x j = 0 x_j=0 xj=0 x j = 1 x_j=1 xj=1 s a t sat sat问题通常是 N P NP NP完全问题。 2 − s a t 2-sat 2sat,顾名思义,就是求满足一些形如 a 1   o r   a 2 a_1\ or\ a_2 a1 or a2的表达式成立的一组解的问题。 2 − s a t 2-sat 2sat是可以在多项式时间内解决的。

方法

可以给每一个 x i x_i xi建立两个点,一个点表示 x i = 0 x_i=0 xi=0,另一个表示 x i = 1 x_i=1 xi=1。现在考虑如何连边。如果按照原表达式的“ o r or or”来连边,最后会出现一些双连通分量,但是不方便我们找到解。但是如果变一下,改成“ a n d and and”来连边,那就会有两条 ¬ a 1 → a 2 \neg a_1\to a_2 ¬a1a2 ¬ a 2 → a 1 \neg a_2\to a_1 ¬a2a1的命题,因为两个条件若有一个没有达成则另一个必须达成。于是,就出现了一个有向图,因为是“ a n d and and”所以每一个强连通分量都必须全选或者全都不选。如果有一个 x i x_i xi 0 0 0 1 1 1都在同一个强连通分量中就会导致 x i x_i xi两种值都不选或都选,显然无解。现在考虑如何构造答案。我们假设选的是靠前的强连通分量,如果最后选的是 ¬ a 1 \neg a_1 ¬a1,则必须选 a 2 a_2 a2,而 a 2 a_2 a2 ¬ a 1 \neg a_1 ¬a1的后面(因为有一条 ¬ a 1 → a 2 \neg a_1\to a_2 ¬a1a2的边), ¬ a 2 \neg a_2 ¬a2 a 1 a_1 a1的前面(因为有一条 ¬ a 2 → a 1 \neg a_2\to a_1 ¬a2a1的边),所以无法保证 a 2 a_2 a2 ¬ a 2 \neg a_2 ¬a2的前面,与我们假设答案一定在前面的强连通分量矛盾。同理,不难证明答案一定在后面。编号越小的强连通分量越靠后,所以选择编号更小的强连通分量即可。不难证明如果有一个强连通分量,则一定存在另一个强连通分量中的元素全都是与前者相反的,所以上述构造答案的方法保证选出来的答案一定在同一个强连通分量内。

例题

AcWing 2402

板子题,按照上述方法写即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=2e6+4;
int dfn[NN],low[NN],scc[NN],scn,cnt,head[NN],e[NN],ne[NN],idx;
bool in[NN];
stack<int>stk;
void add(int u,int v)
{
    e[++idx]=v;
    ne[idx]=head[u];
    head[u]=idx;
}
void tarjan(int u)
{
    low[u]=dfn[u]=++cnt;
    in[u]=true;
    stk.push(u);
    for(int i=head[u];i;i=ne[i])
    {
        int v=e[i];
        if(!dfn[v])
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(in[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u])
    {
        int x;
        scn++;
        do
        {
            x=stk.top();
            stk.pop();
            scc[x]=scn;
            in[x]=false;
        }while(x!=u);
    }
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int x,a,y,b;
        scanf("%d%d%d%d",&x,&a,&y,&b);
        add(x*2+!a,y*2+b);
        add(y*2+!b,x*2+a);
    }
    for(int i=2;i<=n*2+1;i++)
        if(!dfn[i])
            tarjan(i);
    for(int i=1;i<=n;i++)
        if(scc[i*2]==scc[i*2+1])
        {
            printf("IMPOSSIBLE");
            return 0;
        }
    puts("POSSIBLE");
    for(int i=1;i<=n;i++)
        if(scc[i*2]<scc[i*2+1])
            printf("0 ");
        else
            printf("1 ");
    return 0;
}

AcWing 371

每一个仪式都是二选一,符合 2 − s a t 2-sat 2sat。但是每一种选择都是一个区间,并不方便作为条件,所以可以作为点的权值。那么条件就是两个重叠的时段。如果两个时段重叠了,那表达式就是 ¬ a 1   o r ¬ a 2 \neg a_1\ or\neg a_2 ¬a1 or¬a2

#include<bits/stdc++.h>
using namespace std;
const int NN=2004,MM=4000004;
struct node
{
    int s,t,d;
}w[NN];
int dfn[NN],low[NN],scc[NN],scn,cnt,head[NN],e[MM],ne[MM],idx;
bool in[NN];
stack<int>stk;
int is_overlap(int a,int b,int c,int d)
{
    return b>c&&d>a;
}
void add(int u,int v)
{
    e[++idx]=v;
    ne[idx]=head[u];
    head[u]=idx;
}
void tarjan(int u)
{
    low[u]=dfn[u]=++cnt;
    in[u]=true;
    stk.push(u);
    for(int i=head[u];i;i=ne[i])
    {
        int v=e[i];
        if(!dfn[v])
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(in[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u])
    {
        int x;
        scn++;
        do
        {
            x=stk.top();
            stk.pop();
            scc[x]=scn;
            in[x]=false;
        }while(x!=u);
    }
}
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        int h1,s1,h2,s2,d;
        scanf("%d:%d %d:%d %d",&h1,&s1,&h2,&s2,&d);
        w[i]=(node){h1*60+s1,h2*60+s2,d};
    }
    for(int i=1;i<=n;i++)
        for(int j=i+1;j<=n;j++)
        {
            node a=w[i],b=w[j];
            if(is_overlap(a.s,a.s+a.d,b.s,b.s+b.d))
                add(i,j+n),add(j,i+n);
            if(is_overlap(a.s,a.s+a.d,b.t-b.d,b.t))
                add(i,j),add(j+n,i+n);
            if(is_overlap(a.t-a.d,a.t,b.s,b.s+b.d))
                add(i+n,j+n),add(j,i);
            if(is_overlap(a.t-a.d,a.t,b.t-b.d,b.t))
                add(i+n,j),add(j+n,i);
        }
    for(int i=1;i<=n*2;i++)
        if(!dfn[i])
            tarjan(i);
    for(int i=1;i<=n;i++)
        if(scc[i]==scc[i+n])
        {
            printf("NO");
            return 0;
        }
    puts("YES");
    for(int i=1;i<=n;i++)
        if(scc[i]<scc[i+n])
            printf("%02d:%02d %02d:%02d\n",w[i].s/60,w[i].s%60,(w[i].s+w[i].d)/60,(w[i].s+w[i].d)%60);
        else
            printf("%02d:%02d %02d:%02d\n",(w[i].t-w[i].d)/60,(w[i].t-w[i].d)%60,w[i].t/60,w[i].t%60);
    return 0;
}

AcWing 1032

看数据范围,可以发现 d d d很小,所以可以枚举 x x x a , b , c a,b,c a,b,c中的哪一种。但是直接枚举三种可能时间不太够,不难发现只枚举两种可以把剩下的那一种情况枚举到。剩下的就是很裸的 2 − s a t 2-sat 2sat问题了。每张地图的选法是剩余的两个选一个,当命题,条件就是两个必须都选或者都不选,已经是 a n d and and的形式了,不需要转换成 a 1 → a 2 a_1\to a_2 a1a2直接连边即可。但是有可能 a 1 a_1 a1选了后 a 2 a_2 a2却一定不能选,所以只连一条都不选的边即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=100004,MM=200004;
struct node
{
    int x,y;
    char a,b;
}edge[MM];
int n,d,m,xnum[NN],head[NN],ne[MM],e[MM],idx,scn,cnt,low[NN],dfn[NN],in[NN],scc[NN];
char s[NN];
stack<int>stk;
int get(int x,char y,int t)
{
    if(((s[x]-'a'+1)%3!=(y-'A'))^t)
        return x+n;
    else
        return x;
}
char put(int x,int t)
{
    return 'A'+(s[x]-'a'+t)%3;
}
void add(int u,int v)
{
    e[++idx]=v;
    ne[idx]=head[u];
    head[u]=idx;
}
void tarjan(int u)
{
    low[u]=dfn[u]=++cnt;
    in[u]=true;
    stk.push(u);
    for(int i=head[u];~i;i=ne[i])
    {
        int v=e[i];
        if(!dfn[v])
        {
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(in[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u])
    {
        int x;
        scn++;
        do
        {
            x=stk.top();
            stk.pop();
            scc[x]=scn;
            in[x]=false;
        }while(x!=u);
    }
}
int main()
{
    scanf("%d%d%s%d",&n,&d,s,&m);
    for(int i=0,j=0;i<n;i++)
        if(s[i]=='x')
            xnum[j++]=i;
    for(int i=0;i<m;i++)
        scanf("%d %c %d %c",&edge[i].x,&edge[i].a,&edge[i].y,&edge[i].b);
    for(int i=0;i<1<<d;i++)
    {
        for(int j=0;j<d;j++)
            if(i>>j&1)
                s[xnum[j]]='a';
            else
                s[xnum[j]]='b';
        memset(head,-1,sizeof(head));
        memset(dfn,0,sizeof(dfn));
        idx=-1;
        scn=cnt=0;
        for(int j=0;j<m;j++)
        {
            int x=edge[j].x-1,y=edge[j].y-1;
            char a=edge[j].a,b=edge[j].b;
            if(s[x]!=a+32)
            {
                if(s[y]!=b+32)
                    add(get(x,a,0),get(y,b,0)),add(get(y,b,1),get(x,a,1));
                else
                    add(get(x,a,0),get(x,a,1));
            }
        }
        for(int j=0;j<n*2;j++)
            if(!dfn[j])
                tarjan(j);
        bool ok=false;
        for(int j=0;j<n;j++)
            if(scc[j]==scc[j+n])
            {
                ok=true;
                break;
            }
        if(ok)
            continue;
        for(int j=0;j<n;j++)
            if(scc[j]<scc[j+n])
                printf("%c",put(j,1));
            else
                printf("%c",put(j,2));
        return 0;
    }
    printf("-1");
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值