2-SAT
算法理解
首先我们要了解一种很典型的图论建图思想,如果a一定b,那么就将a->b建一条边,然后如果在一个强连通分量中逻辑关系冲突了,则不满足,反之则满足
2-sat与扩展域并查集的区别
2-sat有向图,扩展域并查集无向图
2-sat若a则b,扩展域并查集若a则b若b则a
2-sat的具体实现
我们将或转化为若则的逻辑关系,因为对于一个 \(x_i\) 只有可能有真或假的两种关系,所以对于一个约束条件 \(x_1\) 为真或 \(x_2\)为假,就可以转化为若 \(x_1\) 为假,则 \(x_2\)为假和若 \(x_2\) 为真,则 \(x_1\)为真
像这样我们就把一种逻辑关系转化为一条边了,知周所众,同一个强连通分量的点可以互相到达,所以同一个强连通分量中若出现冲突,就像同时出现 \(x_1\) 为假和真,就一定不合法,反之则一定合法,我们将所有点进行一个拓扑排序,然后拓扑序更大那个就是合法解,因为其出度更小,所以约束别的条件就更少,所以更优
2-sat为什么一个强连通分量中没有冲突则一定有合法解呢?
首先在强连通分量中,一个真可以推出一个假,而一个假也可以推出一个真,所以不成立,然而缩完点之后只有像一个可以推出一个真这种条件,那么我们只需要取真就行,因为真推不出假,所以真一定拓扑序更大,所以取真一定最优
优化
考虑强连通分量的标号顺序恰好是拓扑序的反序,因为在dfs搜索树上,是从前往后搜的,而缩点是从后向前搜的,缩完点后,返祖边不复存在,树边从编号大的指向小的,横叉边也是从还没缩完点的点指向已经缩过点的强连通分量,所以必然编号越小的拓扑序越大,因为指的边多
赋值
将强连通分量值较小的作为答案,原因前文已经解释了
例题
P4782 【模板】2-SAT
#include<bits/stdc++.h>
using namespace std;
const int N=4e6+5;
int n,m,u,v,a,b,colnum,tot,cnt,l;
int col[N],low[N],dfn[N],s[N],vis[N];
vector<int>e[N];
void add(int x,int y){
e[x].push_back(y);
}
void tarjan(int x){
dfn[x]=low[x]=++tot;
s[++l]=x;
for(int i:e[x]){
if(!dfn[i]) tarjan(i);
if(!vis[i]) low[x]=min(low[x],low[i]);
}
if(low[x]==dfn[x]){
colnum++;
while(s[l]!=x){
col[s[l]]=colnum;
if(s[l]>n){
if(vis[s[l]-n]==colnum) cnt=1;
}
if(s[l]<=n){
if(vis[s[l]+n]==colnum) cnt=1;
}
vis[s[l]]=colnum;
l--;
}
col[s[l]]=colnum;
if(s[l]>n){
if(vis[s[l]-n]==colnum) cnt=1;
}
if(s[l]<=n){
if(vis[s[l]+n]==colnum) cnt=1;
}
vis[s[l]]=colnum;
l--;
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
scanf("%d%d%d%d",&u,&a,&v,&b);
if(a&&b){
add(u+n,v);
add(v+n,u);
}
if(!a&&!b){
add(u,v+n);
add(v,u+n);
}
if(a&&!b){
add(u+n,v+n);
add(v,u);
}
if(!a&&b){
add(u,v);
add(v+n,u+n);
}
}
for(int i=1;i<=2*n;i++){//注这里n要*2
if(!dfn[i]) tarjan(i);
}
if(cnt){
printf("IMPOSSIBLE");
return 0;
}
printf("POSSIBLE\n");
for(int i=1;i<=n;i++){
if(col[i]>col[i+n]){
printf("0 ");
}
else{
printf("1 ");
}
}
}
P4171 [JSOI2010] 满汉全席
类模板
#include<bits/stdc++.h>
using namespace std;
const int N=4e5+5;
int K,n,m,a,b,u,v,tot,l,colnum,cnt;
int dfn[N],low[N],vis[N],s[N];
vector<int>e[N];
void add(int x,int y){
e[x].push_back(y);
// printf("%d %d\n",x,y);
}
void tarjan(int x){
dfn[x]=low[x]=++tot;
s[++l]=x;
for(int i:e[x]){
if(!dfn[i]) tarjan(i);
if(!vis[i]) low[x]=min(low[x],low[i]);
}
if(low[x]==dfn[x]){
colnum++;
while(s[l]!=x){
if(s[l]>n){
if(vis[s[l]-n]==colnum) cnt=1;
}
if(s[l]<=n){
if(vis[s[l]+n]==colnum) cnt=1;
}
vis[s[l]]=colnum;
l--;
}
if(s[l]>n){
if(vis[s[l]-n]==colnum) cnt=1;
}
if(s[l]<=n){
if(vis[s[l]+n]==colnum) cnt=1;
}
vis[s[l]]=colnum;
l--;
}
}
int main(){
scanf("%d",&K);
while(K--){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
string s1,s2;
cin>>s1>>s2;
if(s1[0]=='m') a=1;
else a=0;
if(s2[0]=='m') b=1;
else b=0;
u=0,v=0;
int t=1;
while(s1[t]){
u=u*10+(s1[t]-'0');
t++;
}
t=1;
while(s2[t]){
v=v*10+(s2[t]-'0');
t++;
}
// printf("u=%d a=%d v=%d b=%d\n",u,a,v,b);
if(a&&b){
add(u+n,v);
add(v+n,u);
}
if(!a&&!b){
add(u,v+n);
add(v,u+n);
}
if(!a&&b){
add(u,v);
add(v+n,u+n);
}
if(a&&!b){
add(u+n,v+n);
add(v,u);
}
}
for(int i=1;i<=2*n;i++){
if(!dfn[i]){
tarjan(i);
}
}
if(!cnt){
printf("GOOD\n");
}
else{
printf("BAD\n");
}
colnum=cnt=l=tot=0;
for(int i=1;i<=2*n;i++) dfn[i]=low[i]=vis[i]=s[i]=0;
for(int i=1;i<=2*n;i++){//这里也要n*2
e[i].clear();
}
}
}
P3825 [NOI2017] 游戏
一眼:3-sat好耶!
为什么3-sat不可做?
因为2-sat
基于一个事实:一个节点如果不a则b,所以只有两种状态可以选择,然而3-sat,如果不a到底是b还是c呢?
做法:
x如何转化?注意到 \(d<=8\) 暴力枚举每个x是a还是b或c, \(O(3^d)\) 题解说过不了,题解又说可以只枚举两次 \(O(2^d)\) 就可以解决,为什么呢?
考虑你每次枚举的是不选的车,不选a选b,c,不选b选a,c,但是每条赛道只用有一辆车,我们相当于已经涵盖了选a,b,c三种情况了
2-sat变式
2-sat是类似与a或b的问题的,这里是若a则b的问题,哦,那不就更简单了吗,直接做就做完了,然后就喜提20分然后以为写挂了一直调调不出来,考虑这样一组情况
按照2-sat进行赋值,显然会选择1,2,但就不满足条件,因为选2一定选3
so
我们重新加边考虑若2为真则1为假,若1为真则2一定不为真,就有 \(a->b则!b->!a\)
于是这道题就做完了
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,d,ntnum,m,l,tot,colnum,cnt,cn;
char s[N],c1[N],c2[N];
int track[N],nt[N],ti[N],tj[N],col[N],st[N],dfn[N],low[N];
vector<int>b[N];
int giveid(int tx,char x){//返回1/0
int y,acr=0;
if(x=='A') y=1;
if(x=='B') y=2;
if(x=='C') y=3;
for(int i=1;i<y;i++){
if(track[tx]!=i) acr++;
}
if(track[tx]==y) return -1;
return acr;
}
void print(int tx,int x){
int y=0;
for(int i=1;i<=3;i++){
if(track[tx]==i) continue;
if(y==x) printf("%c",'A'+i-1);
y++;
}
}
void tarjan(int x){
dfn[x]=low[x]=++tot;
st[++l]=x;
for(int i:b[x]){
if(!dfn[i]) tarjan(i);
if(!col[i]) low[x]=min(low[i],low[x]);
}
if(low[x]==dfn[x]){
colnum++;
while(st[l]!=x){
col[st[l]]=colnum;
if(st[l]<=n){
if(colnum==col[st[l]+n]) cnt=1;
}
else{
if(colnum==col[st[l]-n]) cnt=1;
}
l--;
}
col[st[l]]=colnum;
if(st[l]<=n){
if(colnum==col[st[l]+n]) cnt=1;
}
else{
if(colnum==col[st[l]-n]) cnt=1;
}
l--;
}
}
void add(int x,int y){
b[x].push_back(y);
// printf("%d %d\n",x,y);
}
void addedge(){
l=0,tot=0,colnum=0,cnt=0;
for(int i=1;i<=2*n;i++){
b[i].clear();
col[i]=st[i]=dfn[i]=low[i]=0;
}
for(int i=1;i<=m;i++){
int u=ti[i],v=tj[i];
int a=giveid(u,c1[i]),b=giveid(v,c2[i]);
if(a==-1) continue;
if(b==-1){
if(!a) add(u,u+n);
else add(u+n,u);
continue;
}
if(a&&b){
add(u+n,v+n);
add(v,u);
}
if(!a&&!b){
add(u,v);
add(v+n,u+n);
}
if(a&&!b){
add(u+n,v);
add(v+n,u);
}
if(!a&&b){
add(u,v+n);
add(v,u+n);
}
}
for(int i=1;i<=2*n;i++){
if(!dfn[i]) tarjan(i);
}
if(!cnt){
cn=1;
for(int i=1;i<=n;i++){
if(col[i]<col[i+n]) print(i,0);
else print(i,1);
}
}
}
void dfs(int x){
if(cn) return;
if(x>d){
// printf("num=%d %d\n",track[1],track[2]);
addedge();
return;
}
track[nt[x]]=1;
dfs(x+1);
track[nt[x]]=2;
dfs(x+1);
track[nt[x]]=0;
}
int main(){
scanf("%d%d",&n,&d);
cin>>(s+1);
for(int i=1;i<=n;i++){
if(s[i]=='x'){
nt[++ntnum]=i;
track[i]=0;
}
if(s[i]=='a'){
track[i]=1;
}
if(s[i]=='b'){
track[i]=2;
}
if(s[i]=='c'){
track[i]=3;
}
}
scanf("%d",&m);
for(int i=1;i<=m;i++){
scanf("%d %c%d %c",&ti[i],&c1[i],&tj[i],&c2[i]);
}
if(d) dfs(1);
else addedge();
if(!cn){
printf("-1");
}
}