【图论】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\rightarrow y_1,y_1\rightarrow x_1\)
  • \(x,y\)不能同时选中:选了\(x\)就不能选\(y\),反之亦然,所以连边情况为\(x_1\rightarrow y_0,y_1\rightarrow x_0\)
  • \(x,y\)至少选一个:三种情况,\(x/y/x\&y\),即如果确定其中一个不选,则另一个就必须选,所以连边情况为\(x_0\rightarrow y_1,y_0\rightarrow x_1\)
  • \(x,y\)必选且只能选一个:相互限制,确定选中另一个就不选,确定不选另一个就选中,所以连边情况为\(x_0\rightarrow y_1,x_1\rightarrow y_0,y_0\rightarrow x_1,y_1\rightarrow x_0\)
  • \(x\)必须选:直接连接\(x_0\rightarrow 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\rightarrow y_0,y_1\rightarrow 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\rightarrow y_1,\ x_0\rightarrow z_1 \\ y_0\rightarrow x_1,\ y_0\rightarrow z_1 \\ z_0\rightarrow x_1,\ z_0\rightarrow 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]='\0';
    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\n",i+1);
        }
        else
            puts("NIE");
    }
    
    return 0;
}

posted @ 2020-08-17 11:50  StelaYuri  阅读(238)  评论(0)    收藏  举报