槛内人分享 http://blog.sciencenet.cn/u/cpcs

博文

2-SAT问题

已有 16000 次阅读 2010-5-17 13:51 |个人分类:技术|系统分类:科研笔记| 算法, ACM, 程序设计竞赛

        初始2-SAT那时还是在研一时上了一门叫做计算复杂性的课,教材是一本英文的书,很难。当时,第一次接触了不可判定的问题,疯狂地看了很多东西。又系统地学习了复杂度类P,NP(C),受益颇多。3-SAT就是NPC了,而2-SAT确是P的。
 
       所谓2-SAT就是2元可满足性问题。首先,作为众所周知的,任何布尔表达式,都可以化为合取范式的形式,即化为:   () and () and () ...and () 其中括号里面的是用析取符号or 连接的变量或者变量的非的形式。我们一般称,变量或者变量的非为“文字”,而括号里的叫做子句。可满足性问题是要给所有的变量一个赋值(真或假)使得表达式值为真。而2元可满足性问题,就是化为合取范式后,每个子句最多有两个文字的可满足性问题。
 
      cook定理已经说明,一般地可满足性问题是NPC的。但是值得注意的是,2-SAT却是P的。简单分析一下2-SAT: 它要求每个子句的值都是真。考虑一个子句,a or b ,显然如果a取false,则b一定取true,反之亦然。先给出算法:
 
      假设2-SAT问题中有N个变量,则构造一个有向图包含2n个节点。每个节点代表一个变量或者变量的非(即代表一个文字)。对每个子句a or b,在途中加2条有向边 (~a,b)和(~b,a)  (其中~表示变量的非)。然后对这个有向图求强连通分量,把每个强连通分量看作一个点(缩点),这样新的有向图是无环的(无环有向图又叫DAG)。如果一个强连通分量里同时包含了同一个变量以及它的非,则原问题是不可满足的。否则,我们可以构造解,对这个DAG可以进行拓扑排序,按照节点的拓扑排序顺序的倒序进行如下操作:找到此顺序中第一个没有标记的节点,把此节点标记成true,并且把和此节点矛盾的节点(一个节点包含了a,另外一个节点包含了~a,则称它们是矛盾的),以及祖先标记成false,直到所有节点都有标记,结束。这些标记也对应着变量的取值。
 
     大概不严谨地证明一下,首先同一个强连通分量里的文字是等价的。因为对一个子句a or b,按照一个取假,另外一个一定取真的意思,连的边表示逻辑中的“蕴含”关系,即有~a->b  ,~b->a。这样一个强连通分量里的文字互相蕴含,所以是等价的。于是,当存在a->~a->a时,显然原问题不可满足。
 
     第二,因为每标记一个true,立刻把与它矛盾的并且“反向蕴含”的点都设置为false,所以,这样的赋值不会产生矛盾。
 
      第三,不会把一个变量和它的非都设置为false。
 
    这是因为,考虑设置变量为false时,假设设置a为true,沿着“反向蕴含链”把x设置为false了,这说明有x->~a,由于对称性,有a->~x  (这里的->表示有路相连,并不一定是一条边),按照算法流程必须要先考虑~x (拓扑排序的逆序),可见这时~x应该已经被设置为true了,因为否则的话,如果~x=false,则会沿着“反向蕴含链”把a也设置为false的,根本不会到考虑a为true的这一步骤了。于是,设置false是安全的。
 
    综上所述,该算法可以找到一组2-SAT的可行解,或者判断无解。该算法的复杂度:求强连通分量、拓扑排序、以及遍历图都是O(m)的,其中m是边数。
 
    从网上找了段2-SAT的C++类的代码,修改了一下:
 
 
 
 
 
 
#include
 
#include
 
#include
 
#include
 
using namespace std;
 
 
 
const int N = 5050; //最大人数
 
const int inf=6000001;
 
class SAT
 
{
 
 private:
 
  vector g[N]; //原图边连接情况
 
  int n, M, h[N], id[N]; //id[]表示原图中每个点都属于哪个强连通分量
 
  int cnt, scnt, dfn[N], low[N], cur[N];
 
  int stack[N], top, est[N], etop;
 
  vector tree[N]; //有向无环图的边连接情况(新图)
 
  vector contain[N]; //新图中每个点都包含原图中的哪些点
 
  void dfs(int);
 
  void tsDfs(int);
 
  void topologicalSort();
 
  void colorDfs(int);
 
  void color();
 
  void tagAnswer();
 
  void printAnswer();
 
  void getOneAnswer();
 
  void buildGraph();
 
 public:
 
  void scR();
 
  void build(int);
 
  bool judge();
 
  bool solve();
 
  void make(int,int);
 
  
 
};
 
/*
 
函数build是对原图初始化(根据实际输入情况做相应的更改)
 
*/
 
void SAT::make(int x,int y) { // x || y      负数表示非 1<=|x| , |y| <=M   注意从1开始
 
int x1,x2,y1,y2;
 
 if (x > 0) {
 
  x1 = x - 1;
 
  x2 = x1 + M;
 
 }
 
 else {
 
  x2 = -x - 1;
 
  x1 = x2 + M;
 
 }
 
 if (y > 0 ) {
 
  y1 = y - 1;
 
  y2 = y1 + M;
 
 }
 
 else {
 
  y2 = -y - 1;
 
  y1 = y2 + M;
 
 }
 
 g[x2].push_back(y1);
 
 g[y2].push_back(x1);
 
}
 
 
void SAT::build(int num)   //变量个数
 
{
 
 int i;
 
 M = num;
 
 n = M << 1;
 
 for (i = 0 ; i < n ; ++i) {
 
  g[i].clear();
 
 }
 
 
 
}
 
void SAT::dfs(int src)
 
{
 
 etop = top = 0;
 
 stack[top++] = src;
 
 while(top != 0)
 
 {
 
  int c = stack[top-1];
 
  if(dfn[c] == -1)
 
  {
 
   h[c] = dfn[c] = low[c] = cnt++;
 
   est[etop++] = c;
 
  }
 
  for(; cur[c] >= 0; cur[c]--)
 
  {
 
   int no = g[c][cur[c]];
 
   if(dfn[no] == -1)
 
   {
 
    stack[top++] = no;
 
    break;
 
   }
 
   if (h[c]>low[no]) {
 
    h[c] = low[no];
 
   }
 
  
 
  }
 
  if(cur[c] >= 0)
 
   continue;
 
  top--;
 
  int k;
 
  if(h[c] != low[c])
 
  {
 
   low[c] = h[c];
 
   continue;
 
  }
 
  do
 
  {
 
   k = est[--etop];
 
   id[k] = scnt; low[k] = N;
 
  } while(k != c);
 
  scnt++;
 
 }
 
}
 
/*
 
函数scR和dfs是求原图的强连通分量(代码由wywcgs原创)
 
*/
 
void SAT::scR()
 
{
 
 int i;
 
 memset(dfn, 0xff, sizeof(dfn));
 
 cnt = scnt = 0;
 
 for(i = 0; i < n; i++)
 
 {
 
  cur[i] = g[i].size()-1;
 
  contain[i].clear();
 
 }
 
 for(i = 0; i < n; i++)
 
  if(dfn[i] == -1)
 
   dfs(i);
 
 
 
 /*
 
 统计每个强连通分量都包含哪些点,为后面求可行方案做准备。如果不求可行解,可注释掉。
 
 */
 
 for ( i=0;i
 
 {
 
  contain[id[i]].push_back(i);
 
 }
 
}
 
/*
 
函数judge判断是否能找出一个可行方案出来
 
*/
 
bool SAT::judge()
 
{
 
 int i;
 
 for ( i=0;i
 
  if (id[i]==id[i + M])
 
   return false;
 
 return true;
 
}
 
/*
 
函数buildGraph把每个强连通分量作为一个点,重新构图。(缩点,得到的是一个有向无环图)
 
用的是链接表的形式,可能有很多重边。可以加一些预处理消除重边。
 
*/
 
void SAT::buildGraph()
 
{
 
 int i,j;
 
 for (i=0;i
 
  tree[i].clear();
 
 for (i=0;i
 
  for (j=0;j
 
  {
 
   int a=id[i];
 
   int b=id[g[i][j]];
 
   if (a!=b)
 
   {
 
    tree.push_back(a);
 
   }
 
  }
 
}
 
void SAT::tsDfs(int k)
 
{
 
 dfn[k]=cnt++;
 
    for (int i=0;i
 
    {
 
        int w=tree[k][i];
 
        if (dfn[w]==-1)
 
        {
 
            tsDfs(w);
 
        }
 
    } 
 
 low[scnt++]=k;            
 
}
 
/*
 
函数topologicalSort和tsDfs是对新图进行拓扑排序,排序后的结果存在low数组中
 
*/
void SAT::topologicalSort()
{
 int i;
 for ( i=0;i<scnt;i++)
 {
  dfn[i]=-1;
  low[i]=-1;
 }
 int nn=scnt;
    cnt=scnt=0;
    for ( i=0;i<nn;i++)
    {
        if (dfn[i]==-1)
            tsDfs(i);
 }
}
 
/* 
函数color和colorDfs是对新图进行着色,新图中着色为1的点组成一组可行解
 
*/
 
void SAT::color()
 
{
 
 int i;
 
 for ( i=0;i
 
     dfn[i]=-1;
 
    for ( i=scnt-1;i>=0;i--)
 
     if (dfn[low[i]]==-1)
 
     {
 
   /*
 
   新图中low[i]着色为1后,它的矛盾点应标记为2
 
   */
 
      int a=contain[low[i]][0]; //在原图中找一点属于强连通分量low[i]的点a,点a所属组的另一点b所属的强连通分量id一定是low[i]矛盾点。
 
      int b;
 
      if (a >= M)
 
       b= a - M;
 
      else
 
       b= a + M;
 
      dfn[low[i]]=1;
 
      if (dfn[id]==-1)
 
       colorDfs(id); //由于依赖关系,有id能达的点都是low[i]的矛盾点
 
  }
 
}
 
/*
 
函数tagAnswer由新图对原图的点进行标记,得到原图的可行解
 
*/
 
void SAT::tagAnswer()
 
{
 
 int i,j;
 
 for ( i=0;i
 
  low[i]=-1;
 
 for (i=0;i
 
  if (dfn[i]==1)//i为新图中可行解包含的点,那么原图中强连通分量属于i的点都是原图中可行解的点
 
  {
 
   for ( j=0;j
 
    low[contain[i][j]]=1;
 
  }
 
}
 
/*
 
函数printAnswer打印原图的可行解
 
*/
 
 
 
void SAT::printAnswer() {
 
 
 
 
}
 
/*
 
函数getOneAnswer得到原图的一组可行解
 
*/
 
void SAT::getOneAnswer()
 
{
 
 buildGraph();
 
 topologicalSort();
 
 color();
 
 tagAnswer();
 
}
 
/*
 
函数solve可根据实际要求,进行更改输出 */
 
bool SAT::solve()
 
{
 
 scR();
 
 if (judge())
 
 {
 
  //puts("YES");
 
 
 
  //getOneAnswer();
 
  //printAnswer();
 
  return true;
 
 }
 
 else {
 
 // puts("NO");
 
  return false;
 
 }
 
}
 
   
 
   用法: 先build(n) n是变量的个数,再通过make(i,j) 加入子句 xi or xj  (负数表示变量取反 i,j是正整数 )
 
               solve判断有无解,getOneAnswer得到一组赋值,printOneAnswer根据情况输出一组解
 
   可优化的地方:可以用set存放临接表,进而去除重边。拓扑排序可以写成非递归的,而不用递归的dfs……
 
   小技巧:
 
        表示xor 关系的表达 如果要表示 a xor b = true 即 a b有且仅有一个为true 则可以表达为2个子句:
 
       a or b 和 ~a or ~b
 
       表示同或关系  即a xor b = false 即a=b 则可表达为两个子句:
 
     ~a or b 和 a or ~b
 
       表示 a and b = true  即两个都是true
 
       可以表达为2个子句 a or a  和b or b
 
       表示 a and b = false 即至少有一个是false
 
      可以表达为: ~a or ~b
 
     表示 a or b = false 即全是false
 
     可表达为 ~a or ~a和~b or ~b
 
  表示 a为true时 b不能为false
 
  可以表达为 ~a or b
 
……
 
作为acm算法的训练题:
 
 
 
 
 
 
 
 
 
 
  
 
    
 
 
 
            
 
               
 
 
 
     
 
      
 
 
 
    
 


https://blog.sciencenet.cn/blog-434885-325398.html

上一篇:转眼间
收藏 IP: .*| 热度|

0

发表评论 评论 (2 个评论)

数据加载中...

Archiver|手机版|科学网 ( 京ICP备07017567号-12 )

GMT+8, 2024-11-8 13:32

Powered by ScienceNet.cn

Copyright © 2007- 中国科学报社

返回顶部