ALGORITMOS VORACES

Anuncio
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.
Descargar