浅谈2—SAT问题

来源:互联网 发布:mac双系统os系统消失 编辑:程序博客网 时间:2024/06/14 15:42

http://blog.csdn.net/pi9nc/article/details/11849843

2-SAT:

1  2 - SAT就是2判定性问题,是一种特殊的逻辑判定问题。
2  2 - SAT问题有何特殊性?该如何求解?
3 我们从一道例题来认识2 - SAT问题,并提出对一类2 - SAT问题通用的解法。
4 
 Poi  0106  Peaceful Commission [和平委员会]:

某国有n个党派,每个党派在议会中恰有2个代表。
现在要成立和平委员会 ,该会满足:
每个党派在和平委员会中有且只有一个代表 
如果某两个代表不和,则他们不能都属于委员会 
代表的编号从1到2n,编号为2a
 - 1 、2a的代表属于第a个党派
 输入n(党派数),m(不友好对数)及m对两两不和的代表编号 
其中1≤n≤
 8000  0 ≤m ≤ 20000  

求和平委员会是否能创立。若能,求一种构成方式。 
输入:     输出:
 3   2         1         
 1   3         4           
 2   4         5                   

 原题可描述为:

有n个组,第i个组里有两个节点Ai, Ai
 '  。需要从每个组中选出一个。而某些点不可以同时选出(称之为不相容)。任务是保证选出的n个点都能两两相容。 
 

(在这里把Ai, Ai
 '  的定义稍稍放宽一些,它们同时表示属于同一个组的两个节点。也就是说,如果我们描述Ai,那么描述这个组的另一个节点就可以用Ai ' 
 初步构图
如果Ai与Aj不相容,那么如果选择了Ai,必须选择Aj‘ ;同样,如果选择了Aj,就必须选择Ai’ 。
   Ai             Aj
 '
    Aj             Ai‘                        
这样的两条边对称

 我们从一个例子来看:
假设4个组,不和的代表为:1和4,2和3,7和3,那么构图:
假设:首先选1 3必须选,2不可选 8必须选,
 4 、7不可选  5 、6可以任选一个

 
 矛盾的情况为:
存在Ai,使得Ai既必须被选又不可选。
 
得到算法1:
枚举每一对尚未确定的Ai, Ai‘ ,任选1个,推导出相关的组,若不矛盾,则可选择;否则选另1个,同样推导。若矛盾,问题必定无解。
此算法正确性简要说明:
由于Ai,Ai
 '  都是尚未确定的,它们不与之前的组相关联,前面的选择不会影响Ai, Ai '  。

算法的时间复杂度在最坏的情况下为O(nm)。
在这个算法中,并没有很好的利用图中边的对称性
 更一般的说:
在每个一个环里,任意一个点的选择代表将要选择此环里的每一个点。不妨把环收缩成一个子节点(规定这样的环是极大强连通子图)。新节点的选择表示选择这个节点所对应的环中的每一个节点.
对于原图中的每条边Ai
 -> Aj(设Ai属于环Si,Aj属于环Sj)如果Si≠Sj,则在新图中连边:Si ->Sj

这样构造出一个新的有向无环图。
此图与原图等价。
 通过求强连通分量,可以把图转换成新的有向无环图,在这个基础上,介绍一个新的算法。

新算法中,如果存在一对Ai, Ai
 ' 属于同一个环,则判无解,否则将采用拓扑排序,以自底向上的顺序进行推导,一定能找到可行解。 
 

至于这个算法的得来及正确性,将在下一段文字中进行详细分析。

回忆构图的过程:
对于两个不相容的点 Ai, Aj,构图方式为:Ai
 -> Aj ' ,Aj->Ai ' ,前面提到过,这样的两条边对称,也就是说:
如果存在Ai
 -> Aj,必定存在Aj ' ->Ai ' 

等价于:Ai
 -> Ak,Ak ' ->Ai '  方便起见,之后“ -> ”代表这样一种传递关系.


 猜测1:图中的环分别对称
如果存在Ai,Aj,Ai,Aj属于同一个环(记作Si),那么Ai
 ' , Aj ' 也必定属于一个环(记作Si ' ). 
 
再根据前面的引理,不难推断出每个环分别对称。 

证明方式与引理相类似
一个稍稍复杂点的结构,其中红、蓝色部分分别为两组对称的链结构
推广2:对于任意一对Si, Si
 '  ,Si的后代节点与Si '  的前代节点相互对称。 
继而提出:
猜测2:若问题无解,则必然存在Ai, Ai
 '  ,使得Ai,Ai ' 属于同一个环。也就是,如果每一对Ai,Ai '  都不属于同一个环,问题必定有解。下面给出简略证明: 
 先提出一个跟算法1相似的步骤: 
如果选择Si,那么对于所有Si
 -> Sj,Sj都必须被选择。 
而Si
 '  必定不可选,这样Si’的所有前代节点也必定不可选(将这一过程称之为删除)。 
 
由推广2可以得到,这样的删除不会导致矛盾。

假设选择S3
 '   
 
选择S3 ' 的后代节点, S1 ' 
删除S3
删除S3的前代节点S1
S1与S1
 ' 是对称的 
 

每次找到一个未被确定的Si,使得不存在Si
 -> Si '  选择Si及其后代节点而删除Si’及Si‘的前代节点。一定可以构造出一组可行解。 
 
因此猜测2成立。

另外,若每次盲目的去找一个未被确定的Si,时间复杂度相当高。
以自底向上的顺序进行选择、删除,这样还可以免去“选择Si的后代节点”这一步。
用拓扑排序实现自底向上的顺序。

一组可能的拓扑序列(自底向上):S1
 ' ,S2,S2 ' ,S3 ' ,S3,S1 
 

算法2的流程: 

 1 .构图
 2 .求图的极大强连通子图
 3 .把每个子图收缩成单个节点,根据原图关系构造一个有向无环图
 4 .判断是否有解,无解则输出(退出)
 5 .对新图进行拓扑排序
 6 .自底向上进行选择、删除
 7 .输出

小结:
整个算法的时间复杂度大概是O(m),解决此问题可以说是相当有效了。
在整个算法的构造、证明中反复提到了一个词:对称。发现、利用了这个图的特殊性质,我们才能够很好的解决问题。
并且,由2
 - SAT问题模型变换出的类似的题目都可以用上述方法解决。 

全文总结:
充分挖掘图的性质,能够更好的解决问题。
不仅仅是对于图论,这种思想可以在很多问题中得到很好的应用。
希望我们能掌握此种解题的思想,在熟练基础算法的同时深入分析、灵活运用、大胆创新,从而解决更多更新的难题。

2-sat问题

Implication_graph

在这篇文章我们提到过sat问题,sat问题是第一个npc问题,具体是这样的SAT全称是satisfiability,他是问对于一个合取范式,是否有一种输入使得他的输出是1,具体点就是类似这样的布尔表达式(x1 or x2 or x3)and(x3 or x4)and(not x1 or x5)对于所有的x是否有一种01取值,使得最后的结果是1。而2-sat问题就是每一个由or连接的子式都只包含两个变量,比如这样(x1 or x2) and (not x3 or x1),2-sat问题是有多项式解法的,而3-sat就是npc问题了,前段时间有人宣称证明了p=np就是因为他自己找到了3-sat的多项式解法,当然最后被证明解法是错误的。。。那么对于2-sat问题解法而言,经典的就是利用强连通分支的算法来解决,最近上的coursera上的algo2有个随机算法也很有趣这里我们也要讲一下。

先来看经典的利用强连通分支的图论解法。我们把每个变量x都拆成2个点,两个点x和~x分别表示这个点取1和这个点取0,所以最后我们就是在这2n个点中选择满足要求的n个点。对于每个子式,假设子式是(x1 or x2),对于2-sat问题而言我们需要每个子式都得1,也就是对于这个子式而言,x1和x2至少要取一个,对应于图中就是,如果我们取~x1就必须取x2,取~x2就必须取x1,所以我们就在图中从~x1到x2和从~x2到x1连两条有向边。同样的如果子式中有not也是类似方法,比如(not x1 or x2)那么就是X1到x2和~x2到~x1两条有向边。一开始的图片构成的图表示的这个式子。

(x0x2)(x0¬x3)(x1¬x3)(x1¬x4)(x2¬x4)(x0¬x5)
(x1¬x5)(x2¬x5)(x3x6)(x4x6)(x5x6)

构建好图之后对图求强连通分支,很显然的如果xi和~xi在同一个强连通分支中那么就是不存在解的。之后进行染色判定,强连通分支缩点之后,把所有的边反向,然后按照拓扑排序的顺序遍历节点,如果节点没有被染色,就涂成红色,然后把和这个点互斥的点(所谓互斥的点就是如果x和~x所在的点),以及这个点的子孙都涂成蓝色,这样取出红色的点就是满足条件的解。这里就不证明了,详细的可以看伍昱的《由对称性解2-SAT问题》和赵爽的《2-SAT解法浅析》两篇。看证明的时候注意对称性,就是说如果x,y在同一个连通分支,那么~x,~y也在同一个连通分支,如果x到y有路,那么~y到~x也有路,注意这个对称性的特点的话,那两篇文章里的证明也就不难看懂了。

talk is easy,show me the code,那么让我们看一下代码吧,这里就用poj 3648来举例。

  1. #include <cstdio>
  2. #include <cstring>
  3.  
  4. const int maxn=10010;
  5. int n, m, nxt[maxn], head[maxn], pnt[maxn], ne, e, a, b, a0, b0;
  6. char c1, c2;
  7. int nnxt[maxn], nhead[maxn], npnt[maxn];
  8. int order[maxn], norder, id[maxn], v[maxn];
  9. int ans[maxn], op[maxn];
  10.  
  11. void dfs(int d){
  12.     v[d] = 1;
  13.     for(int i=head[d]; i!=-1; i=nxt[i])
  14.         if(!v[pnt[i]])
  15.             dfs(pnt[i]);
  16.     order[norder++] = d;
  17. }
  18. void ndfs(int d, int k){
  19.     v[d] = 1;
  20.     id[d] = k;
  21.     for(int i=nhead[d]; i!=-1; i=nnxt[i])
  22.         if(!v[npnt[i]])
  23.             ndfs(npnt[i], k);
  24. }
  25. void addedge(int s, int t){
  26.     pnt[e] = t; nxt[e] = head[s]; head[s] = e++;
  27. }
  28. void addnedge(int t, int s){
  29.     npnt[ne] = s; nnxt[ne] = nhead[t]; nhead[t] = ne++;
  30. }
  31. void color(int d){
  32.     ans[d] = 2;
  33.     for(int i=head[d]; i!=-1; i=nxt[i])
  34.         if(!ans[pnt[i]])
  35.             color(pnt[i]);
  36. }
  37. int main(){
  38.     while(1){
  39.         norder = e = ne = 0;
  40.         memset(head, -1sizeof head);
  41.         memset(nhead, -1sizeof nhead);
  42.         scanf("%d%d"&n, &m);
  43.         if(!n&&!m)
  44.             break;
  45.         for(int i=0; i<m; ++i){
  46.             scanf("%d%c%d%c"&a, &c1, &b, &c2);
  47.             if(c1=='h')
  48.                 a0 = n+a;
  49.             else{
  50.                 a0 = a;
  51.                 a += n;
  52.             }
  53.             if(c2=='h')
  54.                 b0 = n+b;
  55.             else{
  56.                 b0 = b;
  57.                 b += n;
  58.             }
  59.             addedge(a0, b);
  60.             addnedge(b, a0);
  61.             addedge(b0, a);
  62.             addnedge(a, b0);
  63.         }
  64.         addedge(0, n);
  65.         addnedge(n, 0);
  66.         memset(v, 0sizeof v);
  67.         for(int i=0; i<2*n; ++i)
  68.             if(!v[i])
  69.                 dfs(i);
  70.         int k=0;
  71.         memset(v, 0sizeof v);
  72.         memset(id, -1sizeof id);
  73.         for(int i=norder-1; i>=0--i)
  74.             if(!v[order[i]])
  75.                 ndfs(order[i], k++);
  76.         int mark = 1;
  77.         for(int i=0; i<n; ++i)
  78.             if(id[i]==id[i+n])
  79.                 mark = 0;
  80.         if(!mark){
  81.             printf("bad luck\n");
  82.             continue;
  83.         }
  84.         for(int i=0; i<n; ++i){
  85.             op[id[i]] = id[i+n];
  86.             op[id[i+n]] = id[i];
  87.         }
  88.         e = norder = 0;
  89.         memset(head, -1sizeof head);
  90.         memset(v, 0sizeof v);
  91.         memset(ans, 0sizeof ans);
  92.         for(int i=0; i<n; ++i)
  93.             for(int j=nhead[i]; j!=-1; j=nnxt[j])
  94.                 addedge(id[i], id[npnt[j]]);
  95.         for(int i=0; i<k; ++i)
  96.             if(!v[i])
  97.                 dfs(i);
  98.         for(int i=norder-1; i>=0--i)
  99.             if(!ans[order[i]]){
  100.                 ans[order[i]] = 1;
  101.                 color(op[order[i]]);
  102.             }
  103.         for(int i=1; i<n; ++i){
  104.             if(ans[id[i]] == 1)
  105.                 printf("%dh", i);
  106.             else
  107.                 printf("%dw", i);
  108.             if(i<n-1)
  109.                 printf(" ");
  110.             else
  111.                 printf("\n");
  112.         }
  113.     }
  114.     return 0;
  115. }
代码自我感觉还是比较清晰的就不解释了,整个过程就是按照上面说的算法进行的。
0 0
原创粉丝点击