ALGORITMOS VORACES INTRODUCCIÓN Imagínate esta situación: tienes que llevar a cabo cierta tarea, y hay muchas formas de conseguirlo. En principio todas esas formas de completar la tarea son válidas, son planes factibles. Pero además de completar la tarea asignada, que es el objetivo irrenunciable, tienes otro objetivo secundario: hacerlo lo mas rápido posible, o lo mas barato posible, o gastar la menor cantidad posible de un cierto recurso, etc. Entonces, cuando tienes en cuenta este segundo objetivo, no todos los planes factibles son igual de buenos, porque unos son mas costosos que otros (en tiempo, en dinero, en el recurso que queremos ahorrar, etc.) Surge entonces la necesidad de escoger, entre los planes factibles, el que mejor satisface nuestro objetivo. Se trata entonces de un problema de optimización: escoger la mejor solución entre las posibles. Vamos a empezar presentando algunos algoritmos voraces sencillos, pero a la vez útiles e interesantes. Algunos pueden incluso ser ya conocidos de cursos anteriores en matemática discreta, etc. El objetivo es ir señalando algunas características comunes a todos ellos. CARACTERÍSTICAS Una aproximación voraz consiste en que cada elemento a considerar se evalúa una única vez, siendo descartado o seleccionado, de tal forma que si es seleccionado forma parte de la solución, y si es descartado, no forma parte de la solución ni volverá a ser considerado para la misma. Una forma de ver los algoritmos voraces es considerar la estrategia de Vuelta atrás, en la cual se vuelve recursivamente a decisiones anteriormente tomadas para variar la elección entonces tomada, pero eliminando esa recursión y eligiendo la mejor opción. El término voraz se deriva de la forma en que los datos de entrada se van tratando, realizando la elección de desechar o seleccionar un determinado elemento una sola vez. Al contrario que con otros métodos algorítmicos, no siempre es posible dar una solución a un problema empleando un algoritmo voraz. No todos los problemas son resolubles con algoritmos voraces. Los algoritmos voraces tienden a ser bastante eficientes y pueden implementarse de forma relativamente sencilla. Su eficiencia se deriva de la forma en que trata los datos, llegando a alcanzar muchas veces una complejidad de orden lineal. Sin embargo, la mayoría de los intentos de crear un algoritmo voraz correcto fallan a menos que exista previamente una prueba precisa que demuestre la correctitud del algoritmo. Cuando una estrategia voraz falla al producir resultados óptimos en todas las entradas, en lugar de algoritmo suele denominarse heurística. Las heurísticas resultan útiles cuando la velocidad es más importante que los resultados exactos (por ejemplo, cuando resultados "bastante buenos" son suficientes). ALGORITMOS IMPORTANTES Algoritmo de Dijkstra El algoritmo de Dijkstra, también llamado algoritmo de caminos mínimos, es un algoritmo para la determinación del camino más corto dado un vértice origen al resto de vértices en un grafo dirigido y con pesos en cada arista. Su nombre se refiere a Edsger Dijkstra, quien lo describió por primera vez en 1959. La idea subyacente en este algoritmo consiste en ir explorando todos los caminos más cortos que parten del vértice origen y que llevan a todos los demás vértices; cuando se obtiene el camino más corto desde el vértice origen, al resto de vértices que componen el grafo, el algoritmo se detiene. El algoritmo es una especialización de la búsqueda de costo uniforme, y como tal, no funciona en grafos con aristas de costo negativo (al elegir siempre el nodo con distancia menor, pueden quedar excluidos de la búsqueda nodos que en próximas iteraciones bajarían el costo general del camino al pasar por una arista con costo negativo). Algoritmo Sea G = (V,A) un grafo dirigido y etiquetado. Sean los vértices a ∈ V y z ∈ V; a es el vértice de origen y z el vértice de destino. Sea un conjunto C ⊂ V, que contiene los vértices de V cuyo camino más corto desde a todavía no se conoce. Sea un vector D, con tantas dimensiones como elementos tiene V, y que “guarda” las distancias entre a y cada uno de los vértices de V. Sea, finalmente, otro vector, P, con las mismas dimensiones que D, y que conserva la información sobre qué vértice precede a cada uno de los vértices en el camino. El algoritmo para determinar el camino de longitud mínima entre los vértices a y z es: C←V Para todo vértice i ∈ C, i ≠ a, se establece Di ← ∞; Da ← 0 Para todo vértice i ∈ C se establece Pi = a Se obtiene el vértice s ∈ C tal que no existe otro vértice w ∈ C tal que Dw < Ds o Si s = z entonces se ha terminado el algoritmo. 5. Se elimina de C el vértice s: C ← C−{s} 6. Para cada arista e ∈ A de longitud l, que une el vértice s con algún otro vértice t ∈ C, o Si l+Ds < Dt, entonces: 1. Se establece Dt ← l+Ds 2. Se establece Pt ← s 7. Se regresa al paso 4 1. 2. 3. 4. Al terminar este algoritmo, en Dz estará guardada la distancia mínima entre a y z. Por otro lado, mediante el vector P se puede obtener el camino mínimo: en Pz estará y, el vértice que precede a z en el camino mínimo; en Py estará el que precede a y, y así sucesivamente, hasta llegar a a. Complejidad Orden de complejidad del algoritmo: O(|V|2+|E|) = O(|V|2). Implementación C++ #include <math.h> #include <string.h> #include <iostream> using namespace std; int destino, origem, vertices = 0; int custo, *custos = NULL; void dijkstra(int vertices, int origen, int destino, int *costos) { int i, v, cont = 0; int *ant, *tmp; int *z; /* vertices para los cuales se conoce el camino minimo */ double min; double *dist = new double[vertices]; /* vector con los costos de dos caminos */ /* aloca las lineas de la matriz */ ant = new int[vertices]; tmp = new int[vertices]; if (ant == NULL) { cout << "** Error: Memoria Insuficiente **"; exit(-1); } z = new int[vertices]; if (z == NULL) { cout <<"** Error: Memoria Insuficiente **"; exit(-1); } for (i = 0; i < vertices; i++) { if (costos[(origen - 1) * vertices + i] !=- 1) { ant[i] = origen - 1; dist[i] = costos[(origen-1)*vertices+i]; } else { ant[i]= -1; dist[i] = HUGE_VAL; } z[i]=0; } z[origen-1] = 1; dist[origen-1] = 0; /* Bucle principal */ do { /* Encontrando el vertice que debe entrar en z */ min = HUGE_VAL; for (i=0;i<vertices;i++) if (!z[i]) if (dist[i]>=0 && dist[i]<min) { min=dist[i];v=i; } /* Calculando las distancias de los nodos vecinos de z */ if (min != HUGE_VAL && v != destino - 1) { z[v] = 1; for (i = 0; i < vertices; i++) if (!z[i]) { if (costos[v*vertices+i] != -1 && dist[v] + costos[v*vertices+i] < dist[i]) { dist[i] = dist[v] + costos[v*vertices+i]; ant[i] =v; } } } } while (v != destino - 1 && min != HUGE_VAL); /* Muestra el resultado de la búsqueda */ cout << "\tDe " << origen << " para "<<destino<<" \t"; if (min == HUGE_VAL) { cout <<"No Existe\n"; cout <<"\tCoste: \t- \n"; } else { i = destino; i = ant[i-1]; while (i != -1) { // printf("<-%d",i+1); tmp[cont] = i+1; cont++; i = ant[i]; } for (i = cont; i > 0; i--) { cout<< tmp[i-1]<<" -> "; } cout << destino; cout <<"\n\tCoste: " << dist[destino-1] <<"\n"; } delete delete delete delete (dist); (ant); (tmp); (z); } void menu(void) { cout <<"Implementacion del Algoritmo de Dijasktra\n"; cout <<"Comandos:\n"; cout <<"\t d - Aniadir un Grafo\n \t r - Determinar el menor camino de los grafos\n \t x - Sair del programa\n"; cout <<endl; cout << "$"; } void add(void) { do { cout <<"\nInforme o numero de vertices (no minimo 2 ): "; cin>>vertices; } while (vertices < 2 ); if (!custos) delete(custos); custos = new int[vertices * vertices]; for (int i = 0; i <= vertices * vertices; i++) custos[i] = -1; cout <<"Ya tenemos el número de vertices. Nº Vertices = "<< vertices<<endl; cout <<"Ahora unamos los vértices:\n" ; bool sigo=true; int origen; int destino; while (sigo){ cout << "Escoga el primer vértice de la arista: " <<endl; do{ cin >> origen; if (origen>vertices){ cout << "Me temo que se ha equivocado al hacer la selección. El número de vertice ha de ser menor de " << vertices<<endl; } }while(origen > vertices); cout << "Escoga el segundo vértice de la arista: " <<endl; do{ cin >> destino; if (destino>vertices){ cout << "Me temo que se ha equivocado al hacer la selección. El número de vertice ha de ser menor de " << vertices<<endl; } }while(destino> vertices); int peso=0; cout <<"Ahora queda el peso" <<endl; cin>>peso; custos[(origen-1) * vertices + destino - 1] = peso; int seguir=1; cout << "Quiere añadir otra arista (0 - NO, 1 - SÍ, por defecto 1): " ; cin >>seguir; sigo = (seguir==1); } } void buscar(void) { int i, j; cout <<"Lista dos Menores Caminos en Grafo Dado: \n"; for (i = 1; i <= vertices; i++) { for (j = 1; j <= vertices; j++) dijkstra(vertices, i,j, custos); cout<<endl; } cout <<"<Presione ENTER para volver al menu principal. \n"; } int main(int argc, char **argv) { string opcion; do { menu(); cin >> opcion; if (opcion=="d"){ add(); }else if (opcion=="r" && (vertices > 0) ) { buscar(); } } while (opcion!= "x"); cout<<"\nHasta la proxima...\n\n"; delete (custos); exit(0); } Algoritmo de Prim El algoritmo de Prim es un algoritmo de la teoría de los grafos para encontrar un árbol recubridor mínimo en un grafo conexo, no dirigido y cuyas aristas están etiquetadas. En otras palabras, el algoritmo encuentra un subconjunto de aristas que forman un árbol con todos los vértices, donde el peso total de todas las aristas en el árbol es el mínimo posible. Si el grafo no es conexo, entonces el algoritmo encontrará el árbol recubridor mínimo para uno de los componentes conexos que forman dicho grafo no conexo. El algoritmo fue diseñado en 1930 por el matemático Vojtech Jarnik y luego de manera independiente por el científico computacional Robert C. Prim en 1957 y redescubierto por Dijkstra en 1959. Por esta razón, el algoritmo es también conocido como algoritmo DJP o algoritmo de Jarnik. Código en JAVA public class Algorithms { public static Graph PrimsAlgorithm (Graph g, int s) { int n = g.getNumberOfVertices (); Entry[] table = new Entry [n]; for (int v = 0; v < n; ++v) table [v] = new Entry (); table [s].distance = 0; PriorityQueue queue = new BinaryHeap (g.getNumberOfEdges()); queue.enqueue ( new Association (new Int (0), g.getVertex (s))); while (!queue.isEmpty ()) { Association assoc = (Association) queue.dequeueMin(); Vertex v0 = (Vertex) assoc.getValue (); int n0 = v0.getNumber (); if (!table [n0].known) { table [n0].known = true; Enumeration p = v0.getEmanatingEdges (); while (p.hasMoreElements ()) { Edge edge = (Edge) p.nextElement (); Vertex v1 = edge.getMate (v0); int n1 = v1.getNumber (); Int wt = (Int) edge.getWeight (); int d = wt.intValue (); if (!table[n1].known && table[n1].distance>d) { table [n1].distance = d; table [n1].predecessor = n0; queue.enqueue ( new Association (new Int (d), v1)); } } } } Graph result = new GraphAsLists (n); for (int v = 0; v < n; ++v) result.addVertex (v); for (int v = 0; v < n; ++v) if (v != s) result.addEdge (v, table [v].predecessor); return result; } } Demostración Sea G un grafo conexo y ponderado. En toda iteración del algoritmo de Prim, se debe encontrar una arista que conecte un nodo del subgrafo a otro nodo fuera del subgrafo. Ya que G es conexo, siempre habrá un camino para todo nodo. La salida Y del algoritmo de Prim es un árbol porque las aristas y los nodos agregados a Y están conectados. Sea Y el árbol recubridor mínimo de G. Si es el árbol recubridor mínimo. Si no, sea e la primera arista agregada durante la construcción de Y, que no está en Y1 y sea V el conjunto de nodos conectados por las aristas agregadas antes que e. Entonces un extremo de e está en V y el otro no. Ya que Y1 es el árbol recubridor mínimo de G hay un camino en Y1 que une los dos extremos. Mientras que uno se mueve por el camino, se debe encontrar una arista f uniendo un nodo en V a uno que no está en V. En la iteración que e se agrega a Y, f también se podría haber agregado y se hubiese agregado en vez de e si su peso fuera menor que el de e. Ya que f no se agregó se concluye: Sea Y2 el grafo obtenido al remover f y agregando . Es fácil mostrar que Y2 conexo tiene la misma cantidad de aristas que Y1, y el peso total de sus aristas no es mayor que el de Y1, entonces también es un árbol recubridor mínimo de G y contiene a e y todas las aristas agregadas anteriormente durante la construcción de V. Si se repiten los pasos mencionados anteriormente, eventualmente se obtendrá el árbol recubridor mínimo de G que es igual a Y. Esto demuestra que Y es el árbol recubridor mínimo de G. Ejemplo de ejecución del algoritmo Imagen Descripción Este es el grafo ponderado de partida. No es un árbol ya que requiere que no haya circuitos y en este grafo los hay. Los números cerca de las aristas indican el peso. Ninguna de las aristas está marcada, y el vértice D ha sido elegido arbitrariamente como el punto de partida. No En el visto grafo C, G A, B, E, F En el árbol D El segundo vértice es el más cercano a D: A está a 5 de distancia, B a 9, E a 15 y F a C, G 6. De estos, 5 es el valor más pequeño, así que marcamos la arista DA. B, E, F A, D El próximo vértice a elegir es el más cercano a D o A. B está a 9 de distancia de D y a 7 de A, E está a 15, y F está a 6. 6 es el más chico, así que marcamos el vértice F y a la arista DF. C B, E, G A, D, F El algoritmo continua. El vértice B, que está a una distancia de 7 de A, es el siguiente marcado. En este punto la arista DB es marcada en rojo porque sus dos extremos ya están en el árbol y por lo tanto no podrá ser utilizado. null C, E, G A, D, F, B Aquí hay que elegir entre C, E y G. C está a 8 de distancia de B, E está a 7 de distancia de B, y G está a 11 de distancia de F. E está más cerca, entonces marcamos el vértice E y la arista EB. Otras dos aristas fueron marcadas en rojo porque ambos vértices que unen fueron agregados al árbol. null C, G A, D, F, B, E Sólo quedan disponibles C y G. C está a 5 de distancia de E, y G a 9 de distancia de E. Se elige C, y se marca con el arco EC. El arco BC también se marca con rojo. null G A, D, F, B, E, C G es el único vértice pendiente, y está más cerca de E que de F, así que se agrega EG al árbol. Todos los vértices están ya marcados, el árbol de expansión mínimo se muestra en verde. En este caso con un peso de 39. null null A, D, F, B, E, C, G Algoritmo de Kruskal El algoritmo de Kruskal es un algoritmo de la teoría de grafos para encontrar un árbol recubridor mínimo en un grafo conexo y ponderado. Es decir, busca un subconjunto de aristas que, formando un árbol, incluyen todos los vértices y donde el valor total de todas las aristas del árbol es el minimo. Si el grafo no es conexo, entonces busca un bosque expandido mínimo (un árbol expandido mínimo para cada componente conexa). El algoritmo de Kruskal es un ejemplo de algoritmo voraz. Un ejemplo de árbol expandido mínimo. Cada punto representa un vértice, el cual puede ser un árbol por sí mismo. Se usa el Algoritmo para buscar las distancias más cortas (árbol expandido) que conectan todos los puntos o vértices. Funciona de la siguiente manera: se crea un bosque B (un conjunto de árboles), donde cada vértice del grafo es un árbol separado se crea un conjunto C que contenga a todas las aristas del grafo mientras C es novacío o eliminar una arista de peso mínimo de C o si esa arista conecta dos árboles diferentes se añade al bosque, combinando los dos árboles en un solo árbol o en caso contrario, se desecha la arista Al acabar el algoritmo, el bosque tiene una sola componente, la cual forma un árbol de expansión mínimo del grafo. Este algoritmo fue publicado por primera vez en Proceedings of the American Mathematical Society, pp. 48–50 en 1956, y fue escrito por Joseph Kruskal. Complejidad del algoritmo m el número de aristas del grafo y n el número de vértices, el algoritmo de Kruskal muestra una complejidad O(m log m) o, equivalentemente, O(m log n), cuando se ejecuta sobre estructuras de datos simples. Los tiempos de ejecución son equivalentes porque: m es a lo sumo n2 y log n2 = 2logn es O(log n). ignorando los vértices aislados, los cuales forman su propia componente del árbol de expansión mínimo, n ≤ 2m, así que log n es O(log m). Se puede conseguir esta complejidad de la siguiente manera: primero se ordenan las aristas por su peso usando una ordenación por comparación (comparison sort) con una complejidad del orden de O(m log m); esto permite que el paso "eliminar una arista de peso mínimo de C" se ejecute en tiempo constante. Lo siguiente es usar una estructura de datos sobre conjuntos disjuntos (disjoint-set data structure) para controlar qué vértices están en qué componentes. Es necesario hacer varias operaciones del orden de O(m), dos operaciones de búsqueda y posiblemente una unión por cada arista. Incluso una estructura de datos sobre conjuntos disjuntos simple con uniones por rangos puede ejecutar operaciones del orden de O(m) en O(m log n). Por tanto, la complejidad total es del orden de O(m log m) = O(m log n). Con la condición de que las aristas estén ordenadas o puedan ser ordenadas en un tiempo lineal (por ejemplo, mediante el ordenamiento por cuentas o con el ordenamiento Radix), el algoritmo puede usar estructuras de datos de conjuntos disjuntos más complejas para ejecutarse en tiempos del orden de O(m α(n)), donde α es la inversa (tiene un crecimiento extremadamente lento) de la función de Ackermann. Demostración de la corrección Sea P un grafo conexo y valuado y sea Y el subgrafo de P producido por el algoritmo. Y no puede tener ciclos porque cada vez que se añade una arista, ésta debe conectar vértices de dos árboles diferentes y no vértices dentro de un subárbol. Y no puede ser disconexa ya que la primera arista que une dos componentes de Y debería haber sido añadida por el algoritmo. Por tanto, Y es un árbol expandido de P. Sea Y1 el árbol expandido de peso mínimo de P, el cual tiene el mayor número de aristas en común con Y. Si Y1=Y entonces Y es un árbol de expansión mínimo. Por otro lado, sea e la primera arista considerada por el algoritmo que está en Y y que no está en Y1. Sean C1 y C2 las componentes de P que conecta la arista e. Ya que Y1 es un árbol, Y1+e tiene un ciclo y existe una arista diferente f en ese ciclo que también conecta C1 y C2. Entonces Y2=Y1+e-f es también un árbol expandido. Ya que e fue considerada por el algoritmo antes que f, el peso de e es al menos igual que que el peso de f y ya que Y1 es un árbol expandido mínimo, los pesos de esas dos aristas deben ser de hecho iguales. Por tanto, Y2 es un árbol expandido mínimo con más aristas en común con Y que las que tiene Y1, contradiciendo las hipótesis que se habían establecido antes para Y1. Esto prueba que Y debe ser un árbol expandido de peso mínimo. Otros algoritmos para este problema son el algoritmo de Prim y el algoritmo de Boruvka. Ejemplo Este es el grafo original. Los números de las aristas indican su peso. Ninguna de las aristas está resaltada. AD y CE son las aristas mas cortas, con peso 5, y AD se ha elegido arbitrariamente, por tanto se resalta. Sin embargo, ahora es CE la arista mas pequeña que no forma ciclos, con peso 5, por lo que se resalta como segunda arista. La siguiente arista, DF con peso 6, ha sido resaltada utilizando el mismo método. La siguientes aristas mas pequeñas son AB y BE, ambas con peso 7. AB se elige arbitrariamente, y se resalta. La arista BD se resalta en rojo, porque formaría un ciclo ABD si se hubiera elegido. El proceso continúa marcando las aristas, BE con peso 7. Muchas otras aristas se marcan en rojo en este paso: BC (formaría el ciclo BCE), DE (formaría el ciclo DEBA), y FE (formaría el ciclo FEBAD). Finalmente, el proceso termina con la arista EG de peso 9, y se ha encontrado el árbol expandido mínimo. ¿PARA QUE SIRVE EL TEMA? Los algoritmos voraces suelen ser bastante simples. Se emplean sobre todo para resolver problemas de optimización, como por ejemplo, encontrar la secuencia óptima para procesar un conjunto de tareas por un computador, hallar el camino mínimo de un grafo, etc. Habitualmente, los elementos que intervienen son: un conjunto o lista de candidatos (tareas a procesar, vértices del grafo, etc.); un conjunto de decisiones ya tomadas (candidatos ya escogidos); una función que determina si un conjunto de candidatos es una solución al problema (aunque no tiene por qué ser la óptima); una función que determina si un conjunto es completable, es decir, si añadiendo a este conjunto nuevos candidatos es posible alcanzar una solución al problema, suponiendo que esta exista; una función de selección que escoge el candidato aún no seleccionado que es más prometedor; una función objetivo que da el valor/coste de una solución (tiempo total del proceso, la longitud del camino, etc.) y que es la que se pretende maximizar o minimizar; Para resolver el problema de optimización hay que encontrar un conjunto de candidatos que optimiza la función objetivo. Los algoritmos voraces proceden por pasos. Inicialmente el conjunto de candidatos es vacío. A continuación, en cada paso, se intenta añadir al conjunto el mejor candidato de los aún no escogidos, utilizando la función de selección. Si el conjunto resultante no es completable, se rechaza el candidato y no se le vuelve a considerar en el futuro. En caso contrario, se incorpora al conjunto de candidatos escogidos y permanece siempre en él. Tras cada incorporación se comprueba si el conjunto resultante es una solución del problema. Un algoritmo voraz es correcto si la solución así encontrada es siempre óptima. El esquema genérico del algoritmo voraz es: función voraz(C:conjunto):conjunto { C es el conjunto de todos los candidatos } S <= vacio { S es el conjunto en el que se construye la solución} mientras solucion(S) y C <> vacio hacer x <= el elemento de C que maximiza seleccionar(x) C <= C \ {x} si completable(S U {x}) entonces S <= S U {x} si solución(S) entonces devolver S si no devolver no hay solución El nombre voraz proviene de que, en cada paso, el algoritmo escoge el mejor "pedazo" que es capaz de "comer" sin preocuparse del futuro. Nunca deshace una decisión ya tomada: una vez incorporado un candidato a la solución permanece ahí hasta el final; y cada vez que un candidato es rechazado, lo es para siempre. EJEMPLOS Problema de la mochila Enunciado: "Se tiene una mochila que es capaz de soportar un peso máximo P, así como un conjunto de objetos, cada uno de ellos con un peso y un beneficio. La solución pasa por conseguir introducir el máximo beneficio en la mochila, eligiendo los objetos adecuados. Cada objeto puede tomarse completo o fraccionado". Solución: La forma más simple de saber qué objetos se deben tomar es ordenar dichos objetos por la relación beneficio / peso de mayor a menor. De esta forma, tomaremos los objetos con mayor beneficio en este orden hasta que la bolsa se llene, fraccionando si fuera preciso, el último objeto a tomar. Algoritmo: - V almacena los beneficios de cada objeto, - P almacena el peso de cada objeto, - sol devuelve el tanto por 1 de objeto que se toma, - benef devuelve el beneficio total, - valor_obten almacena el beneficio parcial, - peso_ac almacena el peso parcial. fun mochila (V [1..n], P[1..n] de nat; m: nat) dev <sol[1..n] de nat, benef: nat> begin para j := 1 hasta n hacer sol [j] := 0; fpara valor_obten := 0; i:=0; peso_ac := 0; mientras peso_ac < m hacer si peso [i] + peso_ac < m entonces valor_obten := valor_obten + valor [i]; sol [i] := 1; peso_ac := peso_ac + peso [i]; si no sol [i] := (m – peso_ac) / peso [i]; peso_ac := m; valor_obten := valor [i] * sol [i]; fsi i := i + 1; fmientras benef := valor_obten; ffun Problema del cambio de moneda Enunciado: "Se pide crear un algoritmo que permita a una máquina expendedora devolver el cambio mediante el menor número de monedas posible, considerando que el número de monedas es limitado, es decir, se tiene un número concreto de monedas de cada tipo". Solución: La estrategia a seguir consiste en escoger sucesivamente las monedas de valor mayor que no superen la cantidad de cambio a devolver. El buen funcionamiento del algoritmo depende de los tipos de monedas presentes en la entrada. Así, por ejemplo, si no hay monedas de valor menor que diez, no se podrá devolver un cambio menor que diez. Además, la limitación del número de monedas también influye en la optimalidad del algoritmo, el cual devuelve buenas soluciones bajo determinados conjuntos de datos, pero no siempre. Considérense los dos siguientes ejemplos como demostración de lo dicho: Monedas 50 25 5 1 Monedas 6 4 1 Cantidad 3 4 1 6 Cantidad 3 4 1 Si hay que devolver la cantidad Si queremos devolver la cantidad 110 siguiendo el método del 8, siguiendo el procedimiento algoritmo voraz, se tomaría anterior, el algoritmo tomaría primero una moneda de 50, primero una moneda de 6, quedando una cantidad restante quedando un resto de 2. Tomaría de 60. Como 50 es aún menor 2 monedas de valor 1, habiendo que 60, se tomaría otra moneda devuelto por tanto 3 monedas, de 50. Ahora la cantidad restante cuando es fácil ver que con 2 es 10, por tanto ya tenemos que monedas de valor 4 se obtiene el devolver una moneda de 5, ya resultado pedido. que 50 y 25 son mayores que 10, y por tanto se desechan. La cantidad a devolver ahora es 5. Se tomaría otra moneda de 5, pero puesto que ya no nos queda ninguna, deberán devolverse 5 de valor 1, terminando así el problema de forma correcta. Algoritmo: fun cambio (monedas_valor[1..n] de nat, monedas[1..n] de nat, importe: nat) dev cambio[1..n] de nat m := 1; mientras (importe > 0) and (m <= n) hacer si (monedas_valor[m] <= importe) and (monedas[m] > 0) entonces monedas[m] := monedas[m] – 1; cambio[m] := cambio[m] + 1; importe := importe – monedas_valor[m]; si no m := m + 1; fsi fmientras si importe > 0 entonces devolver “Error”; fsi ffun Problema de los intervalos cerrados de puntos Enunciado: "Se pide un algoritmo para que, dado un conjunto de puntos sobre una recta real, determine el menor conjunto de intervalos cerrados de longitud 1 que contenga a todos los puntos dados". Solución: Partiendo de un conjunto ordenado de puntos, se consideran los puntos sucesivos en cada iteración del algoritmo. Si la entrada es un conjunto ordenado, el coste del algoritmo es lineal respecto a la cantidad de puntos. Algoritmo: tipos intervaloCerrado = vector [1..2] de real ftipos fun cubrePuntos(puntos[1..n] de real) dev conjuntoPuntos[intervaloCerrado] var intervaloAux : intervaloCerrado; i : entero; fvar conjuntoPuntos := conjuntoVacio(); i := 1; mientras i <= n hacer intervaloAux[1] := puntos[i]; intervaloAux[2] := puntos[i] + 1; añadir(intervaloAux, conjuntoPuntos); mientras i <= n Λ puntos[i] <= intervaloAux[2] hacer i := i + 1; fmientras fmientras ffun CONCLUSIONES El término voraz se deriva de la forma en que los datos de entrada se van tratando, realizando la elección de desechar o seleccionar un determinado elemento una sola vez. Los algoritmos voraces se emplean sobre todo para resolver problemas de optimización. Los algoritmos voraces son útiles para los problemas planteados con grafos como dimos a conocer en los ejemplos.