基于DPLL的SAT工具箱

一、SAT工具箱设计要求

1、SAT问题介绍

  1. 布尔表达式

命题逻辑公式,也称为布尔表达式,由变量,运算符AND(连接,也用∧表示),OR(分离,∨), NOT (否定,¬)和括号构成。1

  1. 布尔可满足问题

通过对一个布尔表达式进行一种真值指派,从而使该表达式的值为真,则称该布尔表达式可满足。一个可满足性问题就是判定一个给定的合取范式的布尔公式是否是可满足的。

  1. SAT问题

SAT的问题被证明是NP-Hard的问题。目前解决该问题的方法主要有完备的方法和不完备的方法两大类。完备的方法优点是保证能正确地判断SAT问题的可满足性,但其计算效率很低,平均的计算时间为多项式阶,最差的情况计算时间为指数阶,不适用于求解大规模的SAT问题。不完备的方法的优点是求解的时间比完备的方法快得多,但在很少数的情况下不能正确地判断SAT问题的可满足性。传统的方法有:枚举法、局部搜索法和贪婪算法等,但由于搜索空间大,问题一般难以求解。对于像SAT一类的NP-Hard问题,采用一些现代启发式方法如演化算法往往较为有效。2

二、工具箱设计要求

1、输入格式要求

输入文件格式要求为dimacs格式的cnf文件。

书写合取范式文件需要满足如下规则3

a ) 文件主要由 注释块 和 子句块 两部分组成,其中注释块一般用作变量的声明,每个变量占一行;子句块主要书写问题的条件 (即 cnf 范式),每个子句占一行。文件用标记行 p cnf {num_var} {num_clauses} 来区分 注释块 和 子句块 两部分,即该行之前是注释,该行之后是子句。这里的 {num_var} 表示变量的个数,{num_clauses} 表示子句的个数。

// 表示有 10 个变量,20 个子句。
p cnf 10 20

b ) 在注释块中,每行声明一个变量,且以小写字母 c 开头。注释的形式为 c {var_id} {var_name},其中 var_id 表示变量的序号,如 1,2,…,注意此序号必须从 1 开始且连续。var_name 表示变量的名称。这样做是为了后续子句书写的便利,即用序号表示变量。

// 声明变量 a,其序号为 1;声明变量 b,其序号为 2
c 1 a
c 2 b

c ) 在子句块中,每行表示一个析取子句 (仅包含或操作),且以数字 0 结尾。如 1 -2 0 表示 ( a ∨ ¬ b )这个析取子句。2 -1 0 表示 ( b ∨ ¬ a ) 这个析取子句。数字前面的符号表示非 (¬) 的含义。子句块中所包含的所有行用与操作连接起来就是我们的 cnf 了。因此在 SAT 问题中,如果要使得最终的合取范式 (CNF) 结果为真,则必须要求每一行的析取子句结果为真。

// 子句含义为 (a OR !b)
1 -2 0
// 子句含义为 (b OR !a)
2 -1 0

综上所述,我们可以根据 a,b 构建一个简单的 cnf,并存储在 .dimacs 文件中,文件内容为,

c 1 a
c 2 b
p cnf 2 2
1 -2 0
2 -1 0

2、根据输入的合取范式(cnf)判断该范式是否可满足,若可满足则返回一组可满足的真值指派。

二、DPLL算法介绍

DPLL是一种用来解决SAT问题的不完备算法。

假定所给的CNF公式为S, DPLL算法不断的按照如下规则对S做变换,直到不能继续进行为止。

1、重言式规则:删去S中所用的重言式,剩下的子句集记为S’。S可满足当且仅当S’可满足。

• S={p∨¬p, A}

2、单子句规则:如果S中有一个单子句L,那么L必须取真。我们可以从L中删去所有包含L的子句(包括单子句本身),得到集合S1。如果S1是空集,则S可满足。否则对S1中的每一个子句,如果它包含文字┐L,则从该子句中删除这个文字,由此得到子句集合S2。S可满足,当且仅当S2可满足

• {p,p∨¬q,¬p∨r}

3、纯文字规则:如果文字L的补¬L不出现在子句集中,那么L被称为纯文字。从S中删去包含L的所有子句,得到的集合S’。那么S可满足,当且仅当S’可满足。

• {p∨q∨¬r∨s, ¬p∨q∨¬r∨¬s, ¬p∨¬q}

4、分裂规则:假设S可以写出如下形式

( A 1 A_{1} A1∨L)∧… ∧( A m A_{m} Am∨L)∧( B 1 B_{1} B1∨¬L)∧… ∧( B n B_{n} Bn∨¬L)∧ C 1 C_{1} C1∧… ∧ C l C_{l} Cl

这里 A i A_{i} Ai , B j B_{j} Bj C k C_{k} Ck都是子句,而且都不含L或¬L,在这种情况下可以生成两种比S简单的子句集

  1. S1= A 1 A_{1} A1∧… ∧ A m A_{m} Am C 1 C_{1} C1∧… ∧ C l C_{l} Cl
  2. S2= B 1 B_{1} B1∧… ∧ B n B_{n} Bn C 1 C_{1} C1∧… ∧ C l C_{l} Cl

那么S可满足,当且仅当S1可满足,或者S2可满足。

三、实现方法

主要应用算法:DPLL算法;

编程语言:C++;

主要思想:

1、创建一个范式类来模拟范式的存储;

2、创建一个DPLL类来模拟范式的运算;

3、定义一个用于处理变量赋值的函数,具体功能为删除存在赋值为真的该变量的形式的子句,并删除所有子句中该变量的另一种形式;

解释:若我们已将p赋值为真,则对于以下两种形式的子式:

①A∨p

②B∨¬p

直接删除①式,删除②式中的¬p,变为B

4、定义一个用于初始化范式的函数,通过记录每个变量的自身与其否定式的数量的差值来进行对重言式规则、纯文字规则和分裂规则三个规则进行统一(详情可见步骤7、8);

5、定义一个用于找出单子句的函数,在进行计算前,运用单子句规则(即使用步骤3中定义的赋值函数)对范式进行化简;

6、找出出现频率最高的变量,并优先对其进行赋值处理;

7、通过步骤4中的记录,我们可以很方便地找到每个变量出现较多的形式,在步骤6中,我们可以优先将出现较多的形式赋值为真,即利用纯文字规则(变量的另一种形式不存在)、分裂规则(优先求解含有出现次数较少的变量的形式所在的子句集);

8、步骤3中的赋值函数可以通过分别将一个变量的两种形式的真假值赋为真来实现化简,这步将重言式规则、纯文字规则与分裂规则相统一:

(1)重言式规则:若子句中存在该变量的重言式,则该赋值函数的作用相当于直接删除该子句;

(2)纯文字规则:若该子句集中不存在该变量出现次数较多的形式的补,则该赋值函数的作用相当于直接删除所有存在该变量的子句;

(3)分裂规则:若该子句集中该变量的两种形式均存在,则该赋值函数的作用相当于删除所有包含当前被赋值为真的变量的形式的子句;

9、递归地进行上述步骤5到步骤8的运算,直到找到一个可满足的真值指派。

四、实现代码(非原创,转载自github4

/*
 * Program to implement a SAT solver using the DPLL algorithm with unit
 * propagation Sukrut Rao CS15BTECH11036
 */

#include 
#include 
#include 
#include 
#include 
#include 

using namespace std;

/*
 * enum for different types of return flags defined
 */
enum Cat {
     
  satisfied,   // when a satisfying assignment has been found
  unsatisfied, // when no satisfying assignment has been found after
               // exhaustively searching
  normal,   // when no satisfying assignment has been found till now, and DPLL()
            // has exited normally
  completed // when the DPLL algorithm has completed execution
};

/*
 * class to represent a boolean formula
 */
class Formula {
     
public:
  // a vector that stores the value assigned to each variable, where
  // -1 - unassigned
  // 0 - true
  // 1 - false
  vector<int> literals;
  vector<int> literal_frequency; // vector to store the number of occurrences of
                                 // each literal

  // vector to store the difference in number of occurrences with
  // positive and negative polarity of each literal
  vector<int> literal_polarity;

  // vector to store the clauses
  // for each clauses, if the variable n is of positive polarity, then 2n is
  // stored if the variable n is of negative polarity, then 2n+1 is stored here,
  // n is assumed to be zero indexed
  vector<vector<int>> clauses;
  Formula() {
     }

  // copy constructor for copying a formula - each member is copied over
  Formula(const Formula &f) {
     
    literals = f.literals;
    clauses = f.clauses;
    literal_frequency = f.literal_frequency;
    literal_polarity = f.literal_polarity;
  }
};

/*
 * class to represent the structure and functions of the SAT Solver
 */
class SATSolverDPLL {
     
private:
  Formula formula;               // the initial formula given as input
  int literal_count;             // the number of variables in the formula
  int clause_count;              // the number of clauses in the formula
  int unit_propagate(Formula &); // performs unit propagation
  int DPLL(Formula);             // performs DPLL recursively
  int apply_transform(Formula &,
                      int); // applies the value of the literal in every clause
  void show_result(Formula &, int); // displays the result
public:
  SATSolverDPLL() {
     }
  void initialize(); // intiializes the values
  void solve();      // calls the solver
};

/*
 * function that accepts the inputs from the user and initializes the attributes
 * in the solver
 */
void SATSolverDPLL::initialize() {
     
  char c;   // store first character
  string s; // dummy string

  while (true) {
     
    cin >> c;
    if (c == 'c') // if comment
    {
     
      getline(cin, s); // ignore
    } else             // else, if would be a p
    {
     
      cin >> s; // this would be cnf
      break;
    }
  }
  cin >> literal_count;
  cin >> clause_count;

  // set the vectors to their appropriate sizes and initial values
  formula.literals.clear();
  formula.literals.resize(literal_count, -1);
  formula.clauses.clear();
  formula.clauses.resize(clause_count);
  formula.literal_frequency.clear();
  formula.literal_frequency.resize(literal_count, 0);
  formula.literal_polarity.clear();
  formula.literal_polarity.resize(literal_count, 0);

  int literal; // store the incoming literal value
  // iterate over the clauses
  for (int i = 0; i < clause_count; i++) {
     
    while (true) // while the ith clause gets more literals
    {
     
      cin >> literal;
      if (literal > 0) // if the variable has positive polarity
      {
     
        formula.clauses[i].push_back(2 *
                                     (literal - 1)); // store it in the form 2n
        // increment frequency and polarity of the literal
        formula.literal_frequency[literal - 1]++;
        formula.literal_polarity[literal - 1]++;
      } else if (literal < 0) // if the variable has negative polarity
      {
     
        formula.clauses[i].push_back(2 * ((-1) * literal - 1) +
                                     1); // store it in the form 2n+1
        // increment frequency and decrement polarity of the literal
        formula.literal_frequency[-1 - literal]++;
        formula.literal_polarity[-1 - literal]--;
      } else {
     
        break; // read 0, so move to next clause
      }
    }
  }
}

/*
 * function to perform unit resolution in a given formula
 * arguments: f - the formula to perform unit resolution on
 * return value: int - the status of the solver after unit resolution, a member
 * of the Cat enum Cat::satisfied - the formula has been satisfied
 *               Cat::unsatisfied - the formula can no longer be satisfied
 *               Cat::normal - normal exit
 */
int SATSolverDPLL::unit_propagate(Formula &f) {
     
  bool unit_clause_found =
      false; // stores whether the current iteration found a unit clause
  if (f.clauses.size() == 0) // if the formula contains no clauses
  {
     
    return Cat::satisfied; // it is vacuously satisfied
  }
  do {
     
    unit_clause_found = false;
    // iterate over the clauses in f
    for (int i = 0; i < f.clauses.size(); i++) {
     
      if (f.clauses[i].size() ==
          1) // if the size of a clause is 1, it is a unit clause
      {
     
        unit_clause_found = true;
        f.literals[f.clauses[i][0] / 2] =
            f.clauses[i][0] % 2; // 0 - if true, 1 - if false, set the literal
        f.literal_frequency[f.clauses[i][0] / 2] =
            -1; // once assigned, reset the frequency to mark it closed
        int result = apply_transform(f, f.clauses[i][0] /
                                            2); // apply this change through f
        // if this caused the formula to be either satisfied or unsatisfied,
        // return the result flag
        if (result == Cat::satisfied || result == Cat::unsatisfied) {
     
          return result;
        }
        break; // exit the loop to check for another unit clause from the start
      } else if (f.clauses[i].size() == 0) // if a given clause is empty
      {
     
        return Cat::unsatisfied; // the formula is unsatisfiable in this branch
      }
    }
  } while (unit_clause_found);

  return Cat::normal; // if reached here, the unit resolution ended normally
}

/*
 * applies a value of a literal to all clauses in a given formula
 * arguments: f - the formula to apply on
 *            literal_to_apply - the literal which has just been set
 * return value: int - the return status flag, a member of the Cat enum
 *               Cat::satisfied - the formula has been satisfied
 *               Cat::unsatisfied - the formula can no longer be satisfied
 *               Cat::normal - normal exit
 */
int SATSolverDPLL::apply_transform(Formula &f, int literal_to_apply) {
     
  int value_to_apply = f.literals[literal_to_apply]; // the value to apply, 0 -
                                                     // if true, 1 - if false
  // iterate over the clauses in f
  for (int i = 0; i < f.clauses.size(); i++) {
     
    // iterate over the variables in the clause
    for (int j = 0; j < f.clauses[i].size(); j++) {
     
      // if this is true, then the literal appears with the same polarity as it
      // is being applied that is, if assigned true, it appears positive if
      // assigned false, it appears negative, in this clause hence, the clause
      // has now become true
      if ((2 * literal_to_apply + value_to_apply) == f.clauses[i][j]) {
     
        f.clauses.erase(f.clauses.begin() +
                        i); // remove the clause from the list
        i--;                // reset iterator
        if (f.clauses.size() ==
            0) // if all clauses have been removed, the formula is satisfied
        {
     
          return Cat::satisfied;
        }
        break; // move to the next clause
      } else if (f.clauses[i][j] / 2 ==
                 literal_to_apply) // the literal appears with opposite polarity
      {
     
        f.clauses[i].erase(
            f.clauses[i].begin() +
            j); // remove the literal from the clause, as it is false in it
        j--;    // reset the iterator
        if (f.clauses[i].size() ==
            0) // if the clause is empty, the formula is unsatisfiable currently
        {
     
          return Cat::unsatisfied;
        }
        break; // move to the next clause
      }
    }
  }
  // if reached here, the function is exiting normally
  return Cat::normal;
}

/*
 * function to perform the recursive DPLL on a given formula
 * argument: f - the formula to perform DPLL on
 * return value: int - the return status flag, a member of the Cat enum
 *               Cat::normal - exited normally
 *               Cat::completed - result has been found, exit recursion all the
 * way
 */
int SATSolverDPLL::DPLL(Formula f) {
     
  int result = unit_propagate(f); // perform unit propagation on the formula
  if (result == Cat::satisfied) // if formula satisfied, show result and return
  {
     
    show_result(f, result);
    return Cat::completed;
  } else if (result == Cat::unsatisfied) // if formula not satisfied in this
                                         // branch, return normally
  {
     
    return Cat::normal;
  }
  // find the variable with maximum frequency in f, which will be the next to be
  // assigned a value already assigned variables have this field reset to -1 in
  // order to ignore them
  int i = distance(
      f.literal_frequency.begin(),
      max_element(f.literal_frequency.begin(), f.literal_frequency.end()));
  // need to apply twice, once true, the other false
  for (int j = 0; j < 2; j++) {
     
    Formula new_f = f; // copy the formula before recursing
    if (new_f.literal_polarity[i] >
        0) // if the number of literals with positive polarity are greater
    {
     
      new_f.literals[i] = j; // assign positive first
    } else                   // if not
    {
     
      new_f.literals[i] = (j + 1) % 2; // assign negative first
    }
    new_f.literal_frequency[i] =
        -1; // reset the frequency to -1 to ignore in the future
    int transform_result =
        apply_transform(new_f, i); // apply the change to all the clauses
    if (transform_result ==
        Cat::satisfied) // if formula satisfied, show result and return
    {
     
      show_result(new_f, transform_result);
      return Cat::completed;
    } else if (transform_result == Cat::unsatisfied) // if formula not satisfied
                                                     // in this branch, return
                                                     // normally
    {
     
      continue;
    }
    int dpll_result = DPLL(new_f); // recursively call DPLL on the new formula
    if (dpll_result == Cat::completed) // propagate the result, if completed
    {
     
      return dpll_result;
    }
  }
  // if the control reaches here, the function has returned normally
  return Cat::normal;
}

/*
 * function to display the result of the solver
 * arguments: f - the formula when it was satisfied or shown to be unsatisfiable
 *            result - the result flag, a member of the Cat enum
 */
void SATSolverDPLL::show_result(Formula &f, int result) {
     
  if (result == Cat::satisfied) // if the formula is satisfiable
  {
     
    cout << "SAT" << endl;
    for (int i = 0; i < f.literals.size(); i++) {
     
      if (i != 0) {
     
        cout << " ";
      }
      if (f.literals[i] != -1) {
     
        cout << pow(-1, f.literals[i]) * (i + 1);
      } else // for literals which can take either value, arbitrarily assign
             // them to be true
      {
     
        cout << (i + 1);
      }
    }
    cout << " 0";
  } else // if the formula is unsatisfiable
  {
     
    cout << "UNSAT";
  }
}

/*
 * function to call the solver
 */
void SATSolverDPLL::solve() {
     
  int result = DPLL(formula); // final result of DPLL on the original formula
  // if normal return till the end, then the formula could not be satisfied in
  // any branch, so it is unsatisfiable
  if (result == Cat::normal) {
     
    show_result(formula, Cat::unsatisfied); // the argument formula is a dummy
                                            // here, the result is UNSAT
  }
}

int main() {
     
  SATSolverDPLL solver; // create the solver
  solver.initialize();  // initialize
  solver.solve();       // solve
  return 0;
}

参考


  1. 布尔可满足性问题 ↩︎

  2. SAT问题 ↩︎

  3. CNF合取范式 ↩︎

  4. 源代码下载地址 ↩︎

你可能感兴趣的:(数理逻辑,算法,c++,逻辑推理)