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 的题目:
进入大学以后,学生们将面临选课,有 \(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;
}

浙公网安备 33010602011771号