ひとり勉強会

ひとり楽しく勉強会

SATソルバを作る会 (1)

適当にまったりと。今回は下準備みたいなものだけです。

入力

せっかくなのでデータ形式SAT Competitions の形式と揃えておこうかと思いました。出せるようなものはとても作れないですけど、比較する時にその方が便利そう。

まず、入力はConjunctive Normal Formの論理式で与えるとのこと。

(x1 or !x5 or x4) and (!x1 or x5 or x3 or x4) and (!x3 or !x4)

こういう、『「変数か変数の否定 (= リテラル)」をorで繋いだもの (= 選言節)』をandで繋いだもの、という形の式です。ファイル形式としては、DIMACS形式というので与えられるらしい。こんなテキストファイルです。

c
c コメント〜
c
p cnf 5 3
1 -5 4 0
-1 5 3 4 0
-3 -4 0
  • 最初に何行かコメント (c XXXXX)
  • 次にヘッダ的なもの (p cnf 変数の個数 選言節の数)
  • 以下、一行に一個の選言節。スペース区切りの整数列で、
    • 正の数はnotがついてない変数
    • 負の数はnotがついてる変数
    • ゼロは選言節の終わり

出力

出力は、論理式を true にすることができる場合は

s SATISFIABLE
v 1 -2 3 -4 -5 0

のようにして、たとえば x1,x3 をtrueにして x2,x4,x5 をfalseにすればOK、という例を標準出力に出します。複数のtrueにしかたがある場合、どれか1つ出せばよいです。同時に、実行ファイルの終了コードとして 10 を返します。

出力は、論理式を true にすることができない場合は、

s UNSATISFIABLE

だけでいい。実行ファイルの終了コードとして 20 を返す。

コンテストのように時間制限がある場合、時間切れ等でどちらともわからなかった場合は、

s UNKNOWN

で、終了コードとして 0 を返す。とりあえずこれは自分には関係ないかな。

Take 1 : 完全に全探索

まずはウォームアップとして、全ての変数割り当てパターンを総当たりで試してみるソルバを書いてみました。今のところD言語を使っています。

import std.stdio;
import std.stream, std.cstream;
import std.string;
import std.conv;

class SAT
{
private:
  alias int Literal; /// 非ゼロ整数。+n は xn、-n は !xn を表す
  int  varOf( Literal lit )      { return (lit>0 ? lit-1 : -lit-1); }
  bool isPositive( Literal lit ) { return (lit>0); }

  alias Literal[] Clause; /// [-1,+2,+3] が !x1|x2|x3 を表す
  alias Clause[]     CNF; /// [[-1,+2],[+3,-2]] で (!x1|x2)&(x3|!x2) を表す

private:
  int numVar; /// 変数の個数
  CNF cnf;    /// 論理式

public:
  /// DIMACS format の入力ストリームから適当に読み込み
  this( InputStream sin )
  {
    foreach(char[] s; sin)
      if( s.length && s[0]!='c' )
        if( s[0] == 'p' )
          numVar = to!(int)( (s.idup.split)[2] );
        else
          cnf ~= to!(int[])( (s.idup.split)[0..$-1] );
  }

  /// ビットベクトルで変数への値割り当てを与えて評価
  bool eval(BitVec)( BitVec bv )
  {
    bool be = true;
    foreach(clause; cnf)
    {
      bool bc = false;
      foreach(lit; clause)
      {
        static if( is(bv[0]) )
          bool bl = bv[varOf(lit)];
        else
          bool bl = !!((bv>>varOf(lit))&1);
        bc |= isPositive(lit) ? bl : !bl;
      }
      be &= bc;
    }
    return be;
  }
}

bool solve( SAT s )
{
  long MASK = (1L << s.numVar) - 1;

  B_LOOP:
  for(long B=0; B<=MASK; ++B)
    if( s.eval(B) )
    {
      // Satisfied
      writeln("s SATISFIABLE");
      write("v");
      for(int i=0; i<s.numVar; ++i)
        write(" ", (B>>i)&1 ? i+1 : -i-1);
      writeln(" ", 0);
      return true;
    }

  // Not satisfied
  writeln("s UNSATISFIABLE");
  return false;
}

int main()
{
  auto s = new SAT(din);
  return solve(s) ? 10 : 20;
}

さて、これが最低ラインのパフォーマンスと言うことになります。どのくらいのスピードかなー。

20変数85節の3-CNF(各選言説のリテラル数が全部3のもの)ランダムデータを作って試してみました。色々試してみたところ、節数をこれより減らすとSATISFIABLEだらけ、これより増やすとUNSATISFIABLEだらけになってしまうようなので。比較対象はPicoSATです。

<picosat>
s SATISFIABLE
v -1 2 3 -4 -5 6 7 8 9 -10 11 12 13 -14 15 -16 17 -18 19 20 0
[10] : 99 ms

<hzkrsat>
s SATISFIABLE
v 1 2 -3 -4 5 -6 7 8 -9 10 -11 12 -13 14 -15 16 -17 -18 19 -20 0
[10] : 5977 ms

といったところ。もちろん、もっと大きい入力を喰わせるとどんどん差は開いていきました。25変数105節だと…

<picosat>
s SATISFIABLE
v 1 -2 3 -4 5 6 7 -8 -9 -10 11 -12 -13 -14 -15 -16 -17 18 19 20 -21 22 -23 -24
v 25 0
[10] : 105 ms

<hzkrsat>
s SATISFIABLE
v -1 2 -3 4 5 6 -7 -8 -9 10 -11 -12 -13 -14 -15 -16 17 18 19 -20 21 -22 -23 24 -
25 0
[10] : 222315 ms

というわけで、次回から色々試して改善していけたらいいなーと思います。

雑談

そういえば、変数と節数の比が4.4898を超えるとほぼ確実にUNSATISFIABLEという論文を前に読んで「へー!」と思っていたのを思い出しました。確かに自分で手で実験してみるとその辺りに境がありますね。面白かったです。

ググってみると、イマドキのSATソルバは基本的に DPLL という枠組みで解いているらしい。次はこの辺りをちょっと調べてみようかと思います。予定は未定です。