返回   cpper编程论坛 > Blog > bankrock
注册账号 论坛帮助 会员列表 日历事件 搜索 今日新帖 标记版面已读

为这篇文章评分

2-CNF-SAT问题图形解法的C++实现

发表于 2008-07-15 04:40 PM 作者: bankrock
2-CNF-SAT问题图形解法的C++实现
CNF(conjunctive normal form)是指由多个子句(clause)通过AND关系连接组成的布尔公式,每个子句由若干个文字(literal)通过OR关系连接而成,每个literal可能是一个布尔变量或者布尔变量取反。2-CNF中每个clause含有2个或两个以下的literal,而2-CNF-SAT(2-conjunctive normal form satisfiability)问题就是找出一个2-CNF公式是否有对布尔变量赋值的方法使2-CNF公式的最终结果为1。例如:
引用:
(x1 OR !x2) AND (x1 OR x2) (这里!表示NOT)
就是一个2-CNF公式,如果取x1 = 1, x2 = 0,它的值为(1 OR 1) AND (1 OR 0) = 1,因此这是一个可以满足2-CNF的变量赋值,这个公式属于2-CNF-SAT集。这里介绍一种图形算法解决2-CNF-SAT问题的方法以及C++实现。

一.2-CNF-SAT图形算法以及证明
3-CNF-SAT问题是NP-Complete的,也就是不大可能出现多项式时间复杂度的解法。但是2-CNF问题却是多项式可解的。下面的解法将2-CNF-SAT问题转化为求解一个图的强连通区域是否符合一定性质,从而判断2-CNF公式是否有解,如果解存在的话还将给出一个可行解。
2-CNF中一个含有两个literal的clause(l1 OR l2)与(!l1 -> l2)是等价的(这里->是指IF-THEN关系),两式中l1、l2以及结果值value都符合以下的真值表:
引用:
+-----+-----+-----+
|l1 |l2 |value|
+-----+-----+-----+
|1 |1 |1 |
+-----+-----+-----+
|1 |0 |1 |
+-----+-----+-----+
|0 |1 |1 |
+-----+-----+-----+
|0 |0 |0 |
+-----+-----+-----+
因此一个clause可以用IF-THEN关系代替,其他clause中可能出现!l1或者!l2,它们的关系也必须有所表示,这可以通过对上述IF-THEN关系取逆反得到。也就是:
引用:
(l1 OR l2) <==> (!l1 -> l2),(!l2 -> l1)
现在开始将2-CNF转化为一个有向图。对于每个变量x建立对应literal x和!x的端点,将clause得出的每个IF-THEN关系转化为图中端点间的一条边,这里称这个有向图为G。可以看出,当2-CNF有满足结果为1的解时,它的所有clause的值都需要为1,因此由clause得出的IF-THEN关系也必须成立,也就是对于G中的每一条边,起始点和终点的literal值需要符合IF-THEN关系;同时,如果G中所有边的起始点和终点的literal值符合IF-THEN关系,并且它们推导出的变量值不相互冲突(例如x1和!x1不会都赋值为0,或者1),那么这个变量赋值是一个满足2-CNF公式的可行解。
接下来就需要找到是否存在满足图G中的边所代表的IF-THEN关系的赋值方法。方法如下:搜索图G的所有强连通区域,如果将强连通区域收缩为一个点,则和连接这些强连通点之间边将形成一个无环有向图G'。
1.如果某个强连通区域中有互为非的literal存在,则不可能存在符合这些边所代表的IF-THEN关系,2-CNF无解。这是因为此时强连通区域内必定有1和0同时存在,由于IF-THEN关系具有传递性,如果存在1则整个强连通区域所有的literal值都需为1才能满足IF-THEN关系。因此这些IF-THEN关系无法满足。同时可以得出一个强连通区域内要么所有Literal的值都为1,要么都为0。
2.如果没有强连通区域包含互非的literal,那么根据无环有向图G'的拓扑排序关系,可以确定任意两个互非的literal l和!l间的先后关系,将在前的literal值置0,在后的literal置1,可以得出符合IF-THEN关系并且变量赋值不相互冲突的一个2-CNF解。由于0和1互非,明显literal对应的变量值不会相互冲突。因此只需证明这个赋值方法符合IF-THEN关系,证明分为两部分,一为强连通区域内的关系,二为强连通区域之间的关系:
(1)上面说过满足强连通区域内关系只可能有两种情况,literal都为1或者都为0。现假设使用的赋值方法使得某个强连通区域内出现了两个不同值的literal,l1 = 1、l2 = 0,那么在拓扑排序之前的某个强连通区域F中有!l1 = 0,之后的某个强连通区域B内有!l2 = 1。由于我们构造图G的方法决定如果有边(l1,l2),那么必然有边(!l2,!l1),因此由l1到l2有通路可以得出!l2到!l1有通路,得出拓扑排序中B在F之前的矛盾结论,因此强连通区域不可能有不同值的Literal。
(2)强连通区域间违反IF-THEN关系的情况如下:区域A到B是有通路的,但是区域A值为1而区域B为0,用反证法可以证明这种情况在采用指定的赋值方法时是不可能出现的。假设存在这么两个强连通区域A,B,且literal a属于A,literal b属于B,那么!a所属的强连通区域C值为0在A之前,!b所属的强连通区域D值为1在B之后,且由于a可以到达b,!b也可以到达!a,那么存在D>C>A>B>D的循环拓扑排序关系,这说明强连通区域间不可能出现违反IF-THEN关系的情况。
至此我们已经证明了该算法可以有效性,而且根据这个算法可以构造出可行的2-CNF解。至于形成强连通区域的算法可以在O(V+E)的时间内完成,因此整个算法是多项式时间复杂度的。

二.2-CNF-SAT算法的C++实现
实现该算法的关键之处在于如何寻找G的强连通区域,以及检查是否有两个互非的literal存在于同一个强连通区域中。寻找强连通区域的算法由以下几个步骤组成:
1. 对G进行深度优先搜索,记录下每个节点u搜索完毕的时间f[u]。
2. 对G的节点按照搜索完毕时间f由大到小排序,并形成G的转置图Gt(转置图是由G的所有边方向都调转形成的)。
3. 对Gt按排序好节点顺序进行深度优先搜索,此时每次搜索到的点组成的集合就为一个强连通区域。在搜索过程中可以检查是否有互非的literal出现在同一个强连通区域中。
简单解释一下:这个算法第2步的作用相当于为G的强连通区域进行拓扑排序,由于G和Gt的强连通区域相同,第3步按照拓扑排序搜索Gt时,每次搜索得到一个强连通区域,而不会搜索到其他区域(由于Gt所有的边已经反转,按照拓扑排序关系,如果搜索到其他区域,那么必定是已经搜索过的)。具体的证明请看算法导论关于强连通区域的章节。
表示2-CNF的类如下:
C++ 代码:
  1. struct Literal
  2. {
  3.     Literal() : pSmbl(0) {}
  4.  
  5.     Literal(Symbol* pSymbol, bool isNotSymbol)
  6.         : pSmbl(pSymbol), isNotSmbl(isNotSymbol)
  7.     {}
  8.  
  9.     bool Value() const
  10.     { return isNotSmbl ? !pSmbl->value : pSmbl->value; }
  11.  
  12.     Literal operator!() const
  13.     { return Literal(pSmbl, !isNotSmbl); }
  14.  
  15.  
  16.     Symbol* pSmbl;
  17.     bool isNotSmbl;
  18.  
  19. friend bool FindSCCAndCheckSatisfiability(LtrlGraph& transGraph, std::vector<StrCnnCmpnt>& sccs);
  20. private:
  21.     bool SetSymbolValue(StrCnnCmpnt *pSCC);
  22. };
  23.  
  24. ////////////////////////////////////
  25. struct Clause
  26. {
  27.     Clause(Literal const& literal1, Literal const& literal2, size_t numDummyLiterals)
  28.         : numDummyLtrl(numDummyLiterals)
  29.     {
  30.         ltrls&#91;0] = literal1;
  31.         ltrls&#91;1] = literal2;
  32.     }
  33.     Literal ltrls&#91;2];
  34.     size_t numDummyLtrl;
  35. };
  36.  
  37. struct TwoCNF
  38. {
  39.     std::vector<Clause> clauses;
  40.     std::set<Symbol> symbols;
  41.     std::vector<Symbol*> dummySmbls;
  42. };
其中TwoCNF中的dummySmbls指的是当clause中的literal数目小于2时生成的伪布尔变量。例如 l == (l OR RANDOM_SYMBOL) AND (l OR !RANDOM_SYMBOL),这里的RANDOM_SYMBOL就是一个伪布尔变量。
表示由2-CNF生成的图类如下:
C++ 代码:
  1. /////////////////////////////////////////
  2. struct Node
  3. {
  4.     Node(Literal const& literal) : state(UNSEARCHED), ltrl(literal), pNewNode(0) {}
  5.     Node(Node const& other) : state(UNSEARCHED), tBgnSrch(other.tBgnSrch),
  6.         tEndSrch(other.tEndSrch), ltrl(other.ltrl), pNewNode(0)
  7.     {}
  8.  
  9.     enum STATE { SEARCHED, SEARCHING, UNSEARCHED } state;
  10.     size_t tBgnSrch, tEndSrch;
  11.     Literal ltrl;
  12.     std::vector<Node*> cnnNodes;
  13.  
  14. friend void TransposeAndSortGraph(LtrlGraph& graph, LtrlGraph& transGraph);
  15. private:
  16.     Node *pNewNode; //Used for transposing the graph
  17. };
  18.  
  19. struct LtrlGraph
  20. {
  21.     std::list<Node> nodes;
  22. };
上述算法的C++实现分为三部分,第一部分为深度优先搜索算法:
C++ 代码:
  1. void DepthFirstSearch(LtrlGraph& graph)
  2. {
  3.     InitNodesState(graph);
  4.  
  5.     stack<Node*> searchNodes;
  6.  
  7.     size_t time = 0;
  8.     for (list<Node>::iterator itorNode = graph.nodes.begin();
  9.         itorNode != graph.nodes.end(); ++itorNode)
  10.     {
  11.         Node& node = *itorNode;
  12.         if (node.state != Node::UNSEARCHED)
  13.             continue;
  14.  
  15.         searchNodes.push(&node);
  16.         while (!searchNodes.empty())
  17.         {
  18.             Node& node = *searchNodes.top();
  19.             if (node.state == Node::UNSEARCHED)
  20.             {
  21.                 node.state = Node::SEARCHING;
  22.                 node.tBgnSrch = ++time;
  23.                 for (vector<Node*>::iterator itorCnnNode = node.cnnNodes.begin();
  24.                     itorCnnNode != node.cnnNodes.end(); ++itorCnnNode)
  25.                 {
  26.                     Node *pCnnNode = *itorCnnNode;
  27.                     if (pCnnNode->state == Node::UNSEARCHED)
  28.                         searchNodes.push(pCnnNode);
  29.                 }
  30.             }
  31.             else if (node.state == Node::SEARCHING)
  32.             {
  33.                 node.state = Node::SEARCHED;
  34.                 node.tEndSrch = ++time;
  35.                 searchNodes.pop();
  36.             }
  37.             else
  38.             {
  39.                 //The node has already been searched, just pop it from the stack
  40.                 searchNodes.pop();
  41.             }
  42.         }
  43.     }
  44. }
这里使用栈searchNodes记录了搜索节点的顺序,以避免递归调用。searchNodes中的点如果未搜索,则将它标记为正搜索状态,然后搜寻所有相邻的未搜索点将其加入searchNodes中;如果该点的状态为正在搜索,那么说明它的所有子节点都已经搜索完毕,记录下搜索完毕时间并退栈;如果是以搜索状态,说明已经被纳入其他节点的搜索范围,直接退栈。这一步完成后我们得到了所有节点的搜索完毕时间。
接下来需要生成转置图Gt,并且按照搜索完毕时间排序节点。算法如下:
C++ 代码:
  1. struct SortingNode
  2. {
  3.     SortingNode(size_t endSearchTime, Node *pOrgNode)
  4.         : tEndSrch(endSearchTime), pNode(pOrgNode)
  5.     {}
  6.  
  7.     size_t tEndSrch;
  8.     Node *pNode;
  9. };
  10.  
  11. inline bool GreaterEndSearchTime(SortingNode const& n1, SortingNode const& n2)
  12. {
  13.     return n1.tEndSrch > n2.tEndSrch;
  14. }
  15.  
  16. void TransposeAndSortGraph(LtrlGraph& graph, LtrlGraph& transGraph)
  17. {
  18.     //Sort nodes by their end search time
  19.     vector<SortingNode> sortingNodes;
  20.     for (list<Node>::iterator itorNode = graph.nodes.begin();
  21.         itorNode != graph.nodes.end(); ++itorNode)
  22.     {
  23.         Node& node = *itorNode;
  24.         sortingNodes.push_back(SortingNode(node.tEndSrch, &node));
  25.     }
  26.     sort(sortingNodes.begin(), sortingNodes.end(), &GreaterEndSearchTime);
  27.  
  28.     //Add new nodes in the transposed graph
  29.     for (vector<SortingNode>::iterator itorSN = sortingNodes.begin();
  30.         itorSN != sortingNodes.end(); ++itorSN)
  31.     {
  32.         transGraph.nodes.push_back(*(itorSN->pNode));
  33.         itorSN->pNode->pNewNode = &transGraph.nodes.back();
  34.     }
  35.  
  36.     //Transpose the graph
  37.     for (list<Node>::iterator itorNode = graph.nodes.begin();
  38.         itorNode != graph.nodes.end(); ++itorNode)
  39.     {
  40.         Node& node = *itorNode;
  41.         for (vector<Node*>::iterator itorCnnNode = node.cnnNodes.begin();
  42.             itorCnnNode != node.cnnNodes.end(); ++itorCnnNode)
  43.         {
  44.             Node* pCnnNode = *itorCnnNode;
  45.             pCnnNode->pNewNode->cnnNodes.push_back(node.pNewNode);
  46.         }
  47.     }
  48. }
这里先使用了一个辅助排序的结构SortingNode按搜索完毕时间排序节点,然后对原图每个节点和连接节点形成的边,反转连接节点对该节点的边。
最后一步需要搜索强连通区域以及对变量赋值,同时检查是否有互非的两个literal在同一个强连通区域中。
C++ 代码:
  1. //Strongly Connected Component
  2. struct StrCnnCmpnt
  3. {
  4.     size_t rank;
  5.     std::vector<Literal> literals;
  6. };
  7.  
  8. bool FindSCCAndCheckSatisfiability(LtrlGraph& transGraph, vector<StrCnnCmpnt>& sccs)
  9. {
  10.     stack<Node*> searchNodes;
  11.     size_t strCnnCmptRank = 0//Lower number means higher rank
  12.     for (list<Node>::iterator itorNode = transGraph.nodes.begin();
  13.         itorNode != transGraph.nodes.end(); ++itorNode)
  14.     {
  15.         Node& node = *itorNode;
  16.         if (node.state != Node::UNSEARCHED)
  17.             continue;
  18.  
  19.         //Create a strong connected component
  20.         sccs.push_back(StrCnnCmpnt());
  21.         StrCnnCmpnt& scc = sccs.back();
  22.         scc.rank = ++strCnnCmptRank;
  23.  
  24.         searchNodes.push(&node);
  25.         while (!searchNodes.empty())
  26.         {
  27.             Node& node = *searchNodes.top();
  28.             if (node.state == Node::UNSEARCHED)
  29.             {
  30.                 node.state = Node::SEARCHING;
  31.                 for (vector<Node*>::iterator itorCnnNode = node.cnnNodes.begin();
  32.                     itorCnnNode != node.cnnNodes.end(); ++itorCnnNode)
  33.                 {
  34.                     Node *pCnnNode = *itorCnnNode;
  35.                     if (pCnnNode->state == Node::UNSEARCHED)
  36.                         searchNodes.push(pCnnNode);
  37.                 }
  38.             }
  39.             else if (node.state == Node::SEARCHING)
  40.             {
  41.                 node.state = Node::SEARCHED;
  42.                 if (!node.ltrl.SetSymbolValue(&scc))
  43.                 //A symbol and its complement is in the same strong connected component,
  44.                 //Therefore the symbol value is undefined. No solution is available.
  45.                     return false;
  46.                 else
  47.                     scc.literals.push_back(node.ltrl);
  48.                 searchNodes.pop();
  49.             }
  50.             else
  51.             {
  52.                 searchNodes.pop();
  53.             }
  54.         }
  55.     }
  56.     return true;
  57. }
这部分算法的主体也是深度优先搜索,但是每个节点搜索完毕后,需要调用node.ltrl.SetSymbolValue(&scc)检查literal所属的强连通区域是否和它互非的literal所属的强连通区域相同,如果相同则2-CNF-SAT无解。Literal类的SetSymbolValue方法如下:
C++ 代码:
  1. bool Literal::SetSymbolValue(StrCnnCmpnt *pSCC)
  2. {
  3.     assert(pSCC != 0 && (pSmbl->cntSCC.pSCC == 0 || pSmbl->cntSCC.pNotSCC == 0));
  4.  
  5.     Symbol::ContainingSCC& cntSCC = pSmbl->cntSCC;
  6.     if (cntSCC.pSCC == 0 && cntSCC.pNotSCC == 0)
  7.     //The first time this symbol is checked
  8.     {
  9.         if (isNotSmbl)
  10.         {
  11.             pSmbl->value = true;
  12.             cntSCC.pNotSCC = pSCC;
  13.         }
  14.         else
  15.         {
  16.             pSmbl->value = false;
  17.             cntSCC.pSCC = pSCC;
  18.         }
  19.         return true;
  20.     }
  21.     else //The second time this symbol is checked
  22.     {
  23.         isNotSmbl ? (cntSCC.pNotSCC = pSCC) : (cntSCC.pSCC = pSCC);
  24.         return cntSCC.IsNotSameSCC();
  25.     }
  26. }
当该变量第一次被搜索时对其赋值并记录所属的强连通区域,第二次被搜索时检查两个互非Literal所属强连通区域是否相同。
完整的实现还包括从文件读取2-CNF公式,由TwoCNF类生成图,以及一些输出函数,在这里不罗嗦了,详细看附件里的程序。
三. 例子
这里找了几个相对比较简单的例子(其中&表示AND关系,|表示OR关系,!表示NOT):

例1.
引用:
(x0|x2)&(x0|!x3)&(x1|!x3)&(x1|!x4)&(x2|!x4)&(x5|x0)&(!x5|x1)&(x2|x5)&(x3|x6)&(x4|x6)&(x5|x6)
结果:
引用:
2-CNF: (x0|x2)&(x0|!x3)&(x1|!x3)&(x1|!x4)&(x2|!x4)&(x5|x0)&(!x5|x1)&(x2|x5)&(x3|
x6)&(x4|x6)&(x5|x6)

One Satisfiable Solutionx0:0) (x1:1) (x2:1) (x3:0) (x4:0) (x5:1) (x6:1)

Graph:
Node x2:
Node !x0: x2 !x3 x5
Node x0:
Node !x2: x0 !x4 x5
Node !x3: x6
Node x3: x0 x1
Node !x1: !x3 !x4 !x5
Node x1:
Node !x4: x6
Node x4: x1 x2
Node !x5: x0 x2 x6
Node x5: x1
Node x6:
Node !x6: x3 x4 x5

Strongly Connected Components:
The SCC of Rank 1 contains the following literals:
!x6
The SCC of Rank 2 contains the following literals:
x4
The SCC of Rank 3 contains the following literals:
!x1
The SCC of Rank 4 contains the following literals:
!x5
The SCC of Rank 5 contains the following literals:
x3
The SCC of Rank 6 contains the following literals:
!x2
The SCC of Rank 7 contains the following literals:
!x4
The SCC of Rank 8 contains the following literals:
x0
The SCC of Rank 9 contains the following literals:
!x0
The SCC of Rank 10 contains the following literals:
!x3
The SCC of Rank 11 contains the following literals:
x6
The SCC of Rank 12 contains the following literals:
x5
The SCC of Rank 13 contains the following literals:
x1
The SCC of Rank 14 contains the following literals:
x2
例2.
引用:
(x1|x2)&(x1|!x2)&(!x1|x2)&(!x1|!x2)&(!x1|x3)&(!x1|!x3)&(x2|!x3)&(x2|x4)&(!x2|x3)&(!x2|!x3)&(x3|x4)&(!x3|!x4)
结果:
引用:
No solution to 2-CNF: (x1|x2)&(x1|!x2)&(!x1|x2)&(!x1|!x2)&(!x1|x3)&(!x1|!x3)&(x2
|!x3)&(x2|x4)&(!x2|x3)&(!x2|!x3)&(x3|x4)&(!x3|!x4)
上传的附件
文件类型: rar 2CNF.rar (5.0 KB, 31 次查看)
评论 0 Email文章
评论总数 0

评论

发表评论 发表评论
作者为 bankrock 的最新文章

所有时间均为格林尼治时间 +9。现在的时间是 09:25 PM


Powered by vBulletin® 版本 3.7.0
版权所有 ©2000 - 2009,Jelsoft Enterprises Ltd.
(C) Copy Right All Right Reserved 2001 - 2007

Search Engine Friendly URLs by vBSEO 3.1.0