Resolver 2-SAT (satisfacción booleana)

16

El problema general de SAT (satisfacción booleana) es NP-completo. Pero 2-SAT , donde cada cláusula tiene sólo 2 variables, se encuentra en P . Escribe un solucionador para 2-SAT.

Entrada:

Una instancia de 2-SAT, codificada en CNF como sigue. La primera línea contiene V, el número de variables booleanas y N, el número de cláusulas. Luego siguen N líneas, cada una con 2 enteros distintos de cero que representan los literales de una cláusula. Los enteros positivos representan la variable booleana dada y los enteros negativos representan la negación de la variable.

Ejemplo 1

entrada

4 5
1 2
2 3
3 4
-1 -3
-2 -4

que codifica la fórmula (x 1 o x 2 ) y (x 2 o x 3 ) y (x 3 o x 4 ) y (no x 1 o no x 3 ) y (no x 2 o no x 4 ) .

La única configuración de las 4 variables que hace que toda la fórmula sea verdadera es x 1 = falso, x 2 = verdadero, x 3 = verdadero, x 4 = falso , por lo que su programa debería generar la línea única

salida

0 1 1 0

representando los valores de verdad de las variables V (en orden de x 1 a x V ). Si hay varias soluciones, puede generar cualquier subconjunto no vacío de ellas, una por línea. Si no hay solución, debe dar salida UNSOLVABLE.

Ejemplo 2

entrada

2 4
1 2
-1 2
-2 1
-1 -2

salida

UNSOLVABLE

Ejemplo 3

entrada

2 4
1 2
-1 2
2 -1
-1 -2

salida

0 1

Ejemplo 4

entrada

8 12
1 4
-2 5
3 7
2 -5
-8 -2
3 -1
4 -3
5 -4
-3 -7
6 7
1 7
-7 -1

salida

1 1 1 1 1 1 0 0
0 1 0 1 1 0 1 0
0 1 0 1 1 1 1 0

(o cualquier subconjunto no vacío de esas 3 líneas)

Su programa debe manejar todos los N, V ​​<100 en un tiempo razonable. Pruebe este ejemplo para asegurarse de que su programa pueda manejar una gran instancia. El programa más pequeño gana.

Keith Randall
fuente
Usted menciona que 2-SAT está en P, pero no es un requisito que la solución deba ejecutarse en tiempo polinómico ;-)
Timwi
@Timwi: No, pero tiene que manejar V = 99 en un tiempo razonable ...
Keith Randall

Respuestas:

4

Haskell, 278 caracteres

(∈)=elem
r v[][]=[(>>=(++" ").show.fromEnum.(∈v))]
r v[]c@(a:b:_)=r(a:v)c[]++r(-a:v)c[]++[const"UNSOLVABLE"]
r v(a:b:c)d|a∈v||b∈v=r v c d|(-a)∈v=i b|(-b)∈v=i a|1<3=r v c(a:b:d)where i w|(-w)∈v=[]|1<3=r(w:v)(c++d)[]
t(n:_:c)=(r[][]c!!0)[1..n]++"\n"
main=interact$t.map read.words

No es fuerza bruta. Se ejecuta en tiempo polinomial. Resuelve el problema difícil (60 variables, 99 cláusulas) rápidamente:

> time (runhaskell 1933-2Sat.hs < 1933-hard2sat.txt)
1 1 1 0 0 0 0 0 0 1 1 0 0 1 0 1 1 1 0 1 1 0 0 1 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 1 0 0 0 0 1 0 1 1 1 1 0 

real 0m0.593s
user 0m0.502s
sys  0m0.074s

Y, de hecho, ¡la mayor parte de ese tiempo se gasta compilando el código!

Archivo fuente completo, con casos de prueba y pruebas de verificación rápida disponibles .

Ungolf'd:

-- | A variable or its negation
-- Note that applying unary negation (-) to a term inverts it.
type Term = Int

-- | A set of terms taken to be true.
-- Should only contain  a variable or its negation, never both.
type TruthAssignment = [Term]

-- | Special value indicating that no consistent truth assignment is possible.
unsolvable :: TruthAssignment
unsolvable = [0]

-- | Clauses are a list of terms, taken in pairs.
-- Each pair is a disjunction (or), the list as a whole the conjuction (and)
-- of the pairs.
type Clauses = [Term]

-- | Test to see if a term is in an assignment
(∈) :: Term -> TruthAssignment -> Bool
a∈v = a `elem` v;

-- | Satisfy a set of clauses, from a starting assignment.
-- Returns a non-exhaustive list of possible assignments, followed by
-- unsolvable. If unsolvable is first, there is no possible assignment.
satisfy :: TruthAssignment -> Clauses -> [TruthAssignment]
satisfy v c@(a:b:_) = reduce (a:v) c ++ reduce (-a:v) c ++ [unsolvable]
  -- pick a term from the first clause, either it or its negation must be true;
  -- if neither produces a viable result, then the clauses are unsolvable
satisfy v [] = [v]
  -- if there are no clauses, then the starting assignment is a solution!

-- | Reduce a set of clauses, given a starting assignment, then solve that
reduce :: TruthAssignment -> Clauses -> [TruthAssignment]
reduce v c = reduce' v c []
  where
    reduce' v (a:b:c) d
        | a∈v || b∈v = reduce' v c d
            -- if the clause is already satisfied, then just drop it
        | (-a)∈v = imply b
        | (-b)∈v = imply a
            -- if either term is not true, the other term must be true
        | otherwise = reduce' v c (a:b:d)
            -- this clause is still undetermined, save it for later
        where 
          imply w
            | (-w)∈v = []  -- if w is also false, there is no possible solution
            | otherwise = reduce (w:v) (c++d)
                -- otherwise, set w true, and reduce again
    reduce' v [] d = satisfy v d
        -- once all caluses have been reduced, satisfy the remaining

-- | Format a solution. Terms not assigned are choosen to be false
format :: Int -> TruthAssignment -> String
format n v
    | v == unsolvable = "UNSOLVABLE"
    | otherwise = unwords . map (bit.(∈v)) $ [1..n]
  where
    bit False = "0"
    bit True = "1"

main = interact $ run . map read . words 
  where
    run (n:_:c) = (format n $ head $ satisfy [] c) ++ "\n"
        -- first number of input is number of variables
        -- second number of input is number of claues, ignored
        -- remaining numbers are the clauses, taken two at a time

En la versión de golf'd, satisfyy formatse ha incorporado reduce, aunque para evitar pasar n, reducedevuelve una función de una lista de variables ( [1..n]) al resultado de la cadena.


  • Editar: (330 -> 323) hizo sun operador, mejor manejo de la nueva línea
  • Editar: (323 -> 313) el primer elemento de una lista perezosa de resultados es más pequeño que un operador de cortocircuito personalizado; renombrada función de solucionador principal porque me gusta usarla como operador.
  • Editar: (313 -> 296) mantenga las cláusulas como una sola lista, no como una lista de listas; procesarlo dos elementos a la vez
  • Editar: (296 -> 291) fusionó las dos funciones recursivas mutuamente; era más barato en línea, así que ahora cambia el nombre de la prueba
  • Editar: (291 -> 278) formato de salida en línea en generación de resultados
MtnViewMark
fuente
4

J, 119 103

echo'UNSOLVABLE'"_`(#&c)@.(*@+/)(3 :'*./+./"1(*>:*}.i)=y{~"1 0<:|}.i')"1 c=:#:i.2^{.,i=:0&".;._2(1!:1)3

Editar: Eliminado (n#2)y n=:, por lo tanto , así como eliminar algunos parens de rango (gracias, isawdrones). Tácito-> explícito y diádico-> monádico, eliminando unos pocos caracteres más cada uno. }.}.a }.,.

Editar: Whoops. No solo no es una solución para N grande, sino también i. 2^99x-> "error de dominio" para agregar insulto a la estupidez.

Aquí está la versión original sin golf y una breve explicación.

input=:0&".;._2(1!:1)3
n =:{.{.input
clauses=:}.input
cases=:(n#2)#:i.2^n
results =: clauses ([:*./[:+./"1*@>:@*@[=<:@|@[{"(0,1)])"(_,1) cases
echo ('UNSOLVABLE'"_)`(#&cases) @.(*@+/) results
  • input=:0&".;._2(1!:1)3 corta la entrada en las nuevas líneas y analiza los números en cada línea (acumulando los resultados en la entrada).
  • n está asignado a n, matriz de cláusulas asignada a clauses(no necesita el recuento de cláusulas)
  • caseses 0..2 n -1 convertido a dígitos binarios (todos los casos de prueba)
  • (Long tacit function)"(_,1)se aplica a cada caso casescon todos clauses.
  • <:@|@[{"(0,1)] obtiene una matriz de los operandos de las cláusulas (tomando abs (número de operación) - 1 y desreferenciando del caso, que es una matriz)
  • *@>:@*@[ obtiene una matriz en forma de cláusula de bits 'no no' (0 para no) a través del abuso de signum.
  • = aplica los bits no a los operandos.
  • [:*./[:+./"1se aplica +.(y) a través de las filas de la matriz resultante, y *.(o) a través del resultado de eso.
  • Todos esos resultados terminan como una matriz binaria de 'respuestas' para cada caso.
  • *@+/ aplicado a los resultados da un 0 si hay resultados y 1 si no hay ninguno.
  • ('UNSOLVABLE'"_) `(#&cases) @.(*@+/) results ejecuta una función constante que proporciona 'NO RESUELVE' si 0, y una copia de cada elemento de 'solución' de casos si 1.
  • echo imprime magia el resultado.
Jesse Millikan
fuente
Podrías eliminar los parens alrededor de los argumentos de rango. "(_,1)a "_ 1. #:funcionaría sin el argumento izquierdo.
isawdrones
@isawdrones: Creo que la respuesta tradicional sería aplastar mi espíritu produciendo una respuesta la mitad de tiempo. "Grita y salta", como diría el Kzin. Gracias, sin embargo, eso elimina los 10 caracteres impares ... Podría tener menos de 100 cuando vuelva a hacerlo.
Jesse Millikan
¡+1 por la explicación agradable y detallada, lectura muy fascinante!
Timwi
Probablemente no manejará N = V = 99 en un tiempo razonable. Prueba el gran ejemplo que acabo de agregar.
Keith Randall el
3

K - 89

El mismo método que la solución J.

n:**c:.:'0:`;`0::[#b:t@&&/+|/''(0<'c)=/:(t:+2_vs!_2^n)@\:-1+_abs c:1_ c;5:b;"UNSOLVABLE"]
isawdrones
fuente
Bien, no sabía que había una implementación gratuita de K.
Jesse Millikan
Probablemente no manejará N = V = 99 en un tiempo razonable. Prueba el gran ejemplo que acabo de agregar.
Keith Randall
2

Rubí, 253

n,v=gets.split;d=[];v.to_i.times{d<<(gets.split.map &:to_i)};n=n.to_i;r=[1,!1]*n;r.permutation(n){|x|y=x[0,n];x=[0]+y;puts y.map{|z|z||0}.join ' 'or exit if d.inject(1){|t,w|t and(w[0]<0?!x[-w[0]]:x[w[0]])||(w[1]<0?!x[-w[1]]:x[w[1]])}};puts 'UNSOLVABLE'

Pero es lento :(

Bastante legible una vez expandido:

n,v=gets.split
d=[]
v.to_i.times{d<<(gets.split.map &:to_i)} # read data
n=n.to_i
r=[1,!1]*n # create an array of n trues and n falses
r.permutation(n){|x| # for each permutation of length n
    y=x[0,n]
    x=[0]+y
    puts y.map{|z| z||0}.join ' ' or exit if d.inject(1){|t,w| # evaluate the data (magic!)
        t and (w[0]<0 ? !x[-w[0]] : x[w[0]]) || (w[1]<0 ? !x[-w[1]] : x[w[1]])
    }
}
puts 'UNSOLVABLE'
Matma Rex
fuente
Probablemente no manejará N = V = 99 en un tiempo razonable. Prueba el gran ejemplo que acabo de agregar.
Keith Randall el
1

OCaml + Batteries, 438 436 caracteres

Requiere un OCaml Baterías incluidas de nivel superior:

module L=List
let(%)=L.mem
let rec r v d c n=match d,c with[],[]->[String.join" "[?L:if x%v
then"1"else"0"|x<-1--n?]]|[],(x,_)::_->r(x::v)c[]n@r(-x::v)c[]n@["UNSOLVABLE"]|(x,y)::c,d->let(!)w=if-w%v
then[]else r(w::v)(c@d)[]n in if x%v||y%v then r v c d n else if-x%v then!y else if-y%v then!x else r v c((x,y)::d)n
let(v,_)::l=L.of_enum(IO.lines_of stdin|>map(fun s->Scanf.sscanf s"%d %d"(fun x y->x,y)))in print_endline(L.hd(r[][]l v))

Debo confesar que esta es una traducción directa de la solución de Haskell. En mi defensa, que a su vez es una codificación directa del algoritmo presentado aquí [PDF], con la mutua satisfy- eliminaterecursividad, todo en una sola función. Una versión no ofuscada del código, menos el uso de baterías, es:

let rec satisfy v c d = match c, d with
| (x, y) :: c, d ->
    let imply w = if List.mem (-w) v then raise Exit else satisfy (w :: v) (c @ d) [] in
    if List.mem x v || List.mem y v then satisfy v c d else
    if List.mem (-x) v then imply y else
    if List.mem (-y) v then imply x else
    satisfy v c ((x, y) :: d)
| [], [] -> v
| [], (x, _) :: _ -> try satisfy (x :: v) d [] with Exit -> satisfy (-x :: v) d []

let rec iota i =
    if i = 0 then [] else
    iota (i - 1) @ [i]

let () = Scanf.scanf "%d %d\n" (fun k n ->
    let l = ref [] in
    for i = 1 to n do
        Scanf.scanf "%d %d\n" (fun x y -> l := (x, y) :: !l)
    done;
    print_endline (try let v = satisfy [] [] !l in
    String.concat " " (List.map (fun x -> if List.mem x v then "1" else "0") (iota k))
    with Exit -> "UNSOLVABLE") )

(El iota kjuego de palabras espero que me perdones).

Matías Giovannini
fuente
Es bueno ver la versión OCaml! Es el comienzo de un bonito Rosetta Stone para programas funcionales. Ahora, si pudiéramos obtener versiones de Scala y F # ... - En cuanto al algoritmo - ¡No vi ese PDF hasta que lo mencionaste aquí! Basé mi implementación en la descripción de la página de Wikipedia de "Retroceso limitado".
MtnViewMark