Lab 04-AED1-2013

8
Algoritmos y Estructura de Datos I Página 1 Karim Guevara, Álvaro Fernández Sesión 04 UNIVERSIDAD CATÓLICA DE SANTA MARÍA PROGRAMA PROFESIONAL DE INGENIERÍA DE SISTEMAS SESIÓN 04: TÉCNICAS DE DISEÑO DE ALGORITMOS. PARTE I I OBJETIVOS Definir las técnicas de diseño de algoritmos. Valorar los métodos de fuerza bruta, recursividad y divide y vencerás. II TEMAS A TRATAR Método de Fuerza Bruta Recursividad Divide y Vencerás III MARCO TEORICO 1. Fuerza Bruta La búsqueda por fuerza bruta, búsqueda combinatoria, búsqueda exhaustiva o simplemente fuerza bruta, es una técnica trivial pero muy a menudo usada, que consiste en enumerar sistemáticamente todos los posibles candidatos para la solución de un problema, con el fin de chequear si dicho candidato satisface la solución al mismo. Por ejemplo, un algoritmo de fuerza bruta para encontrar el divisor de un numero natural n consistiría en enumerar todos los enteros desde 1 hasta n, chequeando si cada uno de ellos divide n sin generar resto. Otro ejemplo de búsqueda por fuerza bruta, en este caso para solucionar el problema de las ocho reinas (posicionar ocho reinas en el tablero de ajedrez de forma que ninguna de ellas ataque al resto), consistiría en examinar todas las combinaciones de posición para las 8 reinas (en total 64! /56! = 178.462.987.637.760 posiciones diferentes), comprobando en cada una de ellas si las reinas se atacan mutuamente. La búsqueda por fuerza bruta es sencilla de implementar y, siempre que exista, encuentra una solución. Sin embargo, su coste de ejecución es proporcional al numero de soluciones candidatas, el cual es exponencialmente proporcional al tamaño del problema. Por el contrario, la búsqueda por fuerza bruta se usa habitualmente cuando el numero de

Transcript of Lab 04-AED1-2013

Page 1: Lab 04-AED1-2013

Algoritmos y Estructura de Datos I Página 1

Karim Guevara, Álvaro Fernández Sesión 04

UNIVERSIDAD CATÓLICA DE SANTA MARÍA PROGRAMA PROFESIONAL DE INGENIERÍA DE SISTEMAS

SESIÓN 04:

TÉCNICAS DE DISEÑO DE ALGORITMOS. PARTE I

I

OBJETIVOS

Definir las técnicas de diseño de algoritmos.

Valorar los métodos de fuerza bruta, recursividad y divide y vencerás.

II

TEMAS A TRATAR

Método de Fuerza Bruta

Recursividad

Divide y Vencerás

III

MARCO TEORICO

1. Fuerza Bruta

La búsqueda por fuerza bruta, búsqueda combinatoria, búsqueda exhaustiva o

simplemente fuerza bruta, es una técnica trivial pero muy a menudo usada, que consiste

en enumerar sistemáticamente todos los posibles candidatos para la solución de un

problema, con el fin de chequear si dicho candidato satisface la solución al mismo.

Por ejemplo, un algoritmo de fuerza bruta para encontrar el divisor de un numero natural

n consistiría en enumerar todos los enteros desde 1 hasta n, chequeando si cada uno de

ellos divide n sin generar resto. Otro ejemplo de búsqueda por fuerza bruta, en este caso

para solucionar el problema de las ocho reinas (posicionar ocho reinas en el tablero de

ajedrez de forma que ninguna de ellas ataque al resto), consistiría en examinar todas las

combinaciones de posición para las 8 reinas (en total 64! /56! = 178.462.987.637.760

posiciones diferentes), comprobando en cada una de ellas si las reinas se atacan

mutuamente.

La búsqueda por fuerza bruta es sencilla de implementar y, siempre que exista, encuentra

una solución. Sin embargo, su coste de ejecución es proporcional al numero de

soluciones candidatas, el cual es exponencialmente proporcional al tamaño del problema.

Por el contrario, la búsqueda por fuerza bruta se usa habitualmente cuando el numero de

Page 2: Lab 04-AED1-2013

Algoritmos y Estructura de Datos I Página 2

Karim Guevara, Álvaro Fernández Sesión 04

soluciones candidatas no es elevado, o bien cuando este puede reducirse previamente

usando algún otro método heurístico.

Es un método utilizado también cuando es más importante una implementación sencilla

que una de mayor rapidez. Este puede ser el caso en aplicaciones críticas donde cualquier

error en el algoritmo puede acarrear serias consecuencias, o también en aplicaciones

destinadas a demostrar teoremas matemáticos. Este algoritmo es usado como método

base para mediciones de rendimiento de otros algoritmos o metas heurísticas.

La búsqueda por fuerza bruta no se debe confundir con backtracking, método que

descarta un gran numero de conjuntos de soluciones, sin enumerar explícitamente cada

una de las mismas.

Implementación de la búsqueda por fuerza bruta

Algoritmo básico Para poder utilizar la búsqueda por fuerza bruta a un tipo especifico de problema, se

deben implementar las funciones primero, siguiente, valido, y mostrar. Todas recogerán

el parámetro indicando una instancia en particular del problema:

1. primero (P): genera la primera solución candidata para P.

2. siguiente (P, c): genera la siguiente solución candidata para P después de una

solución candidata c.

3. valido (P, c): chequea si una solución candidata c es una solución correcta de P.

4. mostrar (P, c): informa que la solución c es una solución correcta de P.

La función siguiente debe indicar de alguna forma cuando no existen mas soluciones

candidatas para el problema P después de la última. Una forma de realizar esto consiste

en devolver un valor "nulo". De esta misma forma, la función primero devolverá un valor

"nulo" cuando no exista ninguna solución candidata al problema P.

Usando tales funciones, la búsqueda por fuerza bruta se expresa mediante el siguiente

algoritmo:

c primero(P)

mientras c != null

Si valido(P,c) entonces mostrar(P,c)

c siguiente(P,c)

Por ejemplo, para buscar los divisores de un entero n, la instancia del problema P es el

propio numero n. la llamada primero (n) devolverá 1 siempre y cuando n⩾1, y "nulo" en

otro caso; la función siguiente (n,c) debe devolver c + 1 si c<n , y "nulo" caso contrario;

válido (n,c) devolverá verdadero si y solo si c es un divisor de n.

Variaciones comunes en el algoritmo

El algoritmo descrito anteriormente llama a la función mostrar para cada solución al

problema. Este puede ser fácilmente modificado de forma que termine una vez encuentre

la primera solución, o bien después de encontrar un determinado numero de soluciones,

después de probar con un numero especifico de soluciones candidatas, o después de

haber consumido una cantidad fija de tiempo de CPU.

Page 3: Lab 04-AED1-2013

Algoritmos y Estructura de Datos I Página 3

Karim Guevara, Álvaro Fernández Sesión 04

Explosión combinacional

La principal desventaja del método de fuerza bruta es que, para la mayoría de problemas

reales, el numero de soluciones candidatas es prohibitivamente elevado.

Por ejemplo, para buscar los divisores de un numero n tal y como se describe

anteriormente, el numero de soluciones candidatas a probar será de n. Por tanto, si n

consta de, digamos, 16 dígitos, la búsqueda requerirá de al menos 1015 comparaciones

computacionales, tarea que puede tardar varios días en un ordenador personal tipo. Si n

es un bit de 64 dígitos, que aproximadamente puede tener hasta 19 dígitos decimales, la

búsqueda puede tardar del orden de 10 años.

Este crecimiento exponencial en el número de candidatos, cuando crece el tamaño del

problema ocurre en todo tipo de problemas. Por ejemplo, si buscamos una combinación

particular de 10 elementos entonces tendremos que considerar 10! = 3,628,800

candidatos diferentes, lo cual en un PC habitual puede ser generado y probado en menos

de un segundo. Sin embargo, añadir un único elemento mas —lo cual supone solo un

10% más en el tamaño del problema— multiplicará el número de candidatos 11 veces —

lo que supone un 1000% de incremento. Para 20 elementos el número de candidatos

diferentes es 20!, es decir, aproximadamente 2.4×1018 o 2.4 millones de millones de

millones; la búsqueda podría tardar unos 10,000 años. A este fenómeno no deseado se le

denomina explosión combinacional.

2. Recursividad

Imagen recursiva formada por un triangulo. Cada triangulo esta compuesto de otros mas pequeños,

compuestos a su vez de la misma estructura recursiva.

Recurrencia, Recursión o recursividad es la forma en la cual se especifica un proceso

basado en su propia definición. Siendo un poco más precisos, y para evitar el aparente

círculo sin fin en esta definición:

Un problema que pueda ser definido en función de su tamaño, sea este N, pueda ser

dividido en instancias mas pequeñas (< N) del mismo problema y se conozca la solución

explicita a las instancias mas simples, lo que se conoce como casos base, se puede aplicar

inducción sobre las llamadas más pequeñas y suponer que estas quedan resueltas.

Para que se entienda mejor a continuación se exponen algunos ejemplos:

Ordenación por fusión(v: vector): Sea N = tamaño(v), podemos separar el

vector en dos mitades. Estas dos mitades tienen tamaño N/2 por lo que por

inducción podemos aplicar la ordenación en estos dos subproblemas. Una vez

Page 4: Lab 04-AED1-2013

Algoritmos y Estructura de Datos I Página 4

Karim Guevara, Álvaro Fernández Sesión 04

tenemos ambas mitades ordenadas simplemente debemos fusionarlas. El caso

base es ordenar un vector de 0 elementos, que esta trivialmente ordenado y

no hay que hacer nada.

En este ejemplo podemos observar como un problema se divide en varias (>=1)

instancias del mismo problema, pero de tamaño menor gracias a lo cual se puede aplicar

inducción, llegando a un punto donde se conoce el resultado (el caso base).

Funciones definidas de forma recurrente

Aquellas funciones cuyo dominio puede ser recursivamente definido pueden ser

definidas de forma recurrente.

El ejemplo más conocido es la definición recurrente de la función factorial n!:

Con esta definición veamos como funciona esta función para el valor del factorial de 3:

3! = 3 · (3-1)!

= 3 · 2!

= 3 · 2 · (2-1)!

= 3 · 2 · 1!

= 3 · 2 · 1 · (1-1)!

= 3 · 2 · 1 · 0!

= 3 · 2 · 1 · 1

= 6

La implementación del cálculo recursivo del factorial de un número sería el siguiente:

int factorial(int x)

{

if (x > -1 && x < 2) return 1; // Cuando -1 < x < 2 devolvemos 1

// puesto que 0! = 1 y 1! = 1

else if (x < 0) return 0; // Error no existe factorial de

// números negativos

return x * factorial(x - 1); // Si x >= 2 devolvemos el producto

// de x por el factorial de x - 1

}

El seguimiento de la recursividad programada es casi exactamente igual al ejemplo antes

dado, para intentar ayudar a que se entienda mejor se ha acompañado con muchas

explicaciones los distintos sub-procesos de la recursividad.

X = 3 //Queremos 3!, por lo tanto X inicial es 3

X >= 2 -> return 3*factorial(2);

X = 2 //Ahora estamos solicitando el factorial de 2

X >= 2 -> return 2*factorial(1);

X = 1 // Ahora estamos solicitando el factorial de 1

X < 2 -> return 1;

[En este punto tenemos el factorial de 1 por lo que volvemos marcha atrás resolviendo

todos los resultados]

return 2 [es decir: return 2*1 = return 2*factorial(1)]

return 6 [es decir: return 3*2 = return 3*factorial(2)*factorial(1)] // El resultado devuelto es 6

Page 5: Lab 04-AED1-2013

Algoritmos y Estructura de Datos I Página 5

Karim Guevara, Álvaro Fernández Sesión 04

3. Divide y Vencerás

En la cultura popular, divide y vencerás hace referencia a un refrán que implica resolver

un problema difícil, dividiéndolo en partes mas simples tantas veces como sea necesario,

hasta que la resolución de las partes se torna obvia. La solución del problema principal se

construye con las soluciones encontradas.

En las ciencias de la computación, el termino divide y vencerás (DYV) hace referencia a

uno de los mas importantes paradigmas de diseño algorítmico. El método esta basado en

la resolución recursiva de un problema dividiéndolo en dos o más subproblemas de igual

tipo o similar. El proceso continúa hasta que estos llegan a ser lo suficientemente

sencillos como para que se resuelvan directamente. Al final, las soluciones a cada uno de

los subproblemas se combinan para dar una solución al problema original.

Esta técnica es la base de los algoritmos eficientes para casi cualquier tipo de problema

como, por ejemplo, algoritmos de ordenamiento (quicksort, mergesort, entre muchos

otros),multiplicar números grandes (Karatsuba), análisis sintácticos (análisis sintáctico

top-down) y la transformada discreta de Fourier.

Por otra parte, analizar y diseñar algoritmos de DyV son tareas que llevan tiempo

dominar. Al igual que en la inducción, a veces es necesario sustituir el problema original

por uno mas complejo para conseguir realizar la recursión, y no hay un método

sistemático de generalización.

El nombre divide y vencerás también se aplica a veces a algoritmos que reducen cada

problema a un único subproblema, como la búsqueda binaria para encontrar un elemento

en una lista ordenada. Estos algoritmos pueden ser implementados mas eficientemente

que los algoritmos generales de “divide y vencerás”; en particular, si es usando una serie

de recursiones que lo convierten en simples bucles.

La corrección de un algoritmo de “divide y vencerás”, esta habitualmente probada por

medio de una inducción matemática, y su coste computacional se determina resolviendo

relaciones de recurrencia.

Precedentes históricos

La búsqueda binaria, un algoritmo de divide y vencerás en el que el problema original es

partido sucesivamente en subproblemas simples de más o menos la mitad del tamaño,

tiene una larga historia. Otro algoritmo de “divide y vencerás” con un único subproblema

es el algoritmo de Euclides para computar el máximo común divisor de dos números

(mediante reducción de números a problemas equivalentes cada vez mas pequeños), que

data de muchos siglos antes de Cristo.

Un ejemplo antiguo de algoritmo de “divide y vencerás” con múltiples subproblemas es

la descripción realizada por Gauss en 1805 de lo que se le llama ahora algoritmo de la

rápida transformación de Fourier Cooley-Tukey (FFT), aunque él no analizo su conjunto

de operaciones cuantitativamente y los FFT no se difundieron hasta que se

redescubrieron casi un siglo después.

Otro problema antiguo de 2 subdivisiones de “divide y vencerás” que fue

específicamente desarrollado para computadores y analizado adecuadamente es el

algoritmo de merge-sort, inventado por John von Neumann en 1945.

Page 6: Lab 04-AED1-2013

Algoritmos y Estructura de Datos I Página 6

Karim Guevara, Álvaro Fernández Sesión 04

Diseño e implementación

La resolución de un problema mediante esta técnica consta fundamentalmente de los

siguientes pasos:

1. En primer lugar ha de plantearse el problema de forma que pueda ser

descompuesto en k subproblemas del mismo tipo, pero de menor tamaño. Es

decir, si el tamaño de la entrada es n, hemos de conseguir dividir el problema en k

subproblemas (donde 1 ≤ k ≤ n), cada uno con una entrada de tamaño nk y donde

0 ≤ nk < n. A esta tarea se le conoce como división.

2. En segundo lugar han de resolverse independientemente todos los subproblemas,

bien directamente si son elementales o bien de forma recursiva. El hecho de que

el tamaño de los subproblemas sea estrictamente menor que el tamaño original

del problema nos garantiza la convergencia hacia los casos elementales, también

denominados casos base.

3. Por ultimo, combinar las soluciones obtenidas en el paso anterior para construir la

solución del problema original. Los algoritmos divide y vencerás (o divide and

conquer, en ingles), se diseñan como procedimientos generalmente recursivos.

TipoSolucion : AlgoritmoDyV (p: TipoProblema)

Si esCasoBase(p)

retornar resuelve(p)

Sino

TipoProblema : subproblemas[]

subproblemas = divideEnSubproblemas(p)

TipoSolucion : soluciones_parciales[]

Para sp itera subproblemas

soluciones_parciales.push_back(AlgoritmoDYV(sp))

Fin_Para

retornar mezcla(soluciones_parciales)

Fin_Si

Fin_AlgoritmoDyV

Por el hecho de usar un diseño recursivo, los algoritmos diseñados mediante la técnica de

Divide y Vencerás van a heredar las ventajas e inconvenientes que la recursión plantea:

Por un lado el diseño que se obtiene suele ser simple, claro, robusto y elegante, lo

que da lugar a una mayor legibilidad y facilidad de depuración y mantenimiento del

código obtenido.

Por contra, los diseños recursivos conllevan normalmente un mayor tiempo de

ejecución que los iterativos, además de la complejidad espacial que puede

representar el uso de la pila de recursión.

Sin embargo, este tipo de algoritmos también se pueden implementar como un algoritmo

no recursivo que almacene las soluciones parciales en una estructura de datos explicita,

como puede ser una pila, cola, o cola de prioridad. Esta aproximación da mayor libertad

al diseñador, de forma que se pueda escoger que subproblema es el que se va a resolver a

continuación, lo que puede ser importante en el caso de usar técnicas como Ramificación

y acotación o de optimización.

Page 7: Lab 04-AED1-2013

Algoritmos y Estructura de Datos I Página 7

Karim Guevara, Álvaro Fernández Sesión 04

Eligiendo los casos base

En cualquier algoritmo recursivo, hay una libertad considerable para elegir los casos

bases, los subproblemas pequeños que son resueltos directamente para acabar con la

recursión. Elegir los casos base más pequeños y simples posibles es más elegante y

normalmente nos da lugar a programas más simples, porque hay menos casos a

considerar y son más fáciles de resolver. Por ejemplo, un algoritmo FFT podría parar la

recursión cuando la entrada es una muestra simple, y el algoritmo quicksort de

ordenación podría parar cuando la entrada es una lista vacía. En ambos casos, solo hay

que considerar un caso base a considerar, y no requiere procesamiento.

Por otra parte, la eficiencia normalmente mejora si la recursión se para en casos

relativamente grandes, y estos son resueltos no recursivamente. Esta estrategia evita la

sobrecarga de llamadas recursivas que hacen poco o ningún trabajo, y pueden también

permitir el uso de algoritmos especializados no recursivos que, para esos casos base, son

mas eficientes que la recursión explicita. Ya que un algoritmo de DyV reduce cada

instancia del problema o subproblema aun gran numero de instancias base, estas

habitualmente dominan el coste general del algoritmo, especialmente cuando la

sobrecarga de separación/unión es baja.

Compartir subproblemas repetidos

Para algunos problemas, la recursión ramificada podría acabar evaluando el mismo

subproblema muchas veces. En tales casos valdría la pena identificar y guardar las

soluciones de estos subproblemas solapados, una técnica comúnmente conocida como

memorización. Llevado al límite, nos lleva a algoritmos de “divide y vencerás” de

bottom-up tales como la programación dinámica y análisis gráfico.

Ventajas

Resolución de problemas complejos Este modelo algorítmico es una herramienta potente para solucionar problemas

complejos, tales como el clásico juego de las torres de Hanoi. Todo lo que necesita este

algoritmo es dividir el problema en subproblemas más sencillos, y estos en otros mas

sencillos hasta llegar a unos subproblemas sencillos (también llamados casos base). Una

vez ahí, se resuelven y se combinan los subproblemas en orden inverso a su inicio. Como

dividir los problemas es, a menudo, la parte más compleja del algoritmo. Por eso, en

muchos problemas, el modelo solo ofrece la solución más sencilla, no la mejor.

Eficiencia del algoritmo

Normalmente, esta técnica proporciona una forma natural de diseñar algoritmos

eficientes. Por ejemplo, si el trabajo de dividir el problema y de combinar las soluciones

parciales es proporcional al tamaño del problema (n); además, hay un numero limitado p

de subproblemas de tamaño aproximadamente igual a n/p en cada etapa; y por ultimo, los

casos base requieren un tiempo constante (O(1)); entonces el algoritmo divide y vencerás

tiene por cota superior asintótica a O(nlogn). Esta cota es la que tienen los algoritmos

divide y vencerás que solucionan problemas tales como ordenar y la transformada

discreta de fourier. Ambos procedimientos reducen su complejidad, anteriormente

Page 8: Lab 04-AED1-2013

Algoritmos y Estructura de Datos I Página 8

Karim Guevara, Álvaro Fernández Sesión 04

definida por O(n2). Para terminar, cabe destacar que existen otros enfoques y métodos

que mejoran estas cotas.

IV

ACTIVIDADES

1. Leer atentamente la teoría de los algoritmos y buscar implementaciones de los mismos

en internet.

2. Ahora implementarlos en cualquier lenguaje de programación.

V

EJERCICIOS

1. Recurrencia: Implementar en C++, los siguientes algoritmos recursivos:

a) Factorial n! = n × (n-1)!

b) Sucesión de Fibonacci f(n) = f(n-1) + f(n-2)

c) Números de Catalan C(2n, n)/(n+1)

d) Las Torres de Hanoi

e) Función de Ackermann

f) Máximo Común Divisor de dos números.

2. Algoritmo Divide y Vencerás: Implementarla solución de los siguientes problemas:

a) Multiplicación de Enteros Grandes: algoritmo eficiente para multiplicar números

de tamaño considerable, que se salen de los límites de representación, y no

abordable con los algoritmos clásicos debido al excesivo coste.

b) Subvector de suma máxima: Algoritmo eficiente para encontrar subcadenas

dentro de un vector evitando tener que recorrer todo el vector desde cada

posición.

VI

BIBLIOGRAFIA Y REFERENCIAS

Wikipedia (para las implementaciones)

BIBLIOGRAFÍA BÁSICA

D.S.Malik, “DATA STRUCTURES USIGN C++”, Thomson Learning, 2003

J. Galve. “ALGORITMIA”, Ed.Adisson Wesley, España, 2000.

Brassard, “ANÁLISIS DE ALGORITMOS”, Ed. Mc. Graw Hill., España, 1999.,

BIBLIOGRAFÍA COMPLEMENTARIA

Wirth M., “ALGORITMOS Y ESTRUCTURA DE DATOS”, Ed. Addison Wesley, México, 1997

Aho. “DISEÑO Y ANÁLISIS DE ALGORITMOS”, Ed. Addison Wesley. USA, 1999.

Deitel & Deitel “COMO PROGRAMAR EN C/C++”. Editorial Prentice Hall, 1995.