2-SAT问题及其算法


【2-SAT问题】

现有一个由N个布尔值组成的序列A,给出一些限制关系,比如A[x] AND A[y]=0、A[x] OR A[y] OR A[z]=1等,要确定A[0..N-1]的值,使得其满足所有限制关系。这个称为SAT问题,特别的,若每种限制关系中最多只对两个元素进行限制,则称为2-SAT问题。

由于在2-SAT问题中,最多只对两个元素进行限制,所以可能的限制关系共有11种:
A[x]
NOT A[x]
A[x] AND A[y]
A[x] AND NOT A[y]
A[x] OR A[y]
A[x] OR NOT A[y]
NOT (A[x] AND A[y])
NOT (A[x] OR A[y])
A[x] XOR A[y]
NOT (A[x] XOR A[y])
A[x] XOR NOT A[y]
进一步,A[x] AND A[y]相当于(A[x]) AND (A[y])(也就是可以拆分成A[x]与A[y]两个限制关系),NOT(A[x] OR A[y])相当于NOT A[x] AND NOT A[y](也就是可以拆分成NOT A[x]与NOT A[y]两个限制关系)。因此,可能的限制关系最多只有9种。

在实际问题中,2-SAT问题在大多数时候表现成以下形式:有N对物品,每对物品中必须选取一个,也只能选取一个,并且它们之间存在某些限制关系(如某两个物品不能都选,某两个物品不能都不选,某两个物品必须且只能选一个,某个物品必选)等,这时,可以将每对物品当成一个布尔值(选取第一个物品相当于0,选取第二个相当于1),如果所有的限制关系最多只对两个物品进行限制,则它们都可以转化成9种基本限制关系,从而转化为2-SAT模型。

【建模】
其实2-SAT问题的建模是和实际问题非常相似的。
建立一个2N阶的有向图,其中的点分为N对,每对点表示布尔序列A的一个元素的0、1取值(以下将代表A[i]的0取值的点称为i,代表A[i]的1取值的点称为i')。显然每对点必须且只能选取一个。然后,图中的边具有特定含义。若图中存在边<i, j>,则表示若选了i必须选j。可以发现,上面的9种限制关系中,后7种二元限制关系都可以用连边实现,比如NOT(A[x] AND A[y])需要连两条边<x, y'>和<y, x'>,A[x] OR A[y]需要连两条边<x', y>和<y', x>。而前两种一元关系,对于A[x](即x必选),可以通过连边<x', x>来实现,而NOT A[x](即x不能选),可以通过连边<x, x'>来实现。

【O(NM)算法:求字典序最小的解】
根据2-SAT建成的图中边的定义可以发现,若图中i到j有路径,则若i选,则j也要选;或者说,若j不选,则i也不能选;
因此得到一个很直观的算法:
(1)给每个点设置一个状态V,V=0表示未确定,V=1表示确定选取,V=2表示确定不选取。称一个点是 已确定的当且仅当其V值非0。设立两个队列Q1和Q2,分别存放本次尝试选取的点的编号和尝试不选的点的编号。
(2)若图中所有的点均已确定,则找到一组解,结束,否则,将Q1、Q2清空,并任选一个未确定的点i,将i加入队列Q1,将i'加入队列Q2;
(3)找到i的所有后继。对于后继j,若j未确定,则将j加入队列Q1;若j'(这里的j'是指与j在同一对的另一个点)未确定,则将j'加入队列Q2;
(4)遍历Q2中的每个点,找到该点的所有前趋(这里需要先建一个补图),若该前趋未确定,则将其加入队列Q2;
(5)在(3)(4)步操作中,出现以下情况之一,则本次尝试失败,否则本次尝试成功:
<1>某个已被加入队列Q1的点被加入队列Q2;
<2>某个已被加入队列Q2的点被加入队列Q1;
<3>某个j的状态为2;
<4>某个i'或j'的状态为1或某个i'或j'的前趋的状态为1;
(6)若本次尝试成功,则将Q1中的所有点的状态改为1,将Q2中所有点的状态改为2,转(2),否则尝试点i',若仍失败则问题无解。
该算法的时间复杂度为O(NM)(最坏情况下要尝试所有的点,每次尝试要遍历所有的边),但是在多数情况下,远远达不到这个上界。
具体实现时,可以用一个数组vst来表示队列Q1和Q2。设立两个标志变量i1和i2(要求对于不同的i,i1和i2均不同,这样可以避免每次尝试都要初始化一次,节省时间),若vst[i]=i1则表示i已被加入Q1,若vst[i]=i2则表示i已被加入Q2。不过Q1和Q2仍然是要设立的,因为遍历(BFS)的时候需要队列,为了防止重复遍历,加入Q1(或Q2)中的点的vst值必然不等于i1(或i2)。中间一旦发生矛盾,立即中止尝试,宣告失败。

该算法虽然在多数情况下时间复杂度到不了O(NM),但是综合性能仍然不如下面的O(M)算法。不过,该算法有一个很重要的用处:求字典序最小的解!
如果原图中的同一对点编号都是连续的(01、23、45……)则可以依次尝试第0对、第1对……点,每对点中先尝试编号小的,若失败再尝试编号大的。这样一定能求出字典序最小的解(如果有解的话),因为 一个点一旦被确定,则不可更改
如果原图中的同一对点编号不连续(比如03、25、14……)则按照该对点中编号小的点的编号递增顺序将每对点排序,然后依次扫描排序后的每对点,先尝试其编号小的点,若成功则将这个点选上,否则尝试编号大的点,若成功则选上,否则(都失败)无解。

【具体题目】 HDU1814(求字典序最小的解)
#include  < iostream >
#include 
< stdio.h >
using   namespace  std;
#define  re(i, n) for (int i=0; i<n; i++)
const   int  MAXN  =   20000 , MAXM  =   100000 , INF  =   ~ 0U   >>   2 ;
struct  node {
    
int  a, b, pre, next;
} E[MAXM], E2[MAXM];
int  _n, n, m, V[MAXN], ST[MAXN][ 2 ], Q[MAXN], Q2[MAXN], vst[MAXN];
bool  res_ex;
void  init_d()
{
    re(i, n) E[i].a 
=  E[i].pre  =  E[i].next  =  E2[i].a  =  E2[i].pre  =  E2[i].next  =  i;
    m 
=  n;
}
void  add_edge( int  a,  int  b)
{
    E[m].a 
=  a; E[m].b  =  b; E[m].pre  =  E[a].pre; E[m].next  =  a; E[a].pre  =  m; E[E[m].pre].next  =  m;
    E2[m].a 
=  b; E2[m].b  =  a; E2[m].pre  =  E2[b].pre; E2[m].next  =  b; E2[b].pre  =  m; E2[E2[m].pre].next  =  m ++ ;
}
void  solve()
{
    re(i, n) {V[i] 
=   0 ; vst[i]  =   0 ;} res_ex  =   1 ;
    
int  i, i1, i2, j, k, len, front, rear, front2, rear2;
    
bool  ff;
    re(_i, _n) {
        
if  (V[_i  <<   1 ==   1   ||  V[(_i  <<   1 +   1 ==   1 continue ;
        i 
=  _i  <<   1 ; len  =   0 ;
        
if  ( ! V[i]) {
            ST[len][
0 =  i; ST[len ++ ][ 1 =   1 if  ( ! V[i  ^   1 ]) {ST[len][ 0 =  i  ^   1 ; ST[len ++ ][ 1 =   2 ;}
            Q[front 
=  rear  =   0 =  i; vst[i]  =  i1  =  n  +  i; Q2[front2  =  rear2  =   0 =  i  ^   1 ; vst[i  ^   1 =  i2  =  (n  <<   1 +  i; ff  =   1 ;
            
for  (; front <= rear; front ++ ) {
                j 
=  Q[front];
                
for  ( int  p  =  E[j].next; p  !=  j; p = E[p].next) {
                    k 
=  E[p].b;
                    
if  (V[k]  ==   2   ||  vst[k]  ==  i2  ||  V[k  ^   1 ==   1   ||  vst[k  ^   1 ==  i1) {ff  =   0 break ;}
                    
if  (vst[k]  !=  i1) {
                        Q[
++ rear]  =  k; vst[k]  =  i1;
                        
if  ( ! V[k]) {ST[len][ 0 =  k; ST[len ++ ][ 1 =   1 ;}
                    }
                    
if  (vst[k  ^   1 !=  i2) {
                        Q2[
++ rear2]  =  k  ^   1 ; vst[k  ^   1 =  i2;
                        
if  ( ! V[k]) {ST[len][ 0 =  k  ^   1 ; ST[len ++ ][ 1 =   2 ;}
                    }
                }
                
if  ( ! ff)  break ;
            }
            
if  (ff) {
                
for  (; front2 <= rear2; front2 ++ ) {
                    j 
=  Q2[front2];
                    
for  ( int  p  =  E2[j].next; p  !=  j; p = E2[p].next) {
                        k 
=  E2[p].b;
                        
if  (V[k]  ==   1   ||  vst[k]  ==  i1) {ff  =   0 break ;}
                        
if  (vst[k]  !=  i2) {
                            vst[k] 
=  i2; Q2[ ++ rear]  =  k;
                            
if  ( ! V[k]) {ST[len][ 0 =  k; ST[len ++ ][ 1 =   2 ;}
                        }
                    }
                    
if  ( ! ff)  break ;
                }
                
if  (ff) {
                    re(j, len) V[ST[j][
0 ]]  =  ST[j][ 1 ];
                    
continue ;
                }
            }
        }
        i 
=  (_i  <<   1 +   1 ; len  =   0 ;
        
if  ( ! V[i]) {
            ST[len][
0 =  i; ST[len ++ ][ 1 =   1 if  ( ! V[i  ^   1 ]) {ST[len][ 0 =  i  ^   1 ; ST[len ++ ][ 1 =   2 ;}
            Q[front 
=  rear  =   0 =  i; vst[i]  =  i1  =  n  +  i; Q2[front2  =  rear2  =   0 =  i  ^   1 ; vst[i  ^   1 =  i2  =  (n  <<   1 +  i; ff  =   1 ;
            
for  (; front <= rear; front ++ ) {
                j 
=  Q[front];
                
for  ( int  p  =  E[j].next; p  !=  j; p = E[p].next) {
                    k 
=  E[p].b;
                    
if  (V[k]  ==   2   ||  vst[k]  ==  i2  ||  V[k  ^   1 ==   1   ||  vst[k  ^   1 ==  i1) {ff  =   0 break ;}
                    
if  (vst[k]  !=  i1) {
                        Q[
++ rear]  =  k; vst[k]  =  i1;
                        
if  ( ! V[k]) {ST[len][ 0 =  k; ST[len ++ ][ 1 =   1 ;}
                    }
                    
if  (vst[k  ^   1 !=  i2) {
                        Q2[
++ rear2]  =  k  ^   1 ; vst[k  ^   1 =  i2;
                        
if  ( ! V[k]) {ST[len][ 0 =  k  ^   1 ; ST[len ++ ][ 1 =   2 ;}
                    }
                }
                
if  ( ! ff)  break ;
            }
            
if  (ff) {
                
for  (; front2 <= rear2; front2 ++ ) {
                    j 
=  Q2[front2];
                    
for  ( int  p  =  E2[j].next; p  !=  j; p = E2[p].next) {
                        k 
=  E2[p].b;
                        
if  (V[k]  ==   1   ||  vst[k]  ==  i1) {ff  =   0 break ;}
                        
if  (vst[k]  !=  i2) {
                            vst[k] 
=  i2; Q2[ ++ rear]  =  k;
                            
if  ( ! V[k]) {ST[len][ 0 =  k; ST[len ++ ][ 1 =   2 ;}
                        }
                    }
                    
if  ( ! ff)  break ;
                }
                
if  (ff) {
                    re(j, len) V[ST[j][
0 ]]  =  ST[j][ 1 ];
                    
continue ;
                }
            }
        }
        
if  (V[_i  <<   1 +  V[(_i  <<   1 +   1 !=   3 ) {res_ex  =   0 break ;}
    }
}
int  main()
{
    
int  _m, a, b;
    
while  (scanf( " %d%d " & _n,  & _m)  !=  EOF) {
        n 
=  _n  <<   1 ; init_d();
        re(i, _m) {
            scanf(
" %d%d " & a,  & b); a -- ; b -- ;
            
if  (a  !=  (b  ^   1 )) {add_edge(a, b  ^   1 ); add_edge(b, a  ^   1 );}
        }
        solve();
        
if  (res_ex) {re(i, n)  if  (V[i]  ==   1 ) printf( " %d\n " , i  +   1 );}  else  puts( " NIE " );
    }
    
return   0 ;
}

【O(M)算法】
根据图的对称性,可以将图中所有的强连通分支全部缩成一个点(因为强连通分支中的点要么都选,要么都不选),然后按照拓扑逆序(每次找出度为0的点,具体实现时,在建分支邻接图时将所有边取反)遍历分支邻接图,将这个点(表示的连通分支)选上,并将其所有对立点(注意,连通分支的对立连通分支可能有多个,若对于两个连通分支S1和S2,点i在S1中,点i'在S2中,则S1和S2对立)及这些对立点的前趋全部标记为不选,直到所有点均标记为止。这一过程中必然不会出现矛盾(详细证明过程省略,论文里有)。
无解判定:若求出所有强分支后,存在点i和i'处于同一个分支,则无解,否则必定有解。
时间复杂度:求强分支时间复杂度为O(M),拓扑排序的时间复杂度O(M),总时间复杂度为O(M)。

该算法的时间复杂度低,但是只能求出任意一组解,不能保证求出解的字典序最小。当然,如果原题不需要求出具体的解,只需要判定是否有解(有的题是二分 + 2-SAT判有解的),当然应该采用这种算法,只要求强连通分支(Kosaraju、Tarjan均可,推荐后者)即可。

【具体题目】 PKU3648(本题的特殊情况非常多,具体见Discuss)
#include  < iostream >
#include 
< stdio.h >
using   namespace  std;
#define  re(i, n) for (int i=0; i<n; i++)
#define  re2(i, l, r) for (int i=l; i<r; i++)
#define  re3(i, l, r) for (int i=l; i<=r; i++)
const   int  MAXN  =   1000 , MAXM  =   10000 , INF  =   ~ 0U   >>   2 ;
struct  edge {
    
int  a, b, pre, next;
} E[MAXM], E2[MAXM];
int  n, n2, m, m2, stk[MAXN], stk0[MAXN], V[MAXN], w[MAXN], st[MAXN], dfn[MAXN], low[MAXN], sw[MAXN], L[MAXN], R[MAXN];
int  V2[MAXN], Q[MAXN], de[MAXN], Q0[MAXN];
bool  vst[MAXN], res[MAXN], res_ex;
void  init_d()
{
    re(i, n) E[i].a 
=  E[i].pre  =  E[i].next  =  i;
    m 
=  n;
}
void  init_d2()
{
    re(i, n2) {E2[i].a 
=  E2[i].pre  =  E2[i].next  =  i; de[i]  =   0 ;}
    m2 
=  n2;
}
void  add_edge( int  a,  int  b)
{
    E[m].a 
=  a; E[m].b  =  b; E[m].pre  =  E[a].pre; E[m].next  =  a; E[a].pre  =  m; E[E[m].pre].next  =  m ++ ;
    de[b]
++ ;
}
void  add_edge2( int  a,  int  b)
{
    E2[m2].a 
=  a; E2[m2].b  =  b; E2[m2].pre  =  E2[a].pre; E2[m2].next  =  a; E2[a].pre  =  m2; E[E[m2].pre].next  =  m2 ++ ;
}
void  solve()
{
    
int  tp, tp0, x0, x, y, ord  =   0 , ord0  =   0 ; n2  =   0 ;
    
bool  fd;
    res_ex 
=   1 ; re(i, n) V[i]  =   0 ;
    re(i, n) 
if  ( ! V[i]) {
        stk[tp 
=   0 =  stk0[tp0  =   0 =  i; V[i]  =   1 ; dfn[i]  =  low[i]  =   ++ ord; st[i]  =  E[i].next;
        
while  (tp0  >=   0 ) {
            x 
=  stk0[tp0]; fd  =   0 ;
            
for  ( int  p = st[x]; p  !=  x; p  =  E[p].next) {
                y 
=  E[p].b;
                
if  ( ! V[y]) {
                    stk[
++ tp]  =  stk0[ ++ tp0]  =  y; V[y]  =   1 ; dfn[y]  =  low[y]  =   ++ ord; st[y]  =  E[y].next; st[x]  =  E[p].next; fd  =   1 break ;
                } 
else   if  (V[y]  ==   1   &&  dfn[y]  <  low[x]) low[x]  =  dfn[y];
            }
            
if  ( ! fd) {
                V[x] 
=   2 ;
                
if  (low[x]  ==  dfn[x]) {
                    L[n2] 
=  ord0;
                    
while  ((y  =  stk[tp])  !=  x) {w[y]  =  n2; sw[ord0 ++ =  y; tp -- ;}
                    w[stk[tp
-- ]]  =  n2; sw[ord0]  =  x; R[n2 ++ =  ord0 ++ ;
                };
                
if  (tp0) {x0  =  stk0[tp0  -   1 ];  if  (low[x]  <  low[x0]) low[x0]  =  low[x];} tp0 -- ;
            }
        }
    }
    re(i, n) 
if  (w[i]  ==  w[i  ^   1 ]) {res_ex  =   0 return ;}
    init_d2();
    re2(i, n, m) {
        x 
=  w[E[i].a]; y  =  w[E[i].b];
        
if  (x  !=  y) add_edge2(y, x);
    }
    
int  front  =   0 , rear  =   - 1 , front0, rear0; re(i, n2) { if  ( ! de[i]) Q[ ++ rear]  =  i; V2[i]  =   0 ;}
    
for  (; front <= rear; front ++ ) {
        
int  i  =  Q[front], j, j0;
        
if  ( ! V2[i]) {
            V2[i] 
=   1 ; front0  =   0 ; rear0  =   - 1 ;
            re(k, n2) vst[k] 
=   0 ;
            re3(x, L[i], R[i]) {
                j 
=  w[sw[x]  ^   1 ]; Q0[ ++ rear0]  =  j; vst[j]  =   1 ;
            }
            
for  (; front0 <= rear0; front0 ++ ) {
                j 
=  Q0[front0]; V2[j]  =   2 ;
                
for  ( int  p = E2[j].next; p  !=  j; p = E2[p].next) {
                    j0 
=  E2[p].b;
                    
if  ( ! vst[j0]) {Q0[ ++ rear0]  =  j0; vst[j0]  =   1 ;}
                }
            }
        }
        
for  ( int  p = E[i].next; p  !=  i; p = E[p].next) {
            j 
=  E[p].b; de[j] -- ;
            
if  ( ! de[j]) Q[ ++ rear]  =  j;
        }
    }
    re(i, n) res[i] 
=   0 ;
    re(i, n2) 
if  (V2[i]  ==   1 ) re3(j, L[i], R[i]) res[sw[j]]  =   1 ;
}
int  main()
{
    
int  n0, m0, x1, x2, N1, N2;
    
char  c1, c2;
    
while  ( 1 ) {
        scanf(
" %d%d " & n0,  & m0);  if  ( ! n0  &&   ! m0)  break else  {n  =  n0  <<   1 ; init_d();}
        re(i, m0) {
            scanf(
" %d%c%d%c " & x1,  & c1,  & x2,  & c2);
            
if  (c1  ==   ' h ' ) N1  =  x1  <<   1 else  N1  =  (x1  <<   1 +   1 ;
            
if  (c2  ==   ' h ' ) N2  =  x2  <<   1 else  N2  =  (x2  <<   1 +   1 ;
            add_edge(N1 
^   1 , N2); add_edge(N2  ^   1 , N1);
        }
        add_edge(
0 1 );
        solve();
        
if  (res_ex) {
            
bool  spc  =   0 ;
            re(i, n) 
if  (i  !=   1   &&  res[i]) {
                
if  (spc) putchar( '   ' );  else  spc  =   1 ;
                printf(
" %d%c " , i  >>   1 , i  &   1   ?   ' w '  :  ' h ' );
            }
            puts(
"" );
        } 
else  puts( " bad luck " );
    }
    
return   0 ;
}

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值