3-SAT Solver Python

Oct 22 2020

Eu escrevi um solver 3-SAT com base neste prompt:

Alice recentemente começou a trabalhar para uma empresa de design de hardware e como parte de seu trabalho, ela precisa identificar defeitos em circuitos integrados fabricados. Uma abordagem para identificar esses defeitos se resume a resolver uma instância de satisfatibilidade. Ela precisa de sua ajuda para escrever um programa para fazer essa tarefa.

Entrada
A primeira linha de entrada contém um único inteiro, não mais que 5, indicando o número de casos de teste a seguir. A primeira linha de cada caso de teste contém dois inteiros n e m, onde 1 ≤ n ≤ 20 indica o número de variáveis ​​e 1 ≤ m ≤ 100 indica o número de cláusulas. Então, seguem m linhas correspondentes a cada cláusula. Cada cláusula é uma disjunção de literais na forma Xi ou ~ Xi para algum 1 ≤ i ≤ n, onde ~ Xi indica a negação do literal Xi. O operador “ou” é denotado por um caractere 'v' e é separado dos literais com um único espaço.

Saída
Para cada caso de teste, exiba satisfatível em uma única linha se houver uma atribuição satisfatória; caso contrário, exibirá insatisfatório.

Amostra de entrada

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 

Saída de amostra

satisfiable
unsatisfiable

Este código basicamente mantém o mySetsque é uma lista de conjuntos, que representam combinações possíveis de literais que podem tornar toda a afirmação verdadeira. Cada vez que analisamos uma nova cláusula, verificamos se sua negação já existe em um conjunto, caso exista, o conjunto não é incluído.

Isso funciona, mas é um pouco lento.

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

Acho que o problema está aqui, devido aos loops for indentados. O 3-SAT deve ser exponencial, mas eu gostaria de acelerar um pouco (talvez removendo um loop?)

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

Respostas

2 RootTwo Oct 23 2020 at 04:47

Se você instrumentar seu código com algumas instruções de impressão colocadas estrategicamente, verá que há alguns cálculos repetidos em andamento. No segundo caso de teste, ao processar a cláusula X2 v ~X3, o conjunto {'X1', 'X2'}é adicionado mySetsduas vezes. Ao processar a cláusula X3 v ~X1, o conjunto {'X3', 'X1', 'X2'}é adicionado mySetstrês vezes.

Para casos grandes, pode acelerar as coisas mudar mySetspara um em set()vez de uma lista para eliminar as duplicatas. Então, os conjuntos internos precisam ser frozensets.

mySetsé um conjunto de soluções possíveis que satisfazem todas as cláusulas, por isso mudei o nome para candidates.

Se você inicializar candidatespara conter um único conjunto vazio, a primeira cláusula não precisará ser tratada separadamente.

Eu acho que você pode parar quando candidatesestiver vazio.

Além disso, divida o código em funções.

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

Chamado como:

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

ou

import sys

main(sys.stdin)        
3 Reinderien Oct 23 2020 at 01:58

Aqui está uma implementação sugerida que não muda basicamente nada em seu algoritmo, mas

  • tem indentação adequada
  • usa um pouco de sugestão de tipo
  • usa conjuntos literais e geradores
  • usa _para variáveis ​​"não utilizadas"
  • adiciona um parse_clause()porque o código da cláusula é repetido
  • usa um StringIO, para esses fins, para efetivamente zombar stdine usar o exemplo de entrada que você mostrou
  • usa nomes compatíveis com PEP8 (com sublinhados)
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')