Adrianistán

Resolviendo el problema de las 3 jarras usando automaton y clpz

19/06/2022

El problema de las 3 jarras (Three jugs) consiste en 3 jarras de agua, de 3L, 5L y 8L. Las dos primeras están vacías y la de 8L está llena. El objetivo es que, echando el agua de unas jarras a otras, consigamos tener en alguna jarra 4L exactos. Este problema lo vamos a resolver en Prolog intentando usar una técnica más avanzada de la librería clpz, automaton. Los usos básicos de clpz ya los vimos en otro post.

.

¿Qué nos ofrece automaton?

Los predicados automaton (automaton/3 y automaton/8) se usan en problemas de scheduling para obtener el orden de las tareas a realizar si estas pueden describirse como un autómata finito determinista (AFD, en inglés las siglas son DFA). Es decir, si describimos el problema como un AFD, podremos pasárselo a clpz para que encuentre la forma más corta de resolver realizar las tareas.

Veamos un ejemplo muy sencillo. Nosotros podemos definir un AFD para analizar la expresión regular "0*1*0*". Es decir, N ceros, seguido de N unos, seguido de N ceros. El diagrama de estados sería el siguiente:


Diagrama sacado del libro Constraint Solving and Planning with Picat

Nosotros empezamos en el estado S1. Si leemos un cero, seguimos en S1. Si leemos un uno, pasamos a S2. En S2 si recibimos un uno seguimos en S2. Si recibimos un cero pasamos a S3. Si recibimos un 1 es un error. Si recibimos un cero, seguimos en S3 hasta acabar.

¿Cómo pasamos esto a automaton? En primer lugar, vamos a usar el predicado automaton/3 que es más sencillo. El otro contiene más opciones y se puede usar en automátas más complejos. El predicado tiene 3 argumentos: lista de entradas, estados especiales (entradas y salidas; source y sink) y finalmente un listado de arcos entre estados tomando una acción. Podemos usarlo para comprobar si una lista cumple el autómata.


?- Vs = [0,0,1,1,1,0], automaton(Vs, [source(s1), sink(s1), sink(s2), sink(s3)], [arc(s1,0,s1), arc(s1,1,s2),arc(s2,1,s2),arc(s2,0,s3),arc(s3,0,s3)]).

Pero como estamos en Prolog, también podemos preguntar qué listas de longitud N se pueden construir. Para ello necesitamos llamar a label/1, ya que estamos en clpz. Un ejemplo con N=6


?- length(Vs, 6), automaton(Vs, [source(s1), sink(s1), sink(s2), sink(s3)], [arc(s1,0,s1), arc(s1,1,s2),arc(s2,1,s2),arc(s2,0,s3),arc(s3,0,s3)]), label(Vs).

El problema de las 3 jarras

El principal problema de resolver problemas mediante su traslación a DFA, es que muchas veces se requieren muchos pasos para transformar los datos a un DFA correcto.

En este caso lo que voy a hacer es buscar una representación programática del estado. Como son 3 jarras, usaré un guión para separar entre los litros que contiene cada jarra en este momento. Es decir, el término 1-2-5 representará al estado en que la primera jarra tiene 1L, la segunda tiene 2L y la tercera tiene 5L. Con esta representación el estado inicial será 0-0-8 y solo hay uno.

Sacar los estados finales ya es más complicado. Para ello primero voy a generar todos los posibles estados que se pueden hacer con 3 jarras y 8L. De esos filtraré en los que alguna jarra contiene 4L exactos.


states(States) :-
    [A,B,C] ins 0..8,
    A+B+C #= 8,
    findall(A-B-C, label([A,B,C]), States).

end_states(States, EndStates) :-
    findall(sink(S), (
		member(S, States),
		S = A-B-C,
		(A = 4; B = 4; C = 4)
	    ), EndStates).

Ahora toca la parte maś compleja que es definir los arcos. Evidentemente no los vamos a escribir a mano, sino que vamos a hacer reglas. Como tenemos ya todos los estados de 8L, vamos a ir filtrando los arcos que van entre dos estados posibles diferentes con una acción. Las acciones las vamos a numerar del 1 al 6, ya que automaton requiere que estas acciones sean números enteros.

De un estado se puede pasar a otro a través de un arco haciendo una acción si tiene sentido el movimiento de litros que hacemos (llenamos la jarra hasta el tope o hasta que nos quedamos sin agua en el origen). Usamos aritmética de clpz.


arcs(States, Arcs) :-
    findall(arc(S0, Action, S1),(
		member(S0, States),
		member(S1, States),
		S0 \= S1,
		arc(S0, Action, S1)
	    ), Arcs).

% Arcs
% Pour from A to B
arc(A-B-C, 1, X-Y-Z) :-
    Dif #= min(A, 5-B),
    X #= A - Dif,
    Y #= B + Dif,
    Z = C.
% Pour from A to C
arc(A-B-C, 2, X-Y-Z) :-
    Dif #= min(A, 8-C),
    X #= A - Dif,
    Y = B,
    Z #= C + Dif.
% Pour from B to C
arc(A-B-C, 3, X-Y-Z) :-
    Dif #= min(B, 8-C),
    X = A,
    Y #= B - Dif,
    Z #= C + Dif.
% Pour from B to A
arc(A-B-C, 4, X-Y-Z) :-
    Dif #= min(B, 3-A),
    X #= A + Dif,
    Y #= B - Dif,
    Z = C.
% Pour from C to A
arc(A-B-C, 5, X-Y-Z) :-
    Dif #= min(C, 3-A),
    X #= A + Dif,
    Y = B,
    Z #= C - Dif.
% Pour from C to B
arc(A-B-C, 6, X-Y-Z) :-
    Dif #= min(C, 5-B),
    X = A,
    Y #= B + Dif,
    Z #= C - Dif.

¡Ya tenemos todas las piezas! Solo hace falta juntarlas para resolver el problema.


:- use_module(library(lists)).
:- use_module(library(clpz)).

solve(Vs) :-
    states(States),
    end_states(States, EndStates),
    arcs(States, Arcs),
    length(Vs, _),
    automaton(Vs, [source(0-0-8)|EndStates], Arcs),
    label(Vs).

% ?- solve(Vs).
%   Vs = [6,4,2,4,6,4]
%;  Vs = [5,1,5,1,3,1,5]
%;  Vs = [6,4,2,4,6,4,2]
%;  Vs = [5,1,5,1,3,1,5,1]
%;  Vs = [5,1,6,4,2,4,6,4]
%;  Vs = [5,2,6,4,2,4,6,4]
%; ...

La solución más corta parece ser [6,4,2,4,6,4]. Con esto ya tendríamos el problema resuelto, pero es cierto que es complicado de interpretar. Vamos a añadir un predicado para explicar la solución.


print(Xs) :- print(Xs, 0-0-8).
print([], _).
print([X|Xs], S) :-
    arc(S, X, S1),
    S = A-B-C,
    S1 = D-E-F,
    format("Going from (~d,~d,~d) to (~d, ~d, ~d)\n", [A,B,C,D,E,F]),
    print(Xs, S1).

Con esto ya tenemos resuelto el problema de las 3 jarras.

Tags: prolog programacion automatas puzzle tutorial clpz