"""DPLLソルバーのモジュール.
以下のクラスからなる.
* DPLLクラス: 命題変数のクラス
Note:
本プログラムは学習用の目的で作成されている.
実用上の問題への適用は想定していない.
"""
[ドキュメント]
class DPLL():
"""DPLLソルバーのクラス.
Args:
verbose (int, optional): 正ならDPLLソルバーの情報を表示する (デフォルト値は0).
interactive (bool, optional): 真なら会話的に実行する (デフォルト値はFalse).
"""
def __init__(self, verbose=0, interactive=False):
self.verbose = verbose
self.interactive = interactive
self.vname2dimacslit = {}
self.vName = []
self.watches = []
self.vAssigns = []
self.vLevel = []
self.vReason = []
self.clauses = []
self.trail_lim = []
self.trail = []
self.propQueue = []
self.isSat = None
self.stat = {}
def __enter__(self):
return self
def __exit__(self, ex_type, ex_value, trace):
return
[ドキュメント]
def nVars(self):
"""命題変数の個数を返す.
Returns:
命題変数の個数.
"""
return len(self.vAssigns)
[ドキュメント]
def nClauses(self):
"""節の個数を返す.
Returns:
節の個数.
"""
return len(self.clauses)
[ドキュメント]
def toLit(self, v, negative=0):
"""変数番号vに対するリテラル番号を返す.
Args:
v (int): 変数番号.
negative (int, optional): 1なら否定 (デフォールト値は0).
Returns:
リテラル番号.
"""
return (v << 1) + negative
[ドキュメント]
def toVar(self, lit):
"""リテラルlitに対する変数と符号のタプルを返す.
Args:
lit (int): リテラル.
Returns:
変数と符号のタプル.
"""
return (lit >> 1, lit & 1)
[ドキュメント]
def value(self, lit):
"""リテラルlitの現在の真理値を返す.真理値が割当てられていなければNoneを返す.
Args:
lit (int): リテラル.
Returns:
litの真理値 (割当てられていなければNone).
"""
(v, n) = self.toVar(lit)
t = self.vAssigns[v]
return None if t is None else t ^ n
[ドキュメント]
def newVar(self, vname=None):
"""新しい変数を追加する.
Args:
vname (str, optional): 変数名 (デフォールト値はNone).
Returns:
新しい変数番号.
"""
v = self.nVars()
self.vName.append(vname)
self.watches.append([]) # for positive literal
self.watches.append([]) # for negative literal
self.vReason.append(None)
self.vAssigns.append(None)
self.vLevel.append(None)
return v
[ドキュメント]
def decisionLevel(self):
"""現在の決定レベルを返す.
Returns:
決定レベル.
"""
return len(self.trail_lim)
[ドキュメント]
def enqueue(self, lit, reason=None):
"""リテラルlitを伝播用キューに追加する.
リテラルlitの値がすでに1であればTrueを返し,伝播用キューには追加しない.
リテラルlitの値がすでに0であればFalseを返し,伝播用キューには追加しない.
リテラルlitの値が決まっていなければ伝播用キューには追加し,Trueを返す.
Args:
lit (int): リテラル.
reason (list, optional): 理由の節 (デフォールト値はNone).
Returns:
衝突が生じなければTrue,衝突があればFalse.
"""
if reason is not None:
self.info(1, f"propagate '{self.lit2repr(lit)}' by {self.clause2repr(reason)}")
if self.value(lit) is not None:
return self.value(lit) == 1
(v, n) = self.toVar(lit)
assert self.vAssigns[v] is None
self.vAssigns[v] = n ^ 1
self.vLevel[v] = self.decisionLevel()
self.vReason[v] = reason
self.trail.append(lit)
self.propQueue.append(lit)
return True
[ドキュメント]
def assume(self, lit):
"""リテラルlitを仮に真と決定する.
Args:
lit (int): リテラル.
Returns:
衝突が生じなければTrue,衝突があればFalse.
"""
self.trail_lim.append(len(self.trail))
self.stat["decisions"] += 1
self.info(1, f"decide '{self.lit2repr(lit)}'")
return self.enqueue(lit)
[ドキュメント]
def propagateClause(self, num, lit):
"""num番目の節に対しリテラルlitの伝播処理を行う.
Args:
num (int): 節の番号.
lit (int): リテラル.
Returns:
衝突が生じなければTrue,衝突があればFalse.
"""
clause = self.clauses[num]
if clause[0] == lit ^ 1:
clause[0] = clause[1]
clause[1] = lit ^ 1
if self.value(clause[0]) == 1:
self.watches[lit].append(num)
return True
for i in range(2,len(clause)):
if self.value(clause[i]) != 0:
clause[1] = clause[i]
clause[i] = lit ^ 1
self.watches[clause[1] ^ 1].append(num)
return True
self.watches[lit].append(num)
return self.enqueue(clause[0], clause)
[ドキュメント]
def propagate(self):
"""伝播処理を行う.
Returns:
衝突が生じなければNone,衝突が生じれば衝突のあった節の番号.
"""
while self.propQueue:
self.stat["propagations"] += 1
lit = self.propQueue.pop()
tmp = self.watches[lit].copy()
self.watches[lit] = []
for i, num in enumerate(tmp):
if not self.propagateClause(num, lit):
# confliction at clause number num
for j in range(i+1,len(tmp)):
self.watches[lit].append(tmp[j])
self.propQueue.clear()
return self.clauses[num]
return None
[ドキュメント]
def cancel(self):
"""1レベルのバックトラック処理を行う.
"""
c = len(self.trail) - self.trail_lim[-1]
while c > 0:
lit = self.trail.pop()
(v, _) = self.toVar(lit)
self.vAssigns[v] = None
self.vReason[v] = None
self.vLevel[v] = None
c -= 1
self.trail_lim.pop()
[ドキュメント]
def cancelUntil(self, level):
"""決定レベルlevelまでバックトラック処理を行う.
Args:
level (int): 決定レベル.
"""
self.info(1, f"backjump to level {level}")
while self.decisionLevel() > level:
self.cancel()
[ドキュメント]
def newDecision(self):
"""値が未割当ての変数を選択し,それを偽と仮に決定する.
Returns:
負リテラル.未割当ての変数がなければNone.
"""
for v in range(self.nVars()):
if self.vAssigns[v] is None:
lit = self.toLit(v, negative=1)
self.assume(lit)
return lit
return None
[ドキュメント]
def backtrack(self):
"""バックトラック処理を行う.
最後の決定まで戻り,それが正リテラルの決定の間はさらに前の決定に戻る.
負リテラルの決定なら,正リテラルにして決定する.
"""
while True:
level = self.decisionLevel()
if level == 0:
self.isSat = False
return
lastDecision = self.trail[self.trail_lim[level-1]]
self.cancelUntil(level-1)
(v, t) = self.toVar(lastDecision)
if t == 1:
lit = self.toLit(v, t^1)
self.assume(lit)
return
[ドキュメント]
def getModel(self):
"""モデルを返す.
Returns:
モデル.
"""
model = {}
for v in range(self.nVars()):
model[self.var2repr(v)] = self.vAssigns[v]
return model
[ドキュメント]
def search(self, num=1):
"""モデルの探索を行い,見つかったモデルをyieldするジェネレータ関数.
Args:
num (int,optional): 探索するモデルの個数 (0なら全解を探索).
Yields:
モデル.
"""
if self.verbose >= 2:
self.info(2, "clauses")
for i, clause in enumerate(self.clauses):
print(f" Clause {i}: {self.clause2repr(clause)}", flush=True)
self.isSat = None
count = 0
while self.isSat is not False:
clause = self.propagate()
self.info(2, f"decisions {self.decisions2repr()}")
self.info(2, f"assigns {self.assigns2repr()}")
self.info(2, f"watches {self.watches2repr()}")
if clause is not None:
self.info(1, f"conflict at {self.clause2repr(clause)}")
self.stat["conflicts"] += 1
self.backtrack()
else:
lit = self.newDecision()
if lit is None:
# Model found
self.isSat = True
count += 1
model = self.getModel()
self.info(1, f"found model {count}")
yield model
if not num and count >= num:
break
self.backtrack()
[ドキュメント]
def getStat(self):
"""実行の統計情報を返す.
Returns:
統計情報.
"""
return self.stat
[ドキュメント]
def solutions(self, clauses=None, num=1):
"""与えられた節を追加し,モデルをyieldするジェネレータ関数.
Args:
clauses (list,optional): 追加する節のリスト.
num (int,optional): 探索するモデルの個数 (0なら全解を探索).
Yields:
モデル.
"""
if clauses:
self.add(*clauses)
t = time.time()
self.trail_lim = []
self.trail = []
self.isSat = None
self.stat = {}
self.stat["variables"] = self.nVars()
self.stat["clauses"] = self.nClauses()
self.stat["decisions"] = 0
self.stat["conflicts"] = 0
self.stat["propagations"] = 0
for model in self.search(num=num):
self.stat["solving"] = time.time() - t
yield model
if self.decisionLevel() > 0:
self.cancelUntil(0)
self.stat["solving"] = time.time() - t
[ドキュメント]
def solve(self, clauses=None, num=1, stat=False):
"""与えられた節を追加し,モデルを表示する.
Args:
clauses (list,optional): 追加する節のリスト.
num (int,optional): 探索するモデルの個数 (0なら全解を探索).
stat (bool,optional): 真なら統計情報も表示する.
"""
if clauses:
self.add(*clauses)
count = 0
for model in self.solutions(num=num):
if count == 0:
print("SATISFIABLE", flush=True)
count += 1
print(f"Model {count}: {model}", flush=True)
if stat:
print(f"Stat: {self.stat}", flush=True)
if count == 0:
print("UNSATISFIABLE", flush=True)
if stat and (count == 0 or num == 0):
print(f"Stat: {self.stat}", flush=True)
[ドキュメント]
def info(self, l, msg):
"""メッセージを表示する.
表示レベルが self.verbose より大きければ表示しない.
self.interactive が真ならユーザ入力を待つ.
Args:
l (int): 表示レベル.
msg (str): メッセージ.
"""
if l <= self.verbose:
print(f"# {self.decisionLevel()}: {msg}", flush=True)
if self.interactive:
print("Press Ctrl-Enter to proceed", flush=True)
input()
[ドキュメント]
def var2repr(self, v, negative=0):
"""変数の表記文字列を返す関数.
newVarで変数名が与えられていればそれを使用する.
Args:
v (int): 変数番号.
negative (int,optional): 1なら負リテラル.
Returns:
変数の表記文字列.
"""
s = self.vName[v]
if s is None:
s = str(v+1)
return s if negative == 0 else "~" + s
[ドキュメント]
def lit2repr(self, lit):
"""リテラルの表記文字列を返す関数.
Args:
lit (int): リテラル.
Returns:
リテラルの表記文字列.
"""
(v, n) = self.toVar(lit)
return self.var2repr(v, n)
[ドキュメント]
def clause2repr(self, clause):
"""節の表記 (リテラルの表記文字列のリスト)を返す関数.
Args:
clause (list): 節.
Returns:
節の表記.
"""
return None if clause is None else [ self.lit2repr(lit) for lit in clause ]
[ドキュメント]
def assigns2repr(self):
"""現在の値割当ての表記を返す関数.
Returns:
値割当ての表記.
"""
a = {}
for v in range(self.nVars()):
if self.vAssigns[v] is not None:
a[self.var2repr(v)] = self.vAssigns[v]
return a
[ドキュメント]
def decisions2repr(self):
"""現在の決定リストの表記を返す関数.
Returns:
決定リストの表記.
"""
d = []
for t in self.trail_lim:
lit = self.trail[t]
d.append(self.lit2repr(lit))
return d
[ドキュメント]
def watches2repr(self):
"""現在の監視リストの表記を返す関数.
Returns:
監視リストの表記.
"""
w = []
for (lit, num) in enumerate(self.watches):
s = self.lit2repr(lit)
w.append((s, num))
return w
[ドキュメント]
def addInternalClause(self, clause):
"""内部表現の節 (リテラル番号のリスト)を追加する.
Args:
clause (list): 節.
"""
assert len(clause) > 0
if len(clause) == 1:
self.enqueue(clause[0])
else:
i = len(self.clauses)
self.clauses.append(clause)
self.watches[clause[0]^1].append(i)
self.watches[clause[1]^1].append(i)
[ドキュメント]
def addDimacsClause(self, dimacsLits):
"""DIMACS形式の節 (正負の変数番号のリスト)を追加する.
Args:
dimacsLits (list): DIMACS形式の節.
"""
clause = []
for dimacsLit in dimacsLits:
v = abs(dimacsLit) - 1
while v >= self.nVars():
self.newVar()
lit = self.toLit(v, 1 if dimacsLit < 0 else 0)
clause.append(lit)
self.addInternalClause(clause)
[ドキュメント]
def toDimacsLit(self, vname):
"""変数名がvnameの変数をDIMACS形式のリテラル (正負の変数番号)に変換する.
変数名が `~' から始まれば負リテラルとする.
Args:
vname (str): 変数名.
Returns:
DIMACS形式のリテラル.
"""
positive = True
if vname.startswith("~"):
vname = vname[1:]
positive = False
dimacsLit = self.vname2dimacslit.get(vname, 0)
if dimacsLit == 0:
dimacsLit = self.newVar(vname) + 1
self.vname2dimacslit[vname] = dimacsLit
return dimacsLit if positive else -dimacsLit
[ドキュメント]
def toDimacsLits(self, clause):
"""変数名のリストをDIMACS形式の節に変換する.
変数名が `~' から始まれば負リテラルとする.
Args:
clause (list): 変数名のリスト.
Returns:
DIMACS形式の節.
"""
dimacsLits = [ self.toDimacsLit(str(lit)) for lit in clause ]
return dimacsLits
[ドキュメント]
def addClause(self, clause):
"""変数名のリストの形式の節を追加する.
変数名が `~' から始まれば負リテラルとする.
Args:
clause (list): 変数名のリスト.
"""
dimacsLits = clause
if isinstance(clause, str):
return
dimacsLits = self.toDimacsLits(clause)
self.addDimacsClause(dimacsLits)
[ドキュメント]
def add(self, *clauses):
"""節のリスト (変数名のリストの形式)を追加する.
Args:
clauses (list): 節のリスト.
"""
for clause in clauses:
self.addClause(clause)
[ドキュメント]
def load(self, file=None):
"""ファイルから節を読込んで追加する.
Args:
file (str): ファイル名 (Noneなら標準入力).
"""
if file is None:
inp = sys.stdin
elif isinstance(file, str):
inp = open(file, encoding="utf-8")
else:
inp = file
for line in inp:
line = re.sub(r"\s+", " ", line).strip()
if line == "" or line.startswith("#"):
continue
clause = []
for s in line.split(" "):
if s.startswith("~"):
clause.append(Bool(s[1:], False))
else:
clause.append(Bool(s))
self.addClause(clause)
input.close()
import sys
import re
import time
from .util import Bool