zoukankan      html  css  js  c++  java
  • 【图论】2-SAT


    k-SAT 问题

    (SAT)是适定性(Satisfiability)问题的简称。一般形式为 (k-)适定性问题,简称(k-SAT)。而当(k>2)时该问题为NP完全的。所以我们只研究(k=2)的情况。

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

    —— OI Wiki 《2-SAT》


    Example 1

    现有三个物品(a,b,c),给出制约关系——

    1、(a,b)必须同时选

    2、(b,c)不能同时选

    由这两个条件构成的问题即为(2-SAT)问题,它的每个制约关系只针对两个元素

    该例子可行性显然(选(a,b)或者选(c)

    Example 2

    现有三个物品(a,b,c),给出制约关系——

    1、(a,b)必须同时选

    2、(b,c)必须同时选

    3、(a,c)不能同时选

    明显,该例子不存在可行解





    2-SAT 在图中的表示

    在描述中有提及,(2-SAT)问题的每个元素仅存在取或者不取这两种状态

    这里我们将取视作(1),不取视作(0)

    所以对于每个元素(x),可以将其拆成两个点(x_0,x_1)

    对于这两个点,在存在可行解的情况下一定会选到其中一个点

    如果答案中取的是(x_0),则表示答案不取元素(x)

    反之,如果答案中取的是(x_1),则表示答案取了元素(x)


    接下来就是将两点之间的“关系”转化成图形来表示——边

    对于两个元素(x,y)之间的关系,可分为以下几种

    • (x,y)必须同时选中:表示要么都选,要么都不选,所以连边情况为(x_1 ightarrow y_1,y_1 ightarrow x_1)
    • (x,y)不能同时选中:选了(x)就不能选(y),反之亦然,所以连边情况为(x_1 ightarrow y_0,y_1 ightarrow x_0)
    • (x,y)至少选一个:三种情况,(x/y/x&y),即如果确定其中一个不选,则另一个就必须选,所以连边情况为(x_0 ightarrow y_1,y_0 ightarrow x_1)
    • (x,y)必选且只能选一个:相互限制,确定选中另一个就不选,确定不选另一个就选中,所以连边情况为(x_0 ightarrow y_1,x_1 ightarrow y_0,y_0 ightarrow x_1,y_1 ightarrow x_0)
    • (x)必须选:直接连接(x_0 ightarrow x_1),强制选择

    然后对图进行处理,即可开始求解问题




    2-SAT 的 SCC 解法

    主要靠建边后寻找强连通分量,缩点后确定可行解的方式求解

    如果对于某个元素(x),其(x_0)(x_1)若在同一强连通分量内(可互相到达),则会造成条件冲突,显然无解

    否则,缩点后图将会变成一张DAG,那就根据拓扑序来构造答案状态(即判断(x_0)(x_1)的SCC编号大小)

    如果(x_0)编号在(x_1)前((scc[x_0]<scc[x_1])),则取(x=x_0),否则(x=x_1)

    时间复杂度为(O(n+m))


    例题 1 (可行性判断)

    HDU 3062 - Party

    题意

    每对夫妻只能选择一人参加聚会,但由于某些人之间存在矛盾,不能同时让他们参加聚会,问是否存在一种方案使得聚会能够有(n)个人参加(即每对夫妻都能有一人参加)

    显然,两夫妻如果其中一个不参加,另外一个就会参加(假定先不考虑冲突)

    所以可以将一对夫妻看作是一个元素的(x_0)(x_1)这两种情况,合法解下为二选一状态,正好也符合了题意

    所以直接根据(2-SAT)的建图法进行建图,将有矛盾的两人(两点(x,y))对应连边,符合“不能同时选中”的情况,所以建图方式为(x_1 ightarrow y_0,y_1 ightarrow x_0)

    跑SCC缩点,最后判断是否存在一对({x_0,x_1})是在同一个SCC内的即可,如果存在则无解,不存在则有解

    #include<bits/stdc++.h>
    using namespace std;
    const int maxn=2050,maxm=4000050,maxk=10050;
    
    struct Edge
    {
        int to,next;
    }eg[maxm];
    int pre[maxn],lowlink[maxn],sccno[maxn],head[maxn];
    int STACK[maxk],STACKTop;
    int dfs_clock,scc_cnt,tot;
    bool ins[maxn];
    
    inline void SPush(int a)
    {
        STACK[STACKTop++]=a;
    }
    inline void SPop()
    {
        STACKTop--;
    }
    inline bool SEmpty()
    {
        return STACKTop==0;
    }
    inline int STop()
    {
        return STACK[STACKTop-1];
    }
    void init(int nn)
    {
        dfs_clock=scc_cnt=STACKTop=tot=0;
        memset(head,-1,(nn<<2)+5);
        memset(sccno,0,(nn<<2)+5);
        memset(pre,0,(nn<<2)+5);
        memset(ins,false,(nn<<2)+5);
    }
    void addedge(int u,int v)
    {
        eg[tot].to=v;
        eg[tot].next=head[u];
        head[u]=tot++;
    }
    void tarjan(int in)
    {
        pre[in]=lowlink[in]=++dfs_clock;
        SPush(in);
        ins[in]=true;
        for(int i=head[in];i!=-1;i=eg[i].next)
        {
            int &v=eg[i].to;
            if(!pre[v])
            {
                tarjan(v);
                lowlink[in]=min(lowlink[in],lowlink[v]);
            }
            else if(ins[v])
                lowlink[in]=min(lowlink[in],pre[v]);
        }
        if(lowlink[in]==pre[in])
        {
            scc_cnt++;
            while(1)
            {
                int x=STop();
                SPop();
                sccno[x]=scc_cnt;
                ins[x]=false;
                if(x==in)
                    break;
            }
        }
    }
    
    int n;
    void solve()
    {
        int m,a1,a2,c1,c2;
        scanf("%d",&m);
        init(n<<1);
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d%d%d",&a1,&a2,&c1,&c2);
            a1=a1<<1|c1;
            a2=a2<<1|c2;
            addedge(a1,a2^1);
            addedge(a2,a1^1);
        }
        for(int i=0;i<(n<<1);i++)
        {
            if(!pre[i])
                tarjan(i);
        }
        for(int i=0;i<n;i++)
        {
            if(sccno[i<<1]==sccno[i<<1|1])
            {
                puts("NO");
                return;
            }
        }
        puts("YES");
    }
    int main()
    {
        while(scanf("%d",&n)!=EOF)
            solve();
        return 0;
    }
    

    例题 2 (可行方案求解)

    2018-2019 ACM-ICPC, Asia Seoul Regional Contest K - TV Show Game

    题意

    现有(k(k>3))盏灯,仅有两种可能的颜色(R/B)

    每个人都会选择(3)盏灯猜,猜中两盏及以上即可获奖

    试确定一种灯的涂色方案,使得所有人都能获奖,不存在输出(-1)

    根据“猜中两盏及以上即可获奖”这一条件可以得知

    最多只能猜错一盏灯

    假定三盏灯为(x,y,z),假如猜错了(x),那么必须让(y,z)都猜对

    所以可以得到灯之间的关系为

    [x_0 ightarrow y_1, x_0 ightarrow z_1 \ y_0 ightarrow x_1, y_0 ightarrow z_1 \ z_0 ightarrow x_1, z_0 ightarrow y_1 ]

    表示如果猜错一盏,则必须让另外两盏灯都对

    转化到题目中来,虽然我们能在“对/错”的基础上得出连边关系

    但刚开始还是不知道指定的灯具体是什么颜色的,也就没办法判断对错

    那么我们就可以假定所有的(R)色为对,(B)色为错,便能进行建边

    其余套路不变,跑完SCC后判断是否同一位置两种状态在同一SCC内,是则无解

    否则,按照sccno的大小来选择答案即可

    #include<bits/stdc++.h>
    using namespace std;
    const int maxn=10050,maxm=60050,maxk=10050;
    
    struct Edge
    {
        int to,next;
    }eg[maxm];
    int pre[maxn],lowlink[maxn],sccno[maxn],head[maxn];
    int STACK[maxk],STACKTop;
    int dfs_clock,scc_cnt,tot;
    bool ins[maxn];
    
    inline void SPush(int a)
    {
        STACK[STACKTop++]=a;
    }
    inline void SPop()
    {
        STACKTop--;
    }
    inline bool SEmpty()
    {
        return STACKTop==0;
    }
    inline int STop()
    {
        return STACK[STACKTop-1];
    }
    void init(int nn)
    {
        dfs_clock=scc_cnt=STACKTop=tot=0;
        memset(head,-1,(nn<<2)+5);
        memset(sccno,0,(nn<<2)+5);
        memset(pre,0,(nn<<2)+5);
        memset(ins,false,(nn<<2)+5);
    }
    void addedge(int u,int v)
    {
        eg[tot].to=v;
        eg[tot].next=head[u];
        head[u]=tot++;
    }
    void tarjan(int in)
    {
        pre[in]=lowlink[in]=++dfs_clock;
        SPush(in);
        ins[in]=true;
        for(int i=head[in];i!=-1;i=eg[i].next)
        {
            int &v=eg[i].to;
            if(!pre[v])
            {
                tarjan(v);
                lowlink[in]=min(lowlink[in],lowlink[v]);
            }
            else if(ins[v])
                lowlink[in]=min(lowlink[in],pre[v]);
        }
        if(lowlink[in]==pre[in])
        {
            scc_cnt++;
            while(1)
            {
                int x=STop();
                SPop();
                sccno[x]=scc_cnt;
                ins[x]=false;
                if(x==in)
                    break;
            }
        }
    }
    
    int a[4];
    char s[4][2];
    bool vis[10050];
    char ans[10050];
    
    int main()
    {
        int k,n;
        scanf("%d%d",&k,&n);
        init(k<<1|1);
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=3;j++)
                scanf("%d%s",&a[j],s[j]);
            addedge(a[1]<<1|(s[1][0]=='B'),a[2]<<1|(s[2][0]=='R'));
            addedge(a[1]<<1|(s[1][0]=='B'),a[3]<<1|(s[3][0]=='R'));
            addedge(a[2]<<1|(s[2][0]=='B'),a[1]<<1|(s[1][0]=='R'));
            addedge(a[2]<<1|(s[2][0]=='B'),a[3]<<1|(s[3][0]=='R'));
            addedge(a[3]<<1|(s[3][0]=='B'),a[1]<<1|(s[1][0]=='R'));
            addedge(a[3]<<1|(s[3][0]=='B'),a[2]<<1|(s[2][0]=='R'));
        }
        for(int i=2;i<=(k<<1|1);i++)
            if(!pre[i])
                tarjan(i);
        for(int i=2;i<=(k<<1);i+=2)
            if(sccno[i]==sccno[i|1])
            {
                puts("-1");
                return 0;
            }
        for(int i=2;i<=(k<<1);i+=2)
        {
            int x=sccno[i],y=sccno[i|1];
            if(vis[x])
            {
                ans[i>>1]='B';
                continue;
            }
            if(vis[y])
            {
                ans[i>>1]='R';
                continue;
            }
            if(x<y)
            {
                vis[x]=true;
                ans[i>>1]='B';
            }
            else
            {
                vis[y]=true;
                ans[i>>1]='R';
            }
        }
        ans[k+1]='';
        puts(ans+1);
        
        return 0;
    }
    



    2-SAT 的 DFS 解法

    DFS解法如果加上一堆优化后是可以做到(O(n+m))的时间复杂度(不大会)

    一般不大会用这种写法

    但DFS能够直接求出字典序最小的解

    所以也不失为一种好的暴力方法


    例题 (字典序最小可行方案求解)

    HDU 1814 - Peaceful Commission

    题意

    (n)个党派,每个党派有两个代表

    每个党派需要派出一个代表参加聚会

    但是可能有些代表之间关系不好,所以不能同时派出

    问是否存在派出的方案,使得每个党派都能派出一人,且派出的人之间关系不会不好

    存在则输出字典序最小解,不存在输出NIE

    与上面的例题1差不多,也是道模板题

    关键在于字典序最小的限制

    所以直接跑2-SAT的DFS的板子就行

    #include<bits/stdc++.h>
    using namespace std;
    const int maxn=8050,maxm=20050;
    
    struct Edge
    {
        int to,next;
    }eg[maxm<<1];
    bool vis[maxn<<1];
    int head[maxn<<1],tot,sk[maxn<<1],skp;
    
    bool dfs(int p)
    {
        if(vis[p^1]) //另外一人已经派出,则直接返回false
            return false;
        if(vis[p]) //否则如果自己已经派出,直接返回true
            return true;
        vis[p]=true;
        sk[skp++]=p;
        for(int i=head[p];i!=-1;i=eg[i].next)
            if(!dfs(eg[i].to))
                return false;
        return true;
    }
    
    void init(int n)
    {
        n<<=1;
        for(int i=0;i<n;i++)
        {
            vis[i]=false;
            head[i]=-1;
        }
        skp=tot=0;
    }
    
    void addedge(int x,int y)
    {
        eg[tot].next=head[x];
        eg[tot].to=y^1;
        head[x]=tot++;
        eg[tot].next=head[y];
        eg[tot].to=x^1;
        head[y]=tot++;
    }
    
    bool solve(int n)
    {
        for(int i=0;i<(n<<1);i+=2)
        {
            if(!vis[i]&&!vis[i|1])
            {
                skp=0;
                if(!dfs(i))
                {
                    while(skp)
                        vis[sk[--skp]]=false;
                    if(!dfs(i|1))
                        return false;
                }
            }
        }
        return true;
    }
    
    int main()
    {
        int n,m,a,b;
        while(scanf("%d%d",&n,&m)!=EOF)
        {
            init(n);
            for(int i=1;i<=m;i++)
            {
                scanf("%d%d",&a,&b);
                a--;b--;
                addedge(a,b);
            }
            if(solve(n))
            {
                for(int i=0;i<(n<<1);i++)
                    if(vis[i])
                        printf("%d
    ",i+1);
            }
            else
                puts("NIE");
        }
        
        return 0;
    }
    

  • 相关阅读:
    Codeforces Round #652 (Div. 2)
    Codeforces Round #651 (Div. 2)
    The 2017 China Collegiate Programming Contest, Qinhuangdao Site
    2017中国大学生程序设计竞赛-哈尔滨站
    Codeforces Global Round 8
    [CF768D] Jon and Orbs
    2020牛客暑期多校训练营(第一场)I
    2020牛客暑期多校训练营(第一场)F
    [HDU5769] Substring
    [PA2010] Riddle
  • 原文地址:https://www.cnblogs.com/stelayuri/p/13516751.html
Copyright © 2011-2022 走看看