3-SAT Solver Python

Oct 22 2020

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 ~X3 

Przykł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

2 RootTwo Oct 23 2020 at 04:47

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)        
3 Reinderien Oct 23 2020 at 01:58

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')