4 de Enero, 2006

Misioneros y caníbales

Se trata de un problema que tuve que resolver en Prolog para una de las prácticas de lógica de primer orden. Como siempre se me han dado muy mal ese tipo de acertijos lógicos (y no me refiero a Prolog), me impresionaba que mi programa diera el resultado, porque yo no sabía resolverlo a mano.

Además pasa que Prolog es un lenguaje que sigue el paradigma declarativo, mientras que yo ya estaba malformado casi irremediablemente por el uso de lenguajes imperativos. En este lenguaje se define un universo limitado, principalmente mediante asociaciones y declaraciones tipo A → B (A implica B). Una vez hemos aportado suficientes datos, indicamos un estado inicial y un estado objetivo. Y entonces Prolog mágicamente da con el resultado, sin que nosotros le indiquemos explícitamente cómo hacerlo.

Ya lo dice la tercera ley de Clarke: Toda tecnología lo suficientemente avanzada es indistinguible de la magia, y además yo añadiría un corolario: La capacidad de distinción entre magia y tecnología depende del individuo. Es que ahora que Prolog no hace magia :D.

Prolog emplea una técnica en la toma de decisiones que se conoce como backtracking, y consiste en poder volver atrás al punto anterior cuando se toma una decisión que rompe alguna de las condiciones que hemos impuesto al problema, por ejemplo: el problema de los misioneros y los caníbales.

Tres misioneros y tres caníbales quieren cruzar un río. Solo hay una canoa que puede ser usada por una o dos personas, ya sean misioneros o caníbales.

Hay que tener cuidado en que en ningún momento el número de caníbales supere al de misioneros en ninguna de las dos orillas, o se los comerán.

Además está implícito en la descripción que mientras queden individuos en la primera orilla, alguien tendrá que llevar la canoa de vuelta.

Voy a plantear una solución en Ruby, que sigo practicando. Tampoco me he querido calentar la cabeza demasiado, así que voy a dar una solución, sin plantearme si es la mejor o no. De esta forma simplificamos sin tener que calcular todas las soluciones, y acabamos cuando damos con todo el personal y la canoa en la orilla adecuada.

Para implementar mi backtracking voy a emplear las funciones push y pop los objetos Array (vector) para almacenar el estado en cada decisión (individuos a cada lado, posición de la canoa, posibles viajes probados), y así si llegamos a un punto sin solución, podremos volver atrás y probar con otro posible viaje.

Me ha venido bien, porque me he dado de bruces con una característica de Ruby que no esperaba. En este lenguaje todo es una referencia, así que explícitamente tenemos que pedir que nos hagan una copia de un vector (con dup o clone, no vale una asignación normal), pero sorpresa: no se duplica recursivamente, así que si un vector contiene a otro, tendremos que preocuparnos nosotros de duplicar a esos dos niveles. En el código indico los casos, que se verá más claro.

He empleado vectores para manejar los estados y los viajes posibles, con constantes para los índices. Llamemos a los misioneros M y a los caníbales C:

  • Estado_ini: 3M y 3C en la izquierda, 0M y 0C en la derecha, y la canoa en la izquierda. Aquí es donde empezamos.
  • Estado_fin: 0M y 0C en la izquierda, 3M y 3C en la derecha, y la canoa en la derecha. Aquí es a donde queremos llegar.
  • Viajes: 1M y 0C, 0M y 1C, 1M y 1C, 2M y 0C, 0M y 2C. Son las posibilidades de personal que puede ir en la canoa. Como no busco todas las soluciones, el resultado dependerá del orden en el que se prueben estas posibilidades.

He definido 3 funciones auxiliares, para simplificar el algoritmo:

  • valido?(estado): devuelve cierto si un estado es válido. Comprueba que el número de individuos sea correcto, porque probamos todos los viajes, aunque sean imposibles. Además verifica las condiciones del problema (que no se coman a la gente).
  • canoa(estado, viaje): modifica el estado según el viaje indicado.
  • muestraEstado(estado): muestra el estado de una forma más o menos bonita.

Y finalmente, el código:

#!/usr/bin/ruby
=begin

Tres misioneros y tres caníbales quieren cruzar un río. Solo hay una canoa
que puede ser usada por una o dos personas, ya sean misioneros o caníbales.

Hay que tener cuidado en que en ningún momento el número de caníbales supere
al de misioneros en ninguna de las dos orillas, o se los comerán.

Juan J. Martínez <reidrac*en*blackshell.usebox.net>

=end

# algunas constantes útiles para facilitar el trabajo con el vector
Izq = 0
Der = 1
Canoa = 2
M = 0
C = 1

# todos a la izquierda, y la posición de la canoa
Estado_ini = [ [ 3, 3 ], [ 0, 0 ], Izq ]
# todos a la derecha, y la posición de la canoa
Estado_fin = [ [ 0, 0 ], [ 3, 3 ], Der ]

# viajes posibles (misionero, caníbal)
Viajes = [ [ 1, 0], [0, 1], [1, 1], [2, 0], [0, 2] ]

# devuelve si un estado es válido para las reglas del juego
def valido?(estado)
	# logico, que no hayan de más o de menos
	if estado[Izq][M] > 3 || estado[Izq][M] < 0 ||
		estado[Der][M] > 3 || estado[Der][M] < 0 ||
		estado[Izq][C] > 3 || estado[Izq][C] < 0 ||
		estado[Der][C] > 3 || estado[Der][C] < 0
		return false
	# que no haya mas C que M a la Izq
	elsif estado[Izq][M] != 0 &&
		estado[Izq][M] < estado[Izq][C]
		return false
	# que no haya mas C que M a la Der
	elsif estado[Der][M] != 0 &&
		estado[Der][M] < estado[Der][C]
		return false
	end

	# es OK
	true
end

# cambia el estado según indica el viaje
def canoa(estado, viaje)
	estado[estado[Canoa]][M] -= viaje[M]
	estado[estado[Canoa]][C] -= viaje[C]

	if estado[Canoa] == Izq
		estado[Canoa] = Der
	else
		estado[Canoa] = Izq
	end

	estado[estado[Canoa]][M] += viaje[M]
	estado[estado[Canoa]][C] += viaje[C]
end

# una forma cómoda de mostrar el estado
def muestraEstado(estado)
	printf("M:%d C:%d", estado[Izq][M], estado[Izq][C])

	printf(" \\___/") if estado[Canoa] == Izq

	printf(" ~~~~~~~~ ")

	printf("\\___/ ") if estado[Canoa] == Der

	printf("M:%d C:%d", estado[Der][M], estado[Der][C])
	
	printf("\n")
end

# para poder volver atrás
viajes = Array.new
anterior = Array.new
# empezamos por el principio
estado = Estado_ini
# no duplicamos los vectores internos porque nos los mnodificamos
posibles = Viajes.dup

# hasta que no acabemos, pega vueltas
while estado != Estado_fin
	if posibles == nil
		STDOUT.puts "NO he encontrado solucion!"
		exit
	end

	# probamos viajes hasta que se nos acaben o uno sea válido
	while ! posibles.empty?
		viaje = posibles.shift

		# duplicamos los vectores internos
		posibleEstado =  [ estado[Izq].dup, estado[Der].dup,
			estado[Canoa] ]
		canoa(posibleEstado, viaje)

		# salimos si es válido y no se ha repetido
		break if valido?(posibleEstado) &&
			anterior.index(posibleEstado) == nil

		# no es válido
		posibleEstado = nil
	end

	# tenemos uno válido?
	if posibleEstado != nil
		# nos acordamos del estado actual
		anterior.push estado
		viajes.push posibles

		# aceptamos el viaje como bueno (por ahora)
		estado = posibleEstado
		posibles = Viajes.dup
	else
		# el viaje no nos ha llevado a un resultado
		# asi que recuperamos el estado anterior
		estado = anterior.pop
		posibles = viajes.pop

		posibleEstado = nil
	end
end

puts "Tenemos una solución!"

# muestra la solucion
while (resultado = anterior.shift) != nil
	muestraEstado(resultado)
end

muestraEstado(estado)

# EOF

He puesto muchos comentarios, pero el mecanismo es bastante sencillo.

Realizamos un bucle que da vueltas mientras el estado no sea el que deseamos. En una primera fase vamos probando viajes hasta que uno sea válido y además no nos lleve a un estado anterior (para evitar bucles, que sabemos que el resultado no los tiene). Si encontramos un estado válido, almacenamos el estado actual y tomamos en nuevo como bueno. En caso de que no se haya encontrado un estado, pues empleamos el backtracking y volvemos al estado anterior para probar otra posibilidad.

El resultado que da con ese vector de Viajes es:

$ ./enigma.rb
Tenemos una solución!
M:3 C:3 \___/ ~~~~~~~~ M:0 C:0
M:2 C:2 ~~~~~~~~ \___/ M:1 C:1
M:3 C:2 \___/ ~~~~~~~~ M:0 C:1
M:3 C:0 ~~~~~~~~ \___/ M:0 C:3
M:3 C:1 \___/ ~~~~~~~~ M:0 C:2
M:1 C:1 ~~~~~~~~ \___/ M:2 C:2
M:2 C:2 \___/ ~~~~~~~~ M:1 C:1
M:0 C:2 ~~~~~~~~ \___/ M:3 C:1
M:0 C:3 \___/ ~~~~~~~~ M:3 C:0
M:0 C:1 ~~~~~~~~ \___/ M:3 C:2
M:1 C:1 \___/ ~~~~~~~~ M:2 C:2
M:0 C:0 ~~~~~~~~ \___/ M:3 C:3

Si quisiéramos todas las soluciones, tampoco sería mucho más complicado, aunque no sé si hay una que se pueda calificar de mejor. ¿Alguien se anima? :). Los comentarios no es el lugar más indicado, así que me podéis mandar un correo con vuestra propuesta.

Así que Prolog no era tan mágico después de todo, aunque si buscamos soluciones en lenguajes más orientados a este tipo de problema, veremos como como sí hay algo de magia ;).

Actualización: aquí dejo algunas soluciones en otros lenguajes, también en este blog:

Anotación por Juan J. Martínez, clasificada en: programming, ruby.

Hay 4 comentarios

Gravatar

En clase yo he tenido que estudiar varios de estos problemas e incluso he tenido que programar alguno: El de la cabra la col y el lobo (es casi igual que este) y el de los filósofos cenando, aunque este más que lógico es un problema de sincronización, pero no conocía el problema del que tu hablas.

Había leído tus anteriores anotaciones sobre Ruby pero muy por encima y lo cierto es que, viendo el código, tiene buena pinta.

por THroLL, en 2006-01-04 14:00:43

Gravatar

El código es claro y totalmente legible por alguien que no conozca la sintaxis específica de Ruby. En cuanto al problema, es típico. No, si al final voy a tener que probar Ruby para algo más que SDL :D.

/me se levanta y aplaude.

por r0sk, en 2006-01-04 14:04:16

Gravatar

Pues hago un trackback manual ya que en los blogs de Gentoo tenemos todo capado :/

Solucioné el problema usando Haskell, y ha sido bastante divertido. La verdad es que en menos de un par de horas estaba todo hecho:

solución en haskell

Saludos.Ferdy

por ferdy, en 2006-01-10 18:47:28

Gravatar

En vez de definir las variables como números al principio del código ¿por qué no utilizas símbolos? Tendrías que utilizar hashes en vez de arrays pero por lo demás quedaría más o menos igual.

Por supuesto, como esto funciona mejor no lo toques.

por YoNoSoyTu, en 2006-01-10 21:29:19

Los comentarios están cerrados: los comentarios se cierran automáticamente una vez pasados 30 días. Si quieres comentar algo acerca de la anotación, puedes hacerlo por e-mail.

Algunas anotaciones relacionadas: