2-SAT 学习笔记

没想到吧,我现在才会 2-SAT。

\(n\) 个布尔变量 \(x_1\sim x_n\),另有 \(m\) 个需要满足的条件,每个条件的形式都是 「\(x_i\)true / false\(x_j\)true / false」。比如 「\(x_1\) 为真或 \(x_3\) 为假」、「\(x_7\) 为假或 \(x_2\) 为假」。

2-SAT 问题的目标是给每个变量赋值使得所有条件得到满足。

以上是 2-SAT 问题的题面。

显然,一条 \(x=a,y=b\) 的条件可以写成:若 \(x\) 不为 \(a\)\(y\)\(b\),若 \(y\) 不为 \(b\)\(x\)\(a\)

此时,我们将 \(n\) 个变量拆成两个点,分别为代表 \(x\)true\(x_1\) 和代表 \(x\)false\(x_2\)。对于每条条件,我们从 \(x_{!a}\)\(y_b\) 连边,从 \(y_{!b}\)\(x_{a}\) 连边。

此时,若两个点在同一个强连通分量内,则全满足或全不满足。如果 \(x_1\)\(x_2\) 同时处在一个强连通分量,很明显不能成立,此时无解。

接下来构造一组可行的解。我们缩点后跑一遍拓扑,把拓扑序跑出来。对于一个点,若其对应的 \(x_1\) 拓扑序更靠后,则变量 \(x\)\(1\),否则为 \(0\)。拓扑序实际不用跑,缩点时已经求出来了。

先放上模板的代码:

#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#define int long long
using namespace std;
int n,m,dfn[2000005],low[2000005],tot,bl[2000005],instk[2000005],stk[2000005],top,cnt;
vector<int>s[2000005];
void dfs(int x,int fa){
    dfn[x]=low[x]=++tot;stk[++top]=x;instk[x]=1;
    for(int i=0;i<s[x].size();i++){
        int y=s[x][i];
        if(!dfn[y]){
            dfs(y,x);
            low[x]=min(low[x],low[y]);
        }else if(instk[y]) low[x]=min(low[x],dfn[y]);
    }
    if(low[x]==dfn[x]){
        cnt++;int k;
        do{
            k=stk[top];top--;
            bl[k]=cnt;instk[k]=0;
        }while(k!=x);
    }
}
signed main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int a,b,x,y;cin>>a>>x>>b>>y;
        if(x==0&&y==0){
            s[b].push_back(a+n);
            s[a].push_back(b+n);
        }else if(x==0&&y==1){
            s[b+n].push_back(a+n);
            s[a].push_back(b);
        }else if(x==1&&y==0){
            s[b].push_back(a);
            s[a+n].push_back(b+n);
        }else{
            s[b+n].push_back(a);
            s[a+n].push_back(b);
        }
    }
    for(int i=1;i<=2*n;i++){
        if(!dfn[i]){
            dfs(i,0);
        }
    }
    for(int i=1;i<=n;i++) if(bl[i]==bl[i+n]){cout<<"IMPOSSIBLE\n";return 0;}
    cout<<"POSSIBLE\n";
    for(int i=1;i<=n;i++){
        if(bl[i]<bl[i+n]) cout<<"1 ";
        else cout<<"0 ";
    }
    cout<<'\n';
    return 0;
}

接下来放几道 2-SAT 的题目:

学生选课P4835

进入大学以后,学生们将面临选课,有 \(n\) 个学生需要选课,学校里有三个老师 JYY,YJY,YYJ。

第一年里,每个学生们都选择了其中一位老师。经过了一年的学习,学生之间相互都有一定的印象,每个同学会根据自己的印象给另外 \(n-1\) 个学生从好到坏排序。第二年的选课开始了,每个学生需要选择老师,可能是因为被坑多了,每一位同学都想换一个老师。这时需要你来调度同学们选课,使得上同一堂课的学生之间印象最坏的最好。

一道很好的 2-SAT 题。

首先,答案可以二分是显然的,我们先二分答案,这样子就转化为了判定一个 \(T\) 是否可行。

每位同学只能从两位老师中选择一个,把每个同学看成一个变量就变成了 2-SAT 问题。而限制就是两个同学之间是否可以同属于一个老师。若不能,则限制了两位同学变量值不能相同,即对于一位老师 \(p\),两位同学 \(x,y\),有 \(x=p\)\(y \ne p\),有 \(y=p\)\(x \ne p\)

于是我们将每位同学拆成两个点,表示分别属于两个老师。然后依据上面的限制关系,在同学拆成的点之间连边。由于我们二分答案,边是确定的。最后对于每一个 \(T\) 我们都能转化为 2-SAT 问题,直接跑模板判定是否可行即可。

代码如下:


#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
#define int long long
using namespace std;
int n,m,dfn[2000005],low[2000005],tot,bl[2000005],instk[2000005],stk[2000005],top,cnt;
int id[1005][3],c[1005][1005];
vector<int>s[2000005];
void dfs(int x,int fa){
    dfn[x]=low[x]=++tot;stk[++top]=x;instk[x]=1;
    for(int i=0;i<s[x].size();i++){
        int y=s[x][i];
        if(!dfn[y]){
            dfs(y,x);
            low[x]=min(low[x],low[y]);
        }else if(instk[y]) low[x]=min(low[x],dfn[y]);
    }
    if(low[x]==dfn[x]){
        cnt++;int k;
        do{
            k=stk[top];top--;
            bl[k]=cnt;instk[k]=0;
        }while(k!=x);
    }
}
bool check(int x){
    tot=0;cnt=0;top=0;
    for(int i=1;i<=2*n;i++) dfn[i]=low[i]=instk[i]=0,s[i].clear(),bl[i]=0;
    for(int i=1;i<=n;i++){
        for(int j=x+1;j<n;j++){
            int y=c[i][j];
            for(int k=0;k<3;k++){
                if(id[i][k]&&id[y][k]){
                    s[id[i][k]].push_back(id[y][k]+(id[y][k]<=n?n:-n));
                    s[id[y][k]].push_back(id[i][k]+(id[i][k]<=n?n:-n));
                }
            }
        }
    }
    for(int i=1;i<=2*n;i++) if(!dfn[i]) dfs(i,0);
    for(int i=1;i<=n;i++){
        if(bl[i]==bl[i+n]) return 0;
    }
    return 1;
}
signed main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        int x,idd=i;cin>>x;
        for(int j=0;j<3;j++){
            if(j!=x) id[i][j]=idd,idd+=n;
        }
        for(int j=1;j<n;j++) cin>>c[i][j];
    }
    int l=1,r=n,cur=0;
    while(l<=r){
        int mid=(l+r)/2;
        if(check(mid)) cur=mid,r=mid-1;
        else l=mid+1;
    }
    cout<<cur<<endl;
    return 0;
}
posted @ 2026-05-09 09:45  Nikaido_hiro  阅读(4)  评论(0)    收藏  举报