CDCL (conflict-driven clause learning) アルゴリズムのSATソルバーを作る
CDCLアルゴリズム開発リポジトリ
SAT競技会のデータをRustで簡単に取得できるようにするやつ
通常SATソルバと行った時は命題論理の充足可能性問題を解くアルゴリズムを指しますが、今回は論理学には深入りせずにブール論理の言葉で議論していきます。
ブール論理
ブール論理というのは集合
- 結合則:
a \lor (b \lor c) = (a \lor b) \lor c, a \land (b \land c) = (a \land b) \land c - 交換則:
a \lor b = b \lor a, a \land b = b \land a - 吸収則:
a \lor (a \land b) = a, a \land (a \lor b) = a - 分配則:
a \lor (b \land c) = (a \lor b) \land (a \lor c), a \land (b \lor c) = (a \land b) \lor (a \land c) - 可補束:
a \lor \lnot a = 1, a \land \lnot a = 0 - 等冪性:
a \lor a = a, a \land a = a - 有界性:
a \lor 0 = a, a \land 1 = a, a \lor 1 = 1, a \land 0 = 0 - 相補性:
\lnot 0 = 1, \lnot 1 = 0 - ド・モルガンの法則:
\lnot (a \lor b) = \lnot a \land \lnot b, \lnot (a \land b) = \lnot a \lor \lnot b - 対合:
\lnot \lnot a = a
この代数構造を含めてあらためて
論理式と標準形
可換環に対して多項式を考えたように、ブール論理についても変数を導入し、その変数に対して論理積、論理和、否定を適用することで式を構成できます。これを論理式と呼びます。例えば変数を
1つの多項式に対して例えば
SATソルバ
可換環
論理式
変数の代入に対して充足解がどうなるのかを考えてみよう。
もし
これは
この原始的なアルゴリズムを単純な再帰として実装したものが brute_force
である。決定変数の個数が精々30程度のごくごく小さい問題に対してはこれでも解けることが分かる。
なお各ステップにおいてどの変数を固定するのか?という問題がある。これはおそらく変数選択ヒューリスティックという分野が始まるはずだが、ここでは単に論理式に含まれる決定変数のうちIDが最も小さいものを選んでいる。
次に単位伝搬を実装する。その前にもう少し用語を整理する。
リテラル
ある変数
節 (clause)
SAT問題では通常CNFになった論理式を扱う。CNFは次のようなAND (OR (NOT X))) の形になっている
なおANDとORがそれぞれ無い場合はそれぞれ
CNF系の論理式
例えば
単位伝搬
さてCNF形式の論理式
これは探索する範囲が大幅に狭められることを意味する。
このように単位節によって割り当てられた変数を「含意(implication)」と呼び、一方探索の途中で仮定した変数の事を「決定(decision)」と呼んだりする(文献によって呼び方はわりとバラバラ)。
この含意のプロセスは伝搬する。例えば
このアルゴリズムは以下のPRで dpll
として実装した
ある二つの節
と書けるとする。この時に
を考えると、これは
同様に
命題論理では
となることからもこの操作を導出と呼ぶ理由がうかがえる。
導出の重要な性質は次の命題である
と書けるとしてよい。今
- (
の場合)x_k = 1 なので\lnot x_k = 0 であるためにはf_j(x) = 1 でなければならない。よってg_j(x) = 1 (f_i \diamond f_j)(x) = (g_i \lor g_j)(x) = g_i(x) \lor g_j(x) = 1 - (
の場合) 同様にx_k = 0 であるためにはf_i(x) = 1 でなければならず、g_i(x) = 1 (f_i \diamond f_j)(x) = (g_i \lor g_j)(x) = g_i(x) \lor g_j(x) = 1
以上より
節の集合が与えられ時にそれから導出される節を追加しても充足性が変化しないので、もし単位節
CDCLではこのように変数の方を順番に固定していくのではなく節の方を操作していく。
これは次の形の方が使いやすい
証明は上の
充足性を保ったまま節を削除することもできる。その前に少し記法を整理する。
節
もし
つまりある組
この操作によって充足性を維持したまま変数を一つ取り除くことができる。しかし節の数は
以上で素朴なCDCLアルゴリズムを理解する準備が概ね整ったはず。
- Clause Learningというのは導出節を追加する操作の事
- 変数を削除するように導出節を追加するのでは節の数が爆発してしまうので不味い。つまり有益な節だけを学習する必要がある。
- なのでDPLLの時のように変数に仮の値を割り当てていく探索を行いながら矛盾する節を探し、その矛盾した節から導出節を作る事にする。これで必要な節だけが学習できるという目算。
CDCLでは導出により増えていく節と変数の割り当てと同時に連携しながら管理していく必要がある。
Trail
矛盾した節を見つけたときにそれから導出節を作る必要があるので、どの節によって変数を固定することになったかの理由が必要となる。Knuth 4Bではこのデータ構造をTrailと呼んでいる。ここではTrailに何を保存する必要があるのかを議論して最後にTrailのデータ構造を示す。
変数の割り当て
ある変数に値を割り当てる操作は、真であるリテラルのリストにその変数のリテラルを追加することと対応するので、Trailではリテラルのリストを管理する。つまりリテラル
単位伝搬
Trailにリテラルを追加する操作は二通りある。一つはDPLLの時の単位伝搬に相当する操作で、既にTrailにリテラル
この操作も同じように単位伝搬と呼ぶ。どの節から評価するか自由度があるが、そこは実装依存という事にしておく。
ここで重要となるのは、理由
決定
もう一つは決定である。現在のTrailの状態において全ての節を確認して単位伝搬が行えない場合、既にTrailに含まれているリテラルと変数が被らない新しいリテラルをTrailに追加する。この時理由となる節は存在しない。どのようなリテラルを選ぶかは実装依存である。
Trailのリテラルには順序があるので、あるTrail中のリテラルを指定するとそれ以前にいくつ決定により追加されたリテラルが存在するかが一意に定まり、それを決定レベルと呼ぶ。例えば次のようになる:
step | リテラル | 理由 | 決定レベル |
---|---|---|---|
0 | 決定 | 1 | |
1 | 決定 | 2 | |
2 | 2 | ||
3 | 決定 | 3 | |
4 | 3 |
衝突
Trailにリテラルを追加したとき、ある節
と書ける場合がある。この時Trailによって指定される変数の割り当てに対して、他の変数をどう指定してもこの節が
もし決定レベル
DPLLでは衝突が発生したとき最後の決定が間違っていたとしてバックトラックを行うが、CDCLでは衝突の理由を分析して導出節を追加し、さらにTrailの適切な地点まで一気に巻き戻す。
衝突節
上の設定における衝突節
まずTrailに変数を追加した後に必ず全ての節について衝突していない事を判定しており、衝突していた場合は処理を継続していない事を前提とすると(あとでそのようにアルゴリズムを定義するが、ここでは仮定とする)、
続いて決定を行う前に全ての実行可能な単位伝搬を行っていると仮定すると、最後の決定の前の段階で節
すると
ただし
つまり
バックジャンプ
ではどこまで戻ればいいのだろうか?上での議論から既に
上での仮定「決定を行う前に全ての実行可能な単位伝搬を行っている」を維持することを考えると、もしこの導出節を追加する場合、これが単位伝搬に使う事が可能だったところまで戻らないといけない。つまり
現在の決定レベルには有限個しかリテラルが無いので、このプロセスは有限回で終了し、現在の決定レベルにおけるリテラルを一つだけ含む導出節