3-SAT Solver Python
Napisałem solver 3-SAT w oparciu o następujący monit:
Alice niedawno rozpoczęła pracę w firmie zajmującej się projektowaniem sprzętu i w ramach jej pracy musi zidentyfikować usterki w wyprodukowanych układach scalonych. Podejście do identyfikacji tych defektów sprowadza się do rozwiązania zadowalającej instancji. Potrzebuje twojej pomocy, aby napisać program do wykonania tego zadania.
Dane wejściowe
Pierwszy wiersz danych wejściowych zawiera jedną liczbę całkowitą, nie większą niż 5, wskazującą liczbę przypadków testowych do wykonania. W pierwszym wierszu każdego przypadku testowego znajdują się dwie liczby całkowite n i m, gdzie 1 ≤ n ≤ 20 oznacza liczbę zmiennych, a 1 ≤ m ≤ 100 oznacza liczbę klauzul. Następnie następuje m wierszy odpowiadających każdej klauzuli. Każda klauzula jest dysjunkcją literałów w postaci Xi lub ~ Xi dla jakiegoś 1 ≤ i ≤ n, gdzie ~ Xi oznacza negację literału Xi. Operator „lub” jest oznaczony znakiem „v” i jest oddzielony od literałów pojedynczą spacją.Dane wyjściowe
Dla każdego przypadku testowego wyświetl zadowalający w pojedynczym wierszu, jeśli istnieje zadowalające przypisanie; w przeciwnym razie wyświetlać niezadowalające.Przykładowe wejście
2 3 3 X1 v X2 ~X1 ~X2 v X3 3 5 X1 v X2 v X3 X1 v ~X2 X2 v ~X3 X3 v ~X1 ~X1 v ~X2 v ~X3Przykładowe wyjście
satisfiable unsatisfiable
Ten kod zasadniczo zachowuje mySetslistę zestawów, z których wszystkie reprezentują możliwe kombinacje literałów, które mogą sprawić, że całe stwierdzenie będzie prawdziwe. Za każdym razem, gdy parsujemy nową klauzulę, sprawdzamy, czy jej negacja już istnieje w zestawie, jeśli tak, zestaw nie jest uwzględniany.
To działa, ale działa trochę wolno.
import sys
cases = int(sys.stdin.readline())
def GetReverse(literal):
if literal[0] == '~':
return literal[1:]
else:
return '~' + literal
for i in range(cases):
vars, clauses = map(int, sys.stdin.readline().split())
mySets = []
firstClause = sys.stdin.readline().strip().split(" v ")
for c in firstClause:
this = set()
this.add(c)
mySets.append(this)
for i in range(clauses-1):
tempSets = []
currentClause = sys.stdin.readline().strip().split(" v ")
for s in mySets:
for literal in currentClause:
if not s.__contains__(GetReverse(literal)):
newset = s.copy()
newset.add(literal)
tempSets.append(newset)
mySets = tempSets
if mySets:
print("satisfiable")
else:
print("unsatisfiable")
Myślę, że problem tkwi tutaj, ze względu na wcięte pętle for. 3-SAT ma być wykładniczy, ale chciałbym to trochę przyspieszyć (może usuwając pętlę?)
for i in range(clauses-1):
tempSets = []
currentClause = sys.stdin.readline().strip().split(" v ")
for s in mySets:
for literal in currentClause:
if not s.__contains__(GetReverse(literal)):
newset = s.copy()
newset.add(literal)
tempSets.append(newset)
mySets = tempSets
Odpowiedzi
Jeśli oprzyrządujesz swój kod jakimś strategicznie rozmieszczonym poleceniami print, zobaczysz, że są wykonywane powtarzające się obliczenia. W drugim przypadku testowym podczas przetwarzania klauzuli X2 v ~X3zestaw {'X1', 'X2'}jest dodawany mySetsdwukrotnie. Podczas przetwarzania klauzuli X3 v ~X1zestaw {'X3', 'X1', 'X2'}jest dodawany mySetstrzykrotnie.
W dużych przypadkach może to przyspieszyć zmianę mySetsna a set()zamiast listy w celu wyeliminowania duplikatów. Wtedy wewnętrzne zestawy muszą być frozensets.
mySetsjest zbiorem możliwych rozwiązań, które spełniają wszystkie klauzule, więc zmieniłem jego nazwę na candidates.
Jeśli zainicjujesz, candidatesaby zawierał pojedynczy pusty zestaw, to pierwsza klauzula nie musi być obsługiwana oddzielnie.
Myślę, że możesz przestać w każdej chwili candidatesjest pusty.
Podziel kod na funkcje.
def is_satisfiable(n_vars, clauses):
candidates = {frozenset()}
for clause in clauses:
temp = set()
for s in candidates:
for literal in clause:
if GetReverse(literal) not in s:
temp.add(s | {literal})
candidates = temp
if len(candidates) == 0:
return False
return True
def load_case(f):
n_vars, n_clauses = f.readline().split()
clauses = [f.readline().strip().split(' v ') for _ in range(int(n_clauses))]
return int(n_vars), clauses
def main(f=sys.stdin):
num_cases = int(f.readline())
for i in range(num_cases):
n_vars, clauses = load_case(f)
result = is_satisfiable(n_vars, clauses)
print(f"{'satisfiable' if result else 'unsatisfiable'}")
Nazwany jak:
import io
data = """
2
3 3
X1 v X2
~X1
~X2 v X3
3 5
X1 v X2 v X3
X1 v ~X2
X2 v ~X3
X3 v ~X1
~X1 v ~X2 v ~X3
""".strip()
main(io.StringIO(data))
lub
import sys
main(sys.stdin)
Oto sugerowana implementacja, która w zasadzie nie zmienia nic w twoim algorytmie, ale
- ma odpowiednie wcięcie
- używa trochę podpowiedzi typu
- używa zestawów literałów i generatorów
- używa
_dla „nieużywanych” zmiennych - dodaje a,
parse_clause()ponieważ kod klauzuli jest powtarzany - używa
StringIOdo tych celów a, aby skutecznie wyszydzaćstdini używać przykładowych danych wejściowych, które pokazałeś - używa nazw zgodnych z PEP8 (z podkreśleniami)
from io import StringIO
from typing import List
stdin = StringIO('''2
3 3
X1 v X2
~X1
~X2 v X3
3 5
X1 v X2 v X3
X1 v ~X2
X2 v ~X3
X3 v ~X1
~X1 v ~X2 v ~X3
'''
)
def get_reverse(literal: str) -> str:
if literal[0] == '~':
return literal[1:]
return '~' + literal
def parse_clause() -> List[str]:
return stdin.readline().strip().split(' v ')
n_cases = int(stdin.readline())
for _ in range(n_cases):
n_vars, n_clauses = (int(s) for s in stdin.readline().split())
my_sets = [{c} for c in parse_clause()]
for _ in range(n_clauses - 1):
temp_sets = []
current_clause = parse_clause()
for s in my_sets:
for literal in current_clause:
if get_reverse(literal) not in s:
new_set = s.copy()
new_set.add(literal)
temp_sets.append(new_set)
my_sets = temp_sets
if my_sets:
print('satisfiable')
else:
print('unsatisfiable')