Cap´ıtulo 2: Paradigmas de Dise˜
no de Algoritmos
Del libro: Dise˜no de Algoritmos - Nivio Ziviani (UFMG)
C y Pascal
Setiembre - 2011
Ingenier´ıa de Software
´Indice
1 Recursividad
2 Algoritmos de prueba y error (Backtracking)
3 Divide y vencer´as
4 Balanceado
5 Programaci´on Din´amica
6 Algoritmos Voraces y aproximados
Recursividad
Recursividad
• Un m´etodo que se llama as´ı mismo directa o indirectamente es llamado recursivo.
• La recursividad permite describir algoritmos de una forma mas clara y concisa, especialmente para problemas que por su naturaleza utilizan estructuras recursivas.
• Por ejemplo, los ´arboles binarios de b´usqueda:
• Todos los registros con llaves menores se encuentran en los sub´arboles de la izquierda.
• Todos los registros con llaves mayores se encuentran en los sub´arboles de la derecha.
Recursividad
Recursividad
package cap2;
public class ArbolBinario { private static class Nodo {
Object reg; Nodo izq; Nodo der; }
Recursividad
Recursividad
• Uno de los algoritmos mas conocidos para recorrer todos los los registros de un ´arbol, es conocido como recorrido en orden, su funcionamiento es como sigue:
1 Recorrer el sub´arbol izquierdo mediante recorrido en orden.
2 Visitar la ra´ız.
3 Recorrer el sub´arbol derecho mediante recorrido en orden. • El recorrido en orden, los nodos se visitan atendiendo el orden
lexicogr´afico de las claves.
private void enOrden (Nodo p) { if (p != null) { enOrden (p.izq); System.out.println (p.reg.toString()); enOrden (p.der); } }
Recursividad
Implementaci´
on de la recursividad
• Se utiliza un pila para almacenar los datos pasados en cada llamada de un procedimiento que no ha terminado.
• Todos los datos que no son globales se guardan en la pila registrando el estado actual del procedimiento.
• Cuando un procedimiento interior termina, los datos de la pila son recuperados.
• En el caso del algoritmo b´usqueda en orden:
• En cada llamada se almacena en la pila el valor de p y la direcci´on de retorno de la llamada recursiva.
• Cuando el procedimiento en alcanza p = nill, se retorna al
procedimiento que lo llam´o utilizando la direcci´on de llamada que se encuentra encima de la pila.
Recursividad
Cuando no usar recursividad
• No todos los problemas de naturaleza recursiva deben ser resueltos utilizando recursividad.
• Calculo de los n´umeros de Fibonacci:
f0= 0, f1= 1
fn= fn−1+ fn−2, para : n ≥ 2
• Soluci´on: fn=√1 5[Φ
n− (1 − Φ)n] donde Φ = (1 +√5)/2 ≈ 1, 618 es conocida como la
raz´on de oro.
• El procedimiento recursivo obtenido directamente de la sucesi´on es la siguiente: package cap2;
public class Fibonacci {
public static int fibRec (int n) { if (n < 2) return n;
else return (fibRec (n-1) + fibRec (n-2)); }
Recursividad
Cuando no usar recursividad
• El programa es extremadamente ineficiente, ya que recalcula el mismo valor varias veces.
• En este caso la complejidad de tiempo esta dado por f (n) es de orden O(Φn)
Recursividad
Versi´
on iterativa para el calculo de la secuencia de
Fibonacci
public static int fibIter (int n) { int i = 1, f = 0; for (int k = 1; k <= n; k++) { f = i + f; i = f - i; } return f; }
• El programa tiene complejidad temporal O(n)
• Debemos usar recursividad cuando existe una soluci´on obvia de manera iterativa.
Recursividad
Mas ejemplos - Factorial
public class Factorial {
public int factorial(int n){ if (n <= 1)
return (1); else
return (n*factorial(n-1)); }
• Si T(n) es la ecuaci´on de recurrencia para el algoritmo f actorial entonces: T (n) = c1, n ≤ 1 T (n) = T (n − 1) + c2, n > 1 Entonces: T (n) = T (n − 1) + c2 T (n) = (T (n − 2) + c2) + c2= T (n − 2) + 2 × c2 T (n) = (T (n − 3) + c2) + 2 × c2= T (n − 2) + 3 × c2 T (n) = T (n − k) + k × c2
Recursividad
Mas ejemplos - Torres de Hanoi
public class Hanoi {
public void hanoi(int n, int inicio, int temp, int fin){ if (n > 0){
hanoi (n-1, inicio, fin, temp );
System.out.println("mover del poste " + inicio + " al " + fin); hanoi (n-1, temp, inicio, fin);
} }
Algoritmos de prueba y error (Backtracking)
Backtracking
• Prueba y error: descomponer el proceso en un n´umero finito de subtareas parciales que deben ser exploradas exahustivamente.
• El proceso de prueba gradualmente construye y prerecorre un ´arbol de subtareas.
• Los algoritmos de prueba y error no siguen una regla fija de computaci´on:
• Los pasos en direcci´on a la soluci´on final son efectuados y realizados.
• En caso los pasos anteriores no lleven a la soluci´on final, ellos pueden ser retirados y borrados del registro.
• Cuando la b´usqueda en el ´arboles de soluciones crece r´apidamente es necesario utilizar algoritmos aproximados o heur´ısticas que no garantizan una soluci´on ´optima pero son r´apidos.
Algoritmos de prueba y error (Backtracking)
Movimiento del Caballo
• Tablero con n × n posiciones, el caballo se mueve seg´un las reglas del ajedrez.
• Problema: a partir de (x0, y0), encontrar, si existe, un recorrido del caballo que visite
todos los puntos del tablero una ´unica vez.
• Intenta el siguiente movimiento: void Intenta {
Inicializa selecci´on de movimientos; do {
selecciona el siguiente candidato para mover; if (movimiento aceptable) {
registra movimiento;
if (tablero no esta recorrido){
intenta nuevo movimiento; // llamada recursiva if ( no ha tenido exito)
borra registro anterior; }
}
} while (se produzca movimiento) o (no haya candidatos para mover); }
Algoritmos de prueba y error (Backtracking)
Ejemplo de Backtracking: Movimiento de caballo
• El tablero puede ser representado por una matriz de n × n.• La situaci´on de cada posici´on puede ser representada por un entero para registrar la hist´oria del recorrido:
• t[x, y] = 0, posici´on < x, y > no visita.
• t[x, y] = i, posici´on < x, y > visitada en el i−´esimo movimiento, 1 ≤ i ≤ n2.
Algoritmos de prueba y error (Backtracking)
Ejemplo de Backtracking: Movimiento de caballo
package cap2;
public class RecorridoCaballo { private int n; // Tama~no del tablero private int a[], b[], t[][];
public RecorridoCaballo (int n) { this.n = n;
this.t = new int[n][n]; this.a = new int[n]; this.b = new int[n];
a[0] = 2; a[1] = 1; a[2] =-1; a[3] =-2;
b[0] = 1; b[1] = 2; b[2] = 2; b[3] = 1;
a[4] = -2; a[5] = -1; a[6] = 1; a[7] = 2; b[4] = -1; b[5] = -2; b[6] =-2; b[7] = -1; for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) t[i][j] = 0; t[0][0] = 1; // posici´on de inicio
Algoritmos de prueba y error (Backtracking)
Ejemplo de Backtracking: Movimiento de caballo
public boolean intenta (int i, int x, int y) { int u, v, k; boolean q;
k = -1; // inicializa la posici´on de movimientos do {
k = k + 1; q = false; u = x + a[k]; v = y + b[k];
/* Verifica los limites del tablero */
if ((u >= 0) && (u <= 7) && (v >= 0) && (v <= 7)) if (t[u][v] == 0) {
t[u][v] = i;
if (i < n * n) { // el tablero no se encuentra lleno q = intenta (i+1, u, v); // intenta un nuevo movimiento if (!q) t[u][v] = 0; // no hay exito, inicializa el registro }
else q = true; }
} while (!q && (k != 7)); // no hay celdas para visitar a partir de x,y return q;
Algoritmos de prueba y error (Backtracking)
Ejemplo de Backtracking: Movimiento de caballo
public void imprimeRecorrido () { for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++)
System.out.print ("\t" +this.t[i][j]); System.out.println ();
} }
public static void main (String[] args) {
RecorridoCaballo recorridoCaballo = new RecorridoCaballo (8); boolean q = recorridoCaballo.intenta (2, 0, 0);
if (q) recorridoCaballo.imprimeRecorrido(); else System.out.println ("Sin soluci´on"); }
Divide y vencer´as
Divide y vencer´
as
• Consiste en dividir o problema en partes menores, encontrar soluciones para las partes, y combinarlas en una soluci´on global.
• Ejemplo: encontrar el mayor o menor elemento de un vector de enteros, v[0 . . . n − 1], n ≥ 1
• Cada llamada de maxM in4 asigna a maxM in[0] y maxM in[1] el mayor y menor elemento en v[linf ], v[lin + 1], . . . , v[lsup]
Divide y vencer´as
Divide y vencer´
as
package cap2;
public class MaxMin4 {
public static int [] maxMin4 (int v[], int linf, int lsup) { int maxMin[] = new int[2];
if (lsup - linf <= 1) { if (v[linf] < v[lsup]) {
maxMin[0] = v[lsup]; maxMin[1] = v[linf]; }else {
maxMin[0] = v[linf]; maxMin[1] = v[lsup]; } }else {
int medio = (linf + lsup)/2; maxMin = maxMin4 (v, linf, medio); int max1 = maxMin[0], min1 = maxMin[1]; maxMin = maxMin4 (v, medio + 1, lsup); int max2 = maxMin[0], min2 = maxMin[1];
if (max1 > max2) maxMin[0] = max1; else maxMin[0] = max2; if (min1 < min2) maxMin[1] = min1; else maxMin[1] = min2; }
return maxMin; }
Divide y vencer´as
Divide y vencer´
as - An´
alisis del ejemplo
• Sea T (n) una funci´on de complejidad tal que T (n) es el n´umero de comparaciones entre los elementos de v y v contiene n elementos:
T (n) = 1 para n ≤ 2,
T (n) = T (bn/2c) + T (dn/2e) + 2 para n > 2,
• Podemos notar que n = 2jpara alg´un entero postivo j:
T (n) = 2T (n/2) + 2 T (n) = 2(2T (n/4) + 2) + 2 = 4T (n/4) + 4 + 2 T (n) = 4(2T (n/8) + 2) + 4 + 2 = 8T (n/8) + 8 + 4 + 2 Entonces: T (n) = 2kT (n/2k) +Pj=k j=12k
Tendra soluci´on cuando: n
2k= 2 entonces: k = lg n − 1 y T (n) = 2lg n−1+Plg n−1
i=1 2i
T (n) = n2 + [1−21−2lg n] − 1 =n2 − 1 + n − 1 T (n) = 3n2 − 2
Divide y vencer´as
Divide y vencer´
as: An´
alisis del ejemplo
• Conforme al teorema revisado en el cap´ıtulo 1, el algoritmo es ´optimo.
• Sin embargo, puede ser peor que los presentados en el cap´ıtulo 1, pues, por cada llamada del m´etodo se guardan los valores de linf , lsup, maxM in[0], maxM in[1], en la direcci´on de la llamada de retorno de cada llamada.
• Adem´as, una comparaci´on adicional es necesaria para cada llamada recursiva para verificar que: lsup − linf ≤ 1.
• n debe ser menor que la mitad del mayor entero que puede ser representado por el compilador, para no provocar overf low en la operaci´on lsup − linf .
Balanceado
Balanceado
• En el dise˜no de algoritmos es importante mantener el balanceado la subdivisi´on de un problema en partes menores.
• En divide y vencer´as no es la ´unica t´ecnica donde el balanceado es ´
util.
• Si consideramos el siguiente ejemplo de ordenaci´on:
• Selecciona el menor elemento del conjunto v[0 . . . n − 1] entonces intercambia este elemento con el primer elemento v[0].
• Repite el proceso con los n − 1 elementos restantes, el segundo mayor elemento es intercambiado con el segundo elemento v[1].
Balanceado
Balanceado - An´
alisis del ejemplo
• El algoritmo lleva a la siguiente ecuaci´on de recurrencia: T (n) = T (n − 1) + (n − 1), T (1) = 0, para los n´umeros de comparaciones entre elementos.
• Sustituyendo: T (n) = T (n − 1) + n − 1
T (n) = T (n − 2) + n − 2 + n − 1 = T (n − 2) + 2n − 2 T (n) = T (n − k) + kn −Pi=k
i=1i
Tiene soluci´on cuando n − k = 1, entonces k = n − 1, sustituyendo: T (n) = T (1) + (n − 1)n −Pi=n−1 i=1 i = 0 + n2− n − [ n(n+1) 2 − n] T (n) = n22 − n 2
Balanceado
Balanceado - An´
alisis del ejemplo
• El algoritmo no es eficiente para valores grandes de n.
• Para mejorar la eficiencia asint´otica, es necesario balancear: dividir el problema en dos subproblemas de tama˜nos aproximadamente iguales, en vez de uno de tama˜no 1 y otro de tama˜no n − 1.
Balanceado
Ejemplo de Balanceado - MergeSort
• Fusi´on: unir dos archivos ordenados, generando un tercero ordenado (merge).
• Copiar en el tercer archivo, el menor elemento de entre los menores de los ficheros iniciales, obviando este elemento de los pasos posteriores.
• Este proceso se debe repetir hasta que todos los elementos de los archivos de entrada se hayan copiado al tercer archivo.
• Algoritmo MergeSort:
1 Dividir recursivamente el vector a ser ordenado en dos, hasta obtener n vectores de un ´unico elemento.
2 Aplicar la fusi´on teniendo como entrada dos vectores de un elemento, formando un vector ordenado de un elemento.
3 Repetir el proceso formando vectores ordenados cada vez mayores hasta que todo el vector se encuentre ordenado.
Balanceado
Ejemplo de Balanceado - MergeSort
package cap2;
public class Ordenacion {
public static void mergeSort (int v[], int i, int j) { if (i < j) {
int m = (i + j)/2; mergeSort (v, i, m); mergeSort (v, m + 1, j);
merge (v, i, m, j); // Fusiona v[i..m] y v[m+1..j] en v[i..j] }
}
• Considere n como una potencia de 2.
• merge(v, i, m, j), recibe dos secuencias ordenadas v[i...m] y v[m + 1..j] y produce otra secuencia ordenada de los elementos anteriores.
• Como v[i...m] y v[m + 1..j] se encuentran ordenados, merge necesita como m´aximo n − 1 comparaciones.
• merge selecciona repetidamente el menor entre los menores elementos restantes en v[i...m] y v[m + 1..j]. En caso de igualdad lo retira de cualquiera de las listas.
Balanceado
An´
alisis de Ejemplo - MergeSort
• El comportamiento de mergesort puede representarse por la siguiente ecuaci´on de recurrencia:
T (n) = 2T (n/2) + n − 1, para n > 1. T (1) = 0
• La soluci´on a la ecuaci´on de recurrencia es: T (n) = n log n − n + 1. Resolver!!
Programaci´on Din´amica
Programaci´
on Din´
amica
• Cuando la suma de los tama˜nos de los subproblemas en un algoritmo recursivo es O(n), entonces es probable que el algoritmo recursivo tenga complejidad polinomial.
• Pero cuando la divisi´on de un problema de tama˜no n resulta en n subproblemas de tama˜no n − 1 entonces, es probable que el algoritmo recursivo tenga complejidad exponencial.
• En este caso, con la t´ecnica de la programaci´on din´amica se puede proponer un algoritmo m´as eficiente.
• La programaci´on di´amica calcula la soluci´on para todos los
subproblemas, partiendo de subproblemas menores hacia los mayores, almacenando los valores en una tabla.
• La ventaja es, que una vez que un subproblema es resuelto, la respuesta es almacenada en una tabla y nunca m´as es recalculado.
Programaci´on Din´amica
Programaci´
on Din´
amica - Ejemplo
• En el calculo de los n´umeros de Fibonacci, en el ejemplo anterior, se mostr´o un algoritmo recursivo de orden exponencial. Esto se debe, que se realizan calculos repetidos para obtener los valores de la sucesi´on, que habi´endose calculado previamente, no son registrados y utilizados para los calculos posteriores.
• Para el c´alculo de los n´umeros de Fibonacci, es posible dise˜nar un algoritmo que en tiempo lineal lo resuelva mediante la construcci´on de una tabla que permita ir almacenando los calculos realizados y luego reutilizarlos.
Fibonacci (n: entero): entero T[0] = 1
T[1] = 1
para i= 2,..., n hacer T[i]= T[i-1] + T[i-2] fin_para
Algoritmos Voraces y aproximados
Algoritmos voraces
• Resuelve problemas de optimizaci´on.
• Ejemplo: un algoritmo para encontrar el camino mas corto entre dos vertices de un grafo: • Selecciona la arista que parece mas promisoria en cualquier instante.
• Independientemente de lo que pueda suceder adelante y no reconsidera su decisi´on.
• No necesita evaluar alternativas, o usar procedimientos sofisticados para deshacer decisiones tomadas previamente.
• Problema general: dado un conjunto C, determinar un subconjunto S ⊆ C, tal que: • S satisface las propiedades de P , y
• S es m´ınimo (o m´aximo) en relaci´on a alg´un criterio α.
• El algoritmo voraz para resolver un problema, consiste en un proceso iterativo donde S es construido adicionando elementos de C uno a uno.
Algoritmos Voraces y aproximados
Caracter´ısticas de los algoritmos voraces
• Para construir la soluci´on ´optima existe un cojunto o lista de candidatos.
• Se elijen un conjunto de candidatos y otros se rechazan.
• Existe una funci´on que verifica si un conjunto particular de candidatos produce una soluci´on.
• Otra funci´on se encarga de verificar si la soluci´on es viable.
• Una funci´on de selecci´on, evalua en cualquier momento cual de los candidatos es el mas promisorio.
• Una funci´on objetivo, valoriza la soluci´on encontrada, como la longitud del camino construido (no aparece de manera explicita en el algoritmo voraz).
Algoritmos Voraces y aproximados
Seudoc´
odigo del algoritmo voraz
C Conjunto de candidatos S = ∅
Mientras C 6= ∅ y S no sea soluci´on
• x = selecciona (C); • C = C − x; • Si es viable (S + x) entonces S = S + x Si es soluci´on S • Retorna S Caso contrario
Algoritmos Voraces y aproximados
Caracter´ısticas de los algoritmos voraces
• La funci´on de selecci´on generalmente esta relacionada con la funci´on objetivo.
• Si el objetivo es:
• Maximizar ⇒ seleccionar´a el siguiente candidato que proporcione la mayor ganancia individual.
• Minimizar ⇒ seleccionar´a el candidato restante de menor costo. • El algoritmos nunca cambia de idea:
• Una vez que un candidato es seleccionado, es adicionado a la soluci´on y permanece para siempre.
• Una vez que el candidato es rechazado, nunca mas vuelve a ser considerado.
Algoritmos Voraces y aproximados
Algoritmos aproximados
• Los problemas que poseen algoritmos exponenciales para encontrar una soluci´on exacta, son considerados dif´ıciles.
• Los problemas considerados intratables o dif´ıciles son muy comunes, como por ejemplo, el problema del agente viajero el cual es O(n!).
• Frente a un problema dif´ıcil es com´un retirar la exigencia de que el algoritmo tenga que encontrar una soluci´on ´optima.
• En este caso utilizamos algoritmos eficientes que no garanticen tener una soluci´on optima, pero que sea lo mas pr´oxima posible a ella.
Algoritmos Voraces y aproximados
Tipos de algoritmos aproximados
• Heur´ıstica: Es un algoritmo que puede producir un buen resultado, o una soluci´on ´optima, as´ı como obtener una soluci´on que se encuentre distante de la ´optima.
• Algoritmos aproximados: Es un algoritmo que genera soluciones aproximadas dentro de un l´ımite en relaci´on a la soluci´on optima. La eficiencia del algoritmo depende de la calidad de los resultados.
Algoritmos Voraces y aproximados Ejemplos de algoritmos voraces
Dar Cambio Maquina expendedora
• Una maquina expendedora para devolver el cambio puede utilizar monedas de 2, 1, 0,50, 0,20 y 0,10 nuevos soles. El problema consiste en pagar el cambio a un cliente utilizando el menor n´umero posible de monedas.
Algoritmos Voraces y aproximados Ejemplos de algoritmos voraces
El problema de la mochila
• Dados n objetos y una mochila donde llevarlos. Para cada objeto i = 1, 2, 3, . . . , n el objeto i tiene un peso positivo wi y un valor
positivo vi. La mochila tiene una capacidad de carga W .
• El objetivo es llenar la mochila de tal manera que se maximice el valor de los objetos transportados, respetando la capacidad de la misma.
• En esta versi´on del problema suponemos que podemos partir los objetos en trozos mas peque˜nos, de tal manera que podemos llevar una fracci´on xi del objeto i (0 ≤ xi ≤ 1). En este caso el objeto xi
contribuye en xiwi al peso de la carga y en xivi al valor de la carga.
El problema se puede formular de la siguiente manera: M axPn
i=1xivi con la restricci´on
Pn
Algoritmos Voraces y aproximados Ejemplos de algoritmos voraces
El problema de la mochila
• Supongamos que tenemos los siguientes datos para resolver el problema:
Algoritmos Voraces y aproximados Ejemplos de algoritmos voraces
Planificaci´
on de tareas
• Se tiene un unico procesador que tiene que dar servicio a n clientes. El tiempo de atenci´on requerido por cada cliente se conoce de antemano, el cliente i requerir´a un tiempo ti para 1 ≤ i ≤ n. El
objetivo es minimizar el tiempo que pasan los clientes en el sistema. Es decir, se requiere minimizar:
T =Pn
i=1(tiempo en el sistema para el cliente i)
• Supongamos que tenemos 3 clientes, con t1= 5, t2 = 10 y t3= 3,