Clase 13 Algoritmos Voraces

Descargar como ppt, pdf o txt
Descargar como ppt, pdf o txt
Está en la página 1de 20

ALGORITMOS VORACES

Greedy Algorithms
INTRODUCCIÓN
El método que produce algoritmos ávidos es un
método muy sencillo y que puede ser aplicado a
numerosos problemas, especialmente los de
optimización.
Dado un problema con n entradas el método consiste
en obtener un subconjunto de éstas que satisfaga
una determinada restricción definida para el
problema. Cada uno de los subconjuntos que
cumplan las restricciones diremos que son
soluciones prometedoras. Una solución
prometedora que maximice o minimice una
función objetivo la denominaremos solución
óptima.
Como ayuda para identificar si un problema es susceptible de ser resuelto
por un algoritmo ávido vamos a definir una serie de elementos que han
de estar presentes en el problema:

• Un conjunto de candidatos, que corresponden a las n entradas del


problema.

• Una función de selección que en cada momento determine el candidato


idóneo para formar la solución de entre los que aún no han sido
seleccionados ni rechazados.

• Una función que compruebe si un cierto subconjunto de candidatos es


prometedor. Entendemos por prometedor que sea posible seguir
añadiendo candidatos y encontrar una solución.

• Una función objetivo que determine el valor de la solución hallada. Es la


función que queremos maximizar o minimizar.

• Una función que compruebe si un subconjunto de estas entradas es


solución al problema, sea óptima o no.
Con estos elementos, podemos resumir el funcionamiento de los algoritmos
ávidos en los siguientes puntos:

1. Para resolver el problema, un algoritmo ávido tratará de encontrar un


subconjunto de candidatos tales que, cumpliendo las restricciones del problema,
constituya la solución óptima.

2. Para ello trabajará por etapas, tomando en cada una de ellas la decisión que le
parece la mejor, sin considerar las consecuencias futuras, y por tanto escogerá
de entre todos los candidatos el que produce un óptimo local para esa etapa,
suponiendo que será a su vez óptimo global para el problema.

3. Antes de añadir un candidato a la solución que está construyendo comprobará si


es prometedora al añadirlo. En caso afirmativo lo incluirá en ella y en caso
contrario descartará este candidato para siempre y no volverá a considerarlo.

4. Cada vez que se incluye un candidato comprobará si el conjunto obtenido es


solución.

Resumiendo, los algoritmos ávidos construyen la solución en etapas sucesivas,


tratando siempre de tomar la decisión óptima para cada etapa. A la vista de todo
esto, no resulta difícil plantear un esquema general para este tipo de algoritmos:
AlgoritmoAvido(entrada:CONJUNTO):CONJUNTO
ELEMENTO x
CONJUNTO solucion
BOOLEAN encontrada
BEGIN
encontrada:=FALSE
crear(solucion)
WHILE NOT EsVacio(entrada) AND (NOT encontrada) DO
x:=SeleccionarCandidato(entrada)
IF EsPrometedor(x,solucion) THEN
Incluir(x,solucion)
IF EsSolucion(solucion) THEN
encontrada:=TRUE
END
END
END
RETURN solucion
END AlgoritmoAvido
De este esquema se desprende que los algoritmos ávidos son muy fáciles de
implementar y producen soluciones muy eficientes. Entonces ¿por qué no utilizarlos
siempre?

En primer lugar, porque no todos los problemas admiten esta estrategia de solución.
De hecho, la búsqueda de óptimos locales no tiene por qué conducir siempre a
un óptimo global, como veremos en varios ejemplos. La estrategia de los
algoritmos ávidos consiste en tratar de ganar todas las batallas sin pensar que para
ganar la guerra muchas veces es necesario perder alguna batalla.

Desgraciadamente, y como en la vida misma, pocos hechos hay para los que
podamos afirmar sin miedo a equivocarnos que lo que parece bueno para hoy
siempre es bueno para el futuro. Y aquí radica la dificultad de estos algoritmos.

Encontrar la función de selección que nos garantice que el candidato escogido o


rechazado en un momento determinado es el que ha de formar parte o no de la
solución óptima sin posibilidad de reconsiderar dicha decisión. Por ello, una parte
muy importante de este tipo de algoritmos es la demostración formal de que la
función de selección escogida consigue encontrar óptimos globales para cualquier
entrada del algoritmo. No basta con diseñar un procedimiento ávido, que con
seguridad, será rápido y eficiente (en tiempo y en recursos), sino que hay que
demostrar que siempre consigue encontrar la solución óptima del problema.
Debido a su eficiencia, este tipo de algoritmos es muchas veces utilizado aun en los
casos donde se sabe que no necesariamente encuentran la solución óptima. En
algunas ocasiones debemos encontrar pronto una solución razonablemente buena,
aunque no sea la óptima, puesto que conseguir la óptima demoraría muchísimo, y
ya no sería de utilidad (piénsese en el localizador de un avión de combate, o en los
procesos de toma de decisiones de una central nuclear).

También hay otras circunstancias, en donde lo que interesa es conseguir cuanto


antes una solución del problema y, a partir de la información suministrada por ella,
conseguir la óptima más rápidamente. Es decir, la eficiencia de este tipo de
algoritmos hace que se utilicen aunque no consigan resolver el problema de
optimización planteado, sino que sólo den una solución “aproximada”.

El nombre de algoritmos ávidos, o voraces (su nombre original proviene del término
inglés greedy) se debe a su comportamiento: en cada etapa “toman lo que
pueden” sin analizar consecuencias, es decir, son glotones por naturaleza.

En lo que sigue veremos algunos problemas que muestran cómo diseñar algoritmos
ávidos y cuál es su comportamiento. En este tipo de algoritmos el proceso no
acaba cuando disponemos de la implementación del procedimiento que lo
lleva a cabo. Lo importante es la demostración de que el algoritmo encuentra
la solución óptima en todos los casos, o bien la presentación de un
contraejemplo que muestra los casos en donde falla.
EL PROBLEMA DEL CAMBIO
Suponiendo que el sistema monetario de un país está formado por monedas de
valores v1,v2,...,vn, el problema del cambio de dinero consiste en descomponer
cualquier cantidad dada M en monedas de ese país utilizando el menor número
posible de monedas.

En primer lugar, es fácil implementar un algoritmo ávido para resolver este


problema, que es el que sigue el proceso que usualmente utilizamos en nuestra vida
diaria. Sin embargo, tal algoritmo va a depender del sistema monetario utilizado y
por ello plantearemos dos situaciones para las cuales deseamos conocer si el
algoritmo ávido encuentra siempre la solución óptima:

a) Suponiendo que cada moneda del sistema monetario del país vale al menos el
doble que la moneda de valor inferior, que existe una moneda de valor unitario,
y que disponemos de un número ilimitado de monedas de cada valor.

b) Suponiendo que el sistema monetario está compuesto por monedas de valores 1,


p, p2, p3,..., pn, donde p > 1 y n > 0, y que también disponemos de un número
ilimitado de monedas de cada valor.
Solución
TYPE MONEDAS =(M500,M200,M100,M50,M25,M5,M1) (*sistema monetario*)
MONEDAS VALORES [ ] (* valores de monedas *)
MONEDAS SOLUCION [ ]

Algoritmo Cambio(n:INT, valor:VALORES, cambio:SOLUCION)


(* n es la cantidad a descomponer, y el vector "valor" contiene los valores de cada una de las
monedas del sistema monetario *)
MONEDAS moneda
BEGIN
FOR (monedaFIRST(MONEDAS) TO LAST(MONEDAS) DO
cambio[moneda]:=0
END
FOR (monedaFIRST(MONEDAS) TO LAST(MONEDAS) DO
WHILE valor[moneda]<=n DO
INC(cambio[moneda])
DEC(n,valor[moneda])
END
END
END Cambio
Este algoritmo es de complejidad lineal respecto al número de monedas del
país, y por tanto muy eficiente.
Respecto a las dos cuestiones planteadas, comenzaremos por la primera.
Supongamos que nuestro sistema monetario esta compuesto por las
siguientes monedas:

TYPE MONEDAS = (M11,M5,M1); valor:={11,5,1};

Tal sistema verifica las condiciones del enunciado pues disponemos de


moneda de valor unitario, y cada una de ellas vale más del doble de la
moneda inmediatamente inferior.
Consideremos la cantidad n = 15. El algoritmo ávido del cambio de
monedas descompone tal cantidad en:
15 = 11 + 1 + 1 + 1 + 1,

es decir, mediante el uso de cinco monedas. Sin embargo, existe una


descomposición que utiliza menos monedas (exactamente tres):
15 = 5 + 5 + 5.
Aunque queda comprobado que bajo estas circunstancias el diseño ávido
no puede utilizarse, las razones por las que el algoritmo falla quedarán al
descubierto más adelante.
b) En cuanto a la segunda situación, y para demostrar que el algoritmo
ávido encuentra la solución óptima, vamos a apoyarnos en una
propiedad general de los números naturales:
Si p es un número natural mayor que 1, todo número natural x puede
expresarse de forma única como:

Consulte la demostración.
EL PROBLEMA DE LOS RECORRIDOS DEL
CABALLO DE AJEDREZ

Dado un tablero de ajedrez y una casilla inicial, queremos decidir si


es posible que un caballo recorra todos y cada uno de las
casillas sin duplicar ninguna. No es necesario en este problema
que el caballo vuelva a la casilla de partida. Un posible algoritmo
ávido decide, en cada iteración, colocar el caballo en la casilla
desde la cual domina el menor número posible de casillas aún no
visitadas.

a) Implementar dicho algoritmo a partir de un tamaño de tablero nxn


y una casilla inicial (x0,y0).

b) Buscar, utilizando el algoritmo realizado en el apartado anterior,


todas las casillas iniciales para los que el algoritmo encuentra
solución.

c) Basándose en los resultados del apartado anterior, encontrar el


patrón general de las soluciones del recorrido del caballo.
Solución

a)Para implementar el algoritmo pedido comenzaremos


definiendo las constantes y tipos que utilizaremos:

CONST TAMMAX = ... (* dimension maxima del tablero *)


TYPE tablero = ARRAY[1..TAMMAX],[1..TAMMAX] OF INT

Cada una de las casillas del tablero va a almacenar un número


natural que indica el número de orden del movimiento del
caballo en el que visita la casilla. Podrá tomar también el valor
cero, indicando que la casilla no ha sido visitada aún.
Inicialmente todas las casillas tomarán este valor.
Una posible implementación del algoritmo viene dada por la
función Caballo que se muestra a continuación, la cual, dado un
tablero t, su dimensión n y una posición inicial (x,y), decide si el
caballo recorre todo el tablero o no.
Algoritmo Caballo( VAR t:tablero; n:INT; x,y:INT):BOOLEAN

BEGIN
InicTablero(t,n) (* inicializa las casillas del tablero a 0 *)
FOR i  1 TO n*n DO
t[x,y] i
IF NOT NuevoMov(t,n,x,y) AND (i<n*n-1) THEN
RETURN FALSE
END
END
RETURN TRUE (* hemos recorrido las n*n casillas *)
END Caballo
algoritmo NuevoMov(VAR t:tablero; n:INT; VAR x,y:INT):BOOLEAN
(*Esta función es la que va a ir calculando la nueva casilla a la que salta el caballo
siguiendo la indicación del enunciado, devolviendo FALSE si no puede Moverse*)

INT accesibles,minaccesibles,i,solx,soly,nuevax,nuevay
BEGIN
minaccesibles  9
solx  x
soly  y
FOR i:=1 TO 8 DO
IF Salto(t,n,i,x,y,nuevax,nuevay) THEN
accesibles:=Cuenta(t,n,nuevax,nuevay)
IF (accesibles>0) AND (accesibles<minaccesibles) THEN
minaccesibles  accesibles
solx  nuevax soly  nuevay
END
END
END
X  solx
y  soly
RETURN (minaccesibles<9)
END NuevoMov
Algoritmo Salto(VAR t:tablero; n,i,x,y: INT;VAR nx,ny:INT ) :BOOL
(* i indica el numero del movimiento, (x,y) es la casilla actual, y (nx,ny) es la casilla a
donde salta. *)
BEGIN
CASE i OF
1: nx  x-2 ny  y+1
2: nx  x-1 ny  y+2
3: nx  x+1 ny  y+2
4: nx  x+2 ny  y+1
5: nx  x+2 ny  y-1
6: nx  x+1 ny  y-2
7: nx  x-1 ny  y-2
8: nx  x-2 ny  y-1
END
RETURN((1<=nx) AND (nx<=n) AND (1<=ny) AND (ny<=n) AND (t[nx,ny]=0))
END Salto
La función Salto calcula las coordenadas de la casilla a
donde salta el caballo (tiene 8 posibilidades), y devuelve si
es posible o no realizar ese movimiento ya que la casilla
puede estar ocupada o bien salirse del tablero.

Dicha función intenta los movimientos en el orden que


muestra la siguiente figura:
Cuenta(VAR t:tablero; n,x,y:INT):INT
INT acc,i,nx,ny
BEGIN
acc  0
FOR i1 TO 8 DO
IF Salto(t,n,i,x,y,nx,ny) THEN
INC(acc)
END
END
RETURN acc
END Cuenta

La otra función es Cuenta, que devuelve el número de casillas


a las que el caballo puede saltar desde una posición dada.
b) Para resolver este punto necesitamos un programa que nos permita ir recorriendo
todas las posibilidades e imprimir aquellas casillas iniciales desde donde se consigue
solución:

Algoritmo Caballos()
tablero t
INT n,i,j
BEGIN
FOR n  4 TO TAMMAX DO
print (“Dimension = “, n) println;
FOR i1 TO n DO
FOR j  1 TO n DO
IF Caballo(t,n,i,j) THEN
print(“ Desde: “)
print(i,j)
print(“ tiene solucion.”)
END
END
END
WrLn()
END
END Caballos.
La salida del programa anterior nos permite inferir un patrón general para las
soluciones del problema:
• Para n = 4, el problema no tiene solución.

• Para n > 4, n par, el problema tiene solución para cualquier casilla inicial.

• Para n > 4, n impar, el problema tiene solución para aquellas casillas iniciales
(x0,y0) que verifiquen que x0 + y0 sea par, es decir, si el caballo comienza su
recorrido en una casilla blanca.

Pero observemos que el algoritmo implementado no ha encontrado solución en


todas estas situaciones. Por ejemplo, para n = 5, x0 = 5 e y0 = 3 el algoritmo dice
que no la hay. Sin embargo, sí la encuentra para n = 5, x0 = 1 e y0 = 3,
para n = 5, x0 = 3 e y0 = 1 y para n = 5, x0 = 3 e y0 = 5, que son casos
simétricos.
De existir solución para alguno de ellos, por simetría se obtiene para los otros.
¿Por qué no la encuentra nuestro algoritmo?
La respuesta se encuentra en cómo buscamos la siguiente casilla a donde saltar.
Por la forma en la que funciona el algoritmo, siempre probamos las ocho casillas
en el sentido de las agujas del reloj, siguiendo la pauta mostrada en la función
Salto. Esto hace que nuestro algoritmo no sea simétrico. En resumen, estamos
ante un algoritmo ávido que no funciona para todos los casos.

También podría gustarte