2-sat
简介:
- sat问题(适定性(Sat**isfiability)问题)——给定一个布尔方程,求出这个方程的解**.
- 若有 K K 个变量,则称为K-sat问题,故 K=2 K = 2 时,即为2-sat问题…
- 关于它的解法,常用的有两种——(dfs,tarjan+拓扑).
常规操作:
- 在满足布尔方程的性质——一个变量为1或0——下,我们可以建出一个满足对称性的类似于二分图的图.(具体的连边看条件)
- 在审题中,你要准确定义出一个变量的1和0分别表示什么含义,并且满足这个两个定义也是满足对称性的,进而也能满足图的对称性.
建图模型:
模型一: (A,B) ( A , B ) 不能同时取.
对于 A A ,若被“选择”,则只能选 B′ B ′ ,即建边 A−>B′ A − > B ′ ;
对于 B B ,若被“选择”,则只能选 A′ A ′ ,即建边 B−>A′ B − > A ′ ;
模型二: (A,B) ( A , B ) 不能同时不取.
对于 A′ A ′ ,若被“选择”,则只能选 B B ,即建边 A′−>B A ′ − > B ;
对于 B′ B ′ ,若被“选择”,则只能选 A A ,即建边 B′−>A B ′ − > A ;
模型三: (A,B) ( A , B ) 要么都取,要么都不取.
对于 A A ,若被“选择”,则只能选 B B ,即建边 A−>B A − > B ;
对于 B B ,若被“选择”,则只能选 A A ,即建边 B−>A B − > A ;
模型四: (A,A′) ( A , A ′ ) 必取A.
对于 A′ A ′ ,若被“选择”,则只能选 A A ,即建边 A′−>A A ′ − > A ;
解法1:dfs
概念&原理: 类似二分图的方式,一一去匹配,但这里要通过栈进行记录,在匹配失败后便于还原.故复杂度为 Θ(n∗m) Θ ( n ∗ m ) .
code(模板):
int qwq,head[N];
struct edge{
int to,next;
}E[N<<1];
void addedge(int x,int y){E[qwq]=(edge){y,head[x]};head[x]=qwq++};
int stk[N],top;
bool vis[N];
bool dfs(int x){
if(vis[x^1])return 0;//对立面已被选,匹配失败
if(vis[x])return 1;//已匹配过
vis[x]=1;
stk[top++]=x;
for(int i=head[x];~i;i=E[i].next) if(!dfs(E[i].to)) return 0;//匹配是败
return 1;//匹配成功
}
bool check(){
mcl(vis,0);
SREP(i,0,n){
if(vis[i<<1] || vis[i<<1|1])continue;
top=0;
if(!dfs(i<<1)){//当前这一面匹配失败
while(top)vis[stk[--top]]=0;//还原
if(!dfs(i<<1|1))return 0;//该面的对立面也匹配失败,故无解
}
}
return 1;
}
void Clear(){
qwq=0;
mcl(head,-1);
}
int main(){
scanf("%d%d",&n,&m);
Clear();
REP(i,1,m){//条件
int a,b;
scanf("%d%d",&a,&b);
//对立面
int x1=a<<1,x0=a<<1|1;
int y1=b<<1,y0=b<<1|1;
/* 根据题意建边 addedge(x1,y0); addedge(y1,x0); */
}
if(!check())puts("-1");//无解
else SREP(i,0,n) printf("%d\n",vis[i<<1]?1:0);//vis即为方案(解)
return 0;
}
解法2:tarjan+拓扑
概念&原理: 通过tarjan进行缩点,就是满足条件的点为集合,在通过建反图来跑拓扑求方案(解).
解释:
- 为什么建反图:考虑到算法传递标记的时候,“选择”标记是没有进行传递的,也就是说正向边是没有利用到的,传递的都是“不选择”标记,也就是说走的都是反边,故而建反图.故复杂度为 Θ(n) Θ ( n ) .
- 为什么用拓扑:若不运用拓扑来选择集合的话,当访问到集合 A A 时,发现其无标记,于是把它标记为可选择,然后把 A′ A ′ ( A A 的对立面)标记为不可选择,then进行标记传递,那么进行“选择”标记传递,则集合 B B 和 B′ B ′ 将被标记为选择,若进行“不选择”标记传递,则B和B’将被标记为不选择,这就产生矛盾了.所以,正确的方法应该是按照拓扑顺序,从影响小的开始抉择,在转成反边后, A′ A ′ 会处在拓扑序列的队首,若将 A′ A ′ 标记为选择,则 A A 标记为不选择,都不能传递,再将 B B 标记为选择, B′ B ′ 标记为不选择,当然,将 B B 标记为不选择, B′ B ′ 标记为选择也是可以的.
code(模板):
int qwq,head[N];
struct edge{
int to,next;
}E[N<<1];
void addedge(int x,int y){E[qwq]=(edge){y,head[x]};head[x]=qwq++};
int dfn[N],low[N],tim;
int stk[N],top;
int Id[N],tot;
bool vis[N];
vector<int>G[N];
int In[N],mark[N],rev[N];
queue<int>Q;
void tarjan(int x){
dfn[x]=low[x]=++tim;
stk[++top]=x;
vis[x]=1;
for(int i=head[x];~i;i=E[i].next){
int y=E[i].to;
if(!dfn[y]){
tarjan(y);
chkmi(low[x],low[y]);
}
else if(vis[y]) chmin(low[x],dfn[y]);
}
if(dfn[x]==low[x]){//缩点
tot++;
do{
Id[stk[top]]=tot;
vis[stk[top]]=0;
}while(x!=stk[top--]);
}
}
void Build(){
SREP(x,0,n<<1){
for(int i=head[x];~i;i=E[i].next){
int y=E[i].to;
if(Id[x]!=Id[y])G[Id[y]].pb(Id[x]),In[Id[x]]++;//建反边
}
}
SREP(i,0,n) rev[Id[i<<1]]=Id[i<<1|1],rev[Id[i<<1|1]]=Id[i<<1];//记录对立面
}
void Tuopu(){
while(!Q.empty())Q.pop();
REP(i,1,tot) if(!In[i]) Q.push(i);
while(!Q.empty()){
int x=Q.front();Q.pop();
if(!mark[x])mark[x]=1,mark[rev[x]]=2;//解
SREP(i,0,G[x].size()){
int y=G[x][i];
In[y]--;
if(!In[y])Q.push(y);
}
}
}
void Clear(){
qwq=0;
mcl(head,-1);
tot=top=tim=0;
mcl(dfn,0);
SREP(i,0,n<<1)G[i].clear();
mcl(In,0);
mcl(mark,0);
}
int main(){
scanf("%d%d",&n,&m);
Clear();
REP(i,1,m){//条件
int a,b;
scanf("%d%d",&a,&b);
//对立面
int x1=a<<1,x0=a<<1|1;
int y1=b<<1,y0=b<<1|1;
/* 根据题意建边 addedge(x1,y0); addedge(y1,x0); */
}
SREP(i,0,n<<1) if(!dfn[i]) tarjan(i);
bool f=1;
SREP(i,0,n) if(Id[i<<1]==Id[i<<1|1]){f=0;break;}
if(!f)puts("-1");//无解
else {
Build();//建反图
Tuopu();//跑拓扑
SREP(i,0,n) printf("%d\n",mark[Id[i<<1]]==1?0:1);
}
return 0;
}
总结:
2-sat一般的问题都是求解,一般也是二分答案再建图来判断是否存在解.