resumenes programaciÓn iii - extensionuned.es · resumenes programaciÓn iii ... fundamentos de...

194
RESUMENES PROGRAMACIÓN III Curso 2008 2009

Upload: dinhcong

Post on 07-Oct-2018

239 views

Category:

Documents


4 download

TRANSCRIPT

Page 1: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

                        RESUMENES 

PROGRAMACIÓN III        

Curso 2008 ‐ 2009 

  

 

 

 

 

 

 

 

 

Page 2: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

 

Page 3: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Alumna: Alicia Sánchez Centro: UNED-Las Rozas (Madrid)

Resumen de programación 3

Tema 1. Preliminares.

Page 4: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 1 Curso 2007/08 Página 2 de 7

Índice:

1.1. Introducción ……………………………………………………… 3 1.2. ¿Qué es un algoritmo? …………………………………………… 3 1.3. Notación para los programas …………………………………….. 5 1.4. Notación matemática …………………………………………….. 5

1.5. Técnica de demostración 1: Contradicción ……………………… 5

1.6. Técnica de demostración 2: Inducción matemática ……………... 6 1.6.1. El principio de inducción matemática ……………………... 6

1.7. Recordatorio ……………………………………………………... 7

Bibliografía: Se ha tomado apuntes del libro:

Fundamentos de algoritmia. G. Brassard y P. Bratley

Page 5: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 1 Curso 2007/08 Página 3 de 7

Previo a ver este resumen decir que conviene leerse este tema, ya que así nos iremos familiarizando con la algoritmia y determinados conceptos, como la demostración por inducción. Este resumen es complementario al libro, por lo que conviene estudiarlo junto.

1.1. Introducción En los problemas veremos estos pasos a seguir para resolverlos, que será bastante importante el tenerlo claro:

- Elección del esquema (voraz, vuelta atrás, divide y vencerás). - Identificación del problema con el esquema. - Estructura de datos. - Algoritmo completo (con pseudocódigo). - Estudio del coste.

A lo largo de nuestro temario seguiremos este planteamiento: - Empezaremos a ver el estudio del coste (temas 2, 3 y 4). - Luego veremos la estructura de datos (tema 5). - Para a continuación ver los distintos esquemas, que los dividiremos en 3,

siguiendo este mismo orden (es recomendado): 1. Esquema voraz (tema 6). 2. Exploración de grafo, que incluyen exploración en anchura,

profundidad, vuelta atrás y ramificación y poda (tema 9). 3. Esquema de divide y vencerás (tema 7).

A continuación, daremos unas nociones básicas previas antes de entrar en nuestra planificación del temario, de nuevo lo hacemos para que vayan sonando los conceptos.

En este tema, así como en el tema 2 no tendremos problemas del mismo. Por lo que se tendrá que estudiar la teoría dada, que aunque es introductoria siempre conviene hacerlo.

1.2. ¿Qué es un algoritmo? Un algoritmo es un conjunto de reglas para efectuar algún cálculo, bien sea a mano o, más frecuentemente, en una máquina. Nos interesan los algoritmos utilizados en una computadora. La ejecución de un algoritmo no debe de implicar ninguna decisión subjetiva ni tampoco debe de hacer preciso el uso de la intuición y de la creatividad.

Es importante decidir cuál de los algoritmos escoger de entre una gama de ellos para resolver un problema. Dependiendo de nuestras prioridades y de los límites del equipo que esté disponible para nosotros, quizá necesitemos seleccionar el algoritmo que requiera menos tiempo, o el que utilice menos espacio, o el que sea más fácil de programar, y así sucesivamente. La Algoritmia es la ciencia que nos permite evaluar el efecto de estos diferentes factores externos sobre los algoritmos disponibles, de tal modo que sea posible seleccionar el que más se ajuste a nuestras circunstancias particulares; también es la ciencia que nos indica la forma de diseñar un nuevo algoritmo para una tarea concreta.

Page 6: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 1 Curso 2007/08 Página 4 de 7

Ejemplo: multiplicación de dos números enteros. Para hacerlo tendremos estos métodos:

- Algoritmos “clásicos”: Son similares a la multiplicación con lápiz y papel de los dos números enteros. Para ello, tendremos la multiplicación inglesa y americana, donde la única diferencia reside en que se multiplica de izquierda a derecha o viceversa.

- Algoritmo de multiplicación “a la russe”: Para ello seguiremos este procedimiento:

1. Se hacen dos columnas poniendo el número menor a la izquierda y el mayor a la derecha. El de la izquierda se divide por 2 hasta llegar a 1 y el de la derecha se multiplica por dos.

2. Se eliminan las filas en las cuales el número de la columna izquierda sea par y se suman los números que quedan en la columna de la derecha. Es la más parecida a la que se emplea en el hardware de una computadora binaria.

- Algoritmo mediante divide y vencerás: Seguiremos estos pasos por este orden (separado respecto al libro para que sea mayor su comprensión):

1. Necesitamos tener el multiplicando y multiplicador con el mismo número de cifras múltiplo de dos, si no se añadirían ceros, si hiciera falta. Multiplicamos la mitad izquierda del multiplicando por la mitad izquierda del multiplicador y ponemos el resultado desplazado hacia la izquierda tantas veces como cifras haya en el multiplicador: cuatro en nuestro caso.

2. Luego la mitad izquierda del multiplicando con la derecha del multiplicador y desplazamos dos a la izquierda.

3. Después, multiplicamos la mitad derecha del multiplicando por la izquierda del multiplicador y desplazamos dos a la izquierda.

4. Por último, ambas mitades derechas y no desplazamos ninguna posición. Para finalizar, sumamos estos cuatro resultados intermedios.

Hemos reducido la multiplicación de dos números de cuatro cifras a cuatro multiplicaciones de números de cuatro cifras, junto con un cierto número de desplazamientos y una suma final.

De entre los cuatro algoritmos ya vistos, este último reduce la multiplicación de

dos números grandes a tres y no cuatro, por lo que sería el más eficiente. La meta de nuestro libro es enseñar a tomar este tipo de decisiones.

Page 7: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 1 Curso 2007/08 Página 5 de 7

1.3. Notación para los programas.

Es importante decidir la forma en que vamos a describir nuestros algoritmos. Para ello no nos limitaremos en ningún lenguaje de programación concreto (lo que denominaremos pseudocódigo): de esta manera, los aspectos esenciales de un algoritmo no resultarán oscurecidos por detalles de programación relativamente poco importantes y no importa cuál sea el lenguaje bien estructurado que prefiere el lector.

Ejemplo: Tenemos esta función con la notación descrita anteriormente: funcion rusa (m, n) resultado ← 0;

repetir si m es impar entonces resultado ← resultado + n;

푚 ← 푚 ÷ 2; 푚 ← 푛 + 1;

hasta que 푚 = 1; devolver resultado;

1.4. Notación matemática.

Se usará la notación matemática para: - Calculo proposicional. - Teoría de conjuntos. - Enteros, reales e intervalos. - Funciones y relaciones. - Cuantificadores. - Sumas y productos.

1.5. Técnica de demostración 1: Contradicción. La demostración por contradicción, o prueba indirecta, consiste en demostrar la veracidad de una sentencia demostrando que su negación da lugar a una contradicción.

Ejemplo: Se nos da un teorema que dice que existen infinitos números reales, el cual demostraremos por contradicción para ello, realizaremos estos pasos (usaremos esta demostración en los esquemas voraces, por ello es importante el concepto):

- Empezaremos suponiendo lo contrario al teorema, en este caso, que existe un conjunto finito, para así buscar una contradicción.

- Si terminamos en una afirmación evidentemente falsa podemos deducir que la primera sentencia es verdadera.

Page 8: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 1 Curso 2007/08 Página 6 de 7

1.6. Técnica de demostración 2: Inducción matemática.

Es una de las herramientas básicas útiles en la Algoritmia. Tendremos dos enfoques básicos opuestos fundamentales:

1. Inducción: Consiste en inferir una ley general a partir de casos particulares. En general no se puede confiar en el resultado del razonamiento inductivo. Mientras que haya casos que no hayan sido considerados, sigue siendo posible que la regla general inducida sea correcta. Aunque no se puede despreciar este enfoque.

Ejemplo: Consideremos el polinomio 푝(푛) = 푛 + 푛 + 41. Si computamos valores como 푝(0), 푝(1), 푝(2),…, 푝(10), se va encontrando 41, 43, 47,… y 151. Es fácil verificar que todos estos enteros son números primos. Por tanto, es natural inferir por inducción que 푝(푛) es primo para todos los valores enteros de n, pero 푝(40) =1.681 = 41 es compuesto. Deducimos entonces que es una falsa inducción. Hay veces que es necesario usar la inducción porque es el único método, como, por ejemplo, en el descubrimiento del cometa Halley, hecho a partir de datos obtenidos en experimentos.

- Deducción: Es una inferencia de lo general a lo particular. Por contraste, el razonamiento deductivo no está sometido a este tipo de errores. Siempre y cuando la regla invocada sea correcta, y sea aplicable a la situación que se estudia, la conclusión que se alcanza es necesariamente correcta. Una de las técnicas deductivas más útiles que están disponibles en matemáticas se llama inducción matemática. No hay que confundir con el otro enfoque, que aunque se parezca en el nombre no es similar.

1.6.1. El principio de inducción matemática.

Consideremos el siguiente algoritmo: funcion cuadrado (n) si 푛 = 0 entonces devolver 0

si no devolver 2 ∗ 푛 + 푐푢푎푑푟푎푑표(푛 − 1) − 1

Si se hace pruebas en varias entradas pequeñas, observamos que cuadrado (0) = 0; cuadrado (1) = 1; cuadrado (2) = 4;… Por inducción, parece evidente que cuadrado(푛) = 푛 para todos los 푛 ≥ 0. Definición: Este principio dice que la propiedad P es cierto para todo 푛 ≥ 푎 si:

1. 푃(푎) es cierto. 2. 푃(푛) debe ser cierto siempre que 푃(푛 − 1) sea válido para todos los

enteros 푛 > 푎.

A veces, las demostraciones mediante inducción matemática se pueden transformar en algoritmos, como puede ser el ejemplo del embaldosado.

Page 9: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 1 Curso 2007/08 Página 7 de 7

1.7. Recordatorio.

Se nos dan recordatorios, tales como limites, sumas de series sencillas, combinatoria y probabilidad. En ejercicios posteriores los usaremos, por lo que evitamos dar más detalles.

Page 10: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

 

Page 11: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Alumna: Alicia Sánchez Centro: UNED-Las Rozas (Madrid)

Resumen de programación 3

Tema 2. Algoritmia elemental.

Page 12: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 2 Curso 2007/08 Página 2 de 8

Índice:

2.3. La eficiencia de los algoritmos …………………………………... 3 2.4. Análisis de “caso medio” y “caso peor”………….…………….… 4 2.5. ¿Qué es una operación elemental? ………………………...…....... 7 2.6. Más factores de tiempo …………………………………………... 8

Bibliografía: Se ha tomado apuntes del libro:

Fundamentos de algoritmia. G. Brassard y P. Bratley

Page 13: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 2 Curso 2007/08 Página 3 de 8

Nos hemos saltado los apartados 2.1 y 2.2, que corresponden con la introducción y problemas y ejemplares respectivamente, ya que lo siguiente es más propio del tema. Por ello, empezaremos por el 2.3. Por otro lado, comentar que este tema ha sido costoso el resumirlo debido a que el resto de temas serán más complicados de estudiar y conviene entender bien el concepto básico de la algoritmia, que junto con el tema 1 no tiene ejercicios.

2.3. La eficiencia de los algoritmos. Cuando tengamos que resolver un problema es posible que estén disponibles varios algoritmos adecuados. Deseamos seleccionar el mejor posible, tal y como vimos en la introducción (tema 1). Para ello tendremos dos enfoques:

- El enfoque empírico (o a posteriori): Consiste en programar las técnicas comparadoras e ir probándolas en distintos casos con ayuda de una computadora.

- El enfoque teórico (o a priori): Es el que nosotros propugnamos en este libro. Consiste en determinar matemáticamente la cantidad de recursos necesarios para cada uno de los algoritmos como función del tamaño de los casos considerados. Nos interesarán en especial estos recursos:

1. Tiempo de computación: Es el que usaremos a lo largo del libro, por tanto, será el más importante a tener en cuenta.

2. Espacio de almacenamiento: Indica el espacio que ocupa un algoritmo. No lo emplearemos en este tema y posteriores.

Compararemos los algoritmos tomando como base sus tiempos de ejecución. Cuando hablemos de la eficiencia de un algoritmo querremos decir lo rápido que se ejecuta y en esto consistirá nuestro análisis del coste (temas 3 y 4).

Utilizaremos la palabra tamaño para indicar cualquier entero que mida de alguna forma el número de componentes de un ejemplar. Daremos algunas veces la eficiencia de nuestros algoritmos en términos del valor del ejemplar que estemos considerando en lugar de considerar su tamaño.

La ventaja de la aproximación teórica es que no depende ni de la computadora que se esté utilizando ni del lenguaje de programación ni siquiera de las habilidades del programador. Si deseamos medir la eficiencia de un algoritmo en términos del tiempo que necesita para llegar a una respuesta, entonces no existe una opción tan evidente. Se puede expresar en segundos, al no disponer de una computadora estándar a la cual referir todas las medidas.

Los factores para determinar el tiempo necesario del algoritmo son: 1. Implementación: Depende de:

Plataforma. Lenguaje. Compilador.

2. Tamaño del problema: Un problema grande necesitará más tiempo.

3. Contenido de los datos: Cómo vienen ordenados los datos.

Page 14: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 2 Curso 2007/08 Página 4 de 8

Al no poder referir todas las medidas en una computadora estándar tendremos este principio:

Principio de invarianza: Corresponde con el primer factor antes citado, que es la implementación. Definición: Dos implementaciones distintas de un mismo algoritmo no diferirán en su eficiencia en más de una constante multiplicativa. Si dos implementaciones del mismo algoritmo necesitan 푡 (푛) y 푡 (푛) segundos, respectivamente, para resolver un cado de tamaño n, entonces siempre existen constantes positivas c y d, tales que 푡 (푛) ≤ 푐 ∗ 푡 (푛) y 푡 (푛) ≤ 푑 ∗ 푡 (푛) siempre que n sea suficientemente grande:

Implementación 1 푡 (푛) 푡 (푛) ≤ 푐 ∗ 푡 (푛) Implementación 2 푡 (푛) 푡 (푛) ≤ 푑 ∗ 푡 (푛)

El principio sigue siendo cierto sea cual fuere la computadora utilizada para implementar el algoritmo. Para un problema más grande va a tardar lo mismo en la misma proporción, salvo por una constante multiplicativa, denominada en estos casos constante oculta.

2.4. Análisis de “caso medio” y “caso peor”

Analizaremos el coste de este algoritmo, que corresponde con el de la ordenación por inserción:

procedimiento insertar 푇([1. .푛]) para 푖 ← 2 hasta n hacer 푥 ← 푇[푖]; 푗 ← 푖 − 1; mientras 푗 > 0 y 푥 < 푇[푗] hacer 푇[푗 + 1] ← 푇[푗]; 푗 ← 푗 − 1; fmientras 푇[푗 + 1] ← 푥 fpara fprocedimiento

La nomenclatura empleada será: ←: Asignaciones. mientras .. fmientras, para .. fpara,…: Bucles mientras, para,…

Como norma general para analizar el coste realizaremos estos pasos: 1. Análisis del funcionamiento: Simularemos su funcionamiento, usando

para ello ejemplos de distintos valores de vectores, … 2. A continuación, analizaremos el coste propiamente dicho.

Page 15: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 2 Curso 2007/08 Página 5 de 8

En nuestro ejemplo tendremos lo siguiente, aunque lo retomaremos en el resumen del tema 7, no obstante lo veremos en este apartado:

1. Análisis del funcionamiento:

i

Parte ordenada j Parte desordenada

La idea básica es insertar un elemento dado en el lugar que le corresponda de una parte ordenada del vector. En un primer paso, se considera que la parte ordenada está formada por el primer elemento del vector. Entonces, se elige el elemento de la posición 2 y se inserta en la parte ordenada, de manera que si debe estar en la posición anterior se desplaza el elemento de la posición 1 hacia la derecha, haciéndole sitio, y se inserta en la posición 1. Como puede observarse se requerirá de una variable temporal que almacene el elemento a insertar. Seguidamente, se considera la parte del vector 푎[1] … 푎[2] ordenada, se elige el tercer elemento y se siguen las mismas opciones. Este método se repite hasta que el último elemento 푎[푛] haya sido tratado.

En resumen, el algoritmo puede describirse de la siguiente forma: - Tomar un elemento en la posición i. - Buscar en su lugar en las posiciones anteriores. - Mover hacia la derecha los restantes. - Insertarlo.

2. Análisis del coste: El algoritmo de ordenación por inserción es:

procedimiento insertar 푇([1. . 푛]) (1) para 푖 ← 2 hasta n hacer (2) 푥 ← 푇[푖]; 푗 ← 푖 − 1;

(3) mientras 푗 > 0 푦 푥 < 푇[푗] hacer

(4) 푇[푗 + 1] ← 푇[푗]푗 ← 푗 − 1

fmientras (5) 푇[푗 + 1] ← 푥 fpara fprocedimiento

Tendremos en cuenta estos tiempos: 푡 : Asignación 푡 : Comparación 푡 : Resta 푡 : Incremento 푡 : Acceso al vector

Page 16: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 2 Curso 2007/08 Página 6 de 8

Las distintas sentencias del algoritmo anterior son:

(1) 푡푎 + (푛 − 1) ∗ 푡푖 + 푛 ∗ 푡푐 siendo:

푡 : Asigna i el valor 2 (푖 ← 2). 푡 : Incrementa 푛 − 1 veces (de 2 a n). 푡 : Compara de 2 a n, incluido n, que sale del bucle.

(2) (2 ∗ 푡 + 푡 + 푡 ) ∗ (푛 − 1) siendo:

푡 : Significa que hay dos asignaciones (푥 ← 푇[푖] 푦 푗 ← 푖 − 1) 푡 : Indica acceso al vector (푇[푖]).

(3) (2 ∗ 푡 + 푡 ) ∗ 푁(푖,푇)

(4) (2 ∗ 푡 + 2 ∗ 푡 + 푡 + 푡 ) ∗ 푁(푖,푇) siendo:

푁(푖,푇): Número de iteraciones del bucle “mientras” en cada pasada del bucle “para”. Depende de i y del vector T, por lo que será variable.

El tiempo total del bucle “mientras” será la suma de (3) y (4):

(3) + (4) (3 ∗ 푡 + 2 ∗ 푡 + 2 ∗ 푡 + 푡 + 푡 ) ∗ 푁(푖,푇)

(5) (푡 + 푡 + 푡 ) ∗ (푛 − 1)

Hemos visto los tiempos que emplean. Luego analizaremos los casos peores y mejores, aunque el caso promedio ya veremos que no es tan fácil hallarlo, ya que requiere un cono cimiento a priori acerca de la distribución de los casos que hay que resolver. Esto suele ser un requisito poco realista. Normalmente, consideraremos el caso peor del algoritmo, esto es, para cada tamaño de caso sólo consideraremos aquéllos en los cuales el algoritmo requiera más tiempo, a no ser que se indique lo contrario. Suele ser más difícil analizar el comportamiento medio del algoritmo que hacerlo en el caso peor.

Page 17: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 2 Curso 2007/08 Página 7 de 8

2.5. ¿Qué es una operación elemental? Corresponderá también como el principio de invarianza con el primer factor visto previamente, recordemos que era la implementación. Definición: Una operación elemental es aquélla cuyo tiempo de ejecución puede ser acotada superiormente por una constante que sólo dependerá de la implementación particular usada: de la máquina, del lenguaje de programación, etc. De esta manera, la constante no depende ni del tamaño ni de los parámetros del ejemplar que se esté considerando. Pueden ser operaciones tales como sumas, restas, multiplicaciones, divisiones, accesos a un vector, operaciones booleanas, comparaciones. Veremos al igual que el anterior algoritmo esta parte en el tema 7, no obstante hay que hacer hincapié en la definición, porque se usa mucho y es importante.

Un ejemplo: Supongamos que cuando se analiza algún algoritmo, encontramos que para resolver un caso de un cierto tamaño se necesita efectuar a adiciones, m multiplicaciones y s instrucciones de asignación. Supongamos también que se sabe que una suma nunca requiere más de 푡 ms., que una multiplicación nunca requiere más de 푡 ms. y que una asignación nunca requiere más de 푡 ms., donde 푡 , 푡 y 푡 son constantes que dependen de la máquina utilizada. El tiempo total T requerido por nuestro algoritmo estará acotado por: 푇 ≤ 푎 ∗ 푡 + 푚 ∗ 푡 + 푠 ∗ 푡 ≤ max (푡 , 푡 , 푡 ) ∗ (푎 + 푚 + 푠) siendo: 푎,푚, 푠: Variables según la implementación. 푡 , 푡 , 푡 : Constantes que cambiarán de una implementación a otra. T estará acotado por un múltiplo constante del número de operaciones elementales (ejecutadas a coste unitario) que hay que ejecutar.

En conclusión, el primer factor del tiempo de ejecución visto anteriormente, que es la implementación no afectará mucho al coste del algoritmo.

Page 18: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 2 Curso 2007/08 Página 8 de 8

2.6. Más factores de tiempo En cuanto a los otros factores del tiempo de algoritmo trataremos los dos que nos quedan los siguientes (añadido del autor, tomando apuntes de otra persona). Recordemos que hemos visto previamente la implementación (apartado 2.3): El contenido de los datos:

En nuestro ejemplo de la ordenación por inserción tendremos estos casos:

1. Vector ya ordenado: i

Parte ordenada j Parte desordenada

Nunca se entrará en el bucle “mientras” (mientras 푗 > 0 y 푥 < 푇[푗]) por no cumplirse la condición de 푥 < 푇[푗]. Por tanto, 푵(풊,푻) = ퟎ.

Recordemos que 푁(푖,푇) es el número de iteraciones del bucle “mientras” en cada pasada del bucle “para”.

2. Vector completamente desordenado (de mayor a menor): Se recorrerán todos los elementos de la parte desordenada hasta encontrar su posición. En este caso, 푵(풊,푻) = 풊.

Tamaño del problema: Siguiendo el ejemplo anterior tendremos:

(1) Coste n (2) Coste n

Coste - Caso mejor 0

(3) + (4) - Caso peor ∑ 푖

(5) Coste n

Caso mejor: Vector ya ordenado. Tiene coste lineal (n). Caso peor: Vector completamente desordenado.

푛 + ∑ 푖 ≈ 푐표푠푡푒 푛 (∑ 푖 = ∗( ) ≈ 푛 )

Caso promedio: En general haremos el análisis para el caso peor. No es normal hacerlo para el caso promedio.

Page 19: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Alumna: Alicia Sánchez Centro: UNED-Las Rozas (Madrid)

Resumen de programación 3

Tema 3. Notación asintótica.

Page 20: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 3 Curso 2007/08 Página 2 de 8

Índice:

3.1. Introducción ……………………………………………………... 3 3.2. Una notación para “el orden de” ………………………………… 3 3.3. Otra notación asintótica.………………………………………….. 7

Bibliografía: Se ha tomado apuntes del libro:

Fundamentos de algoritmia. G. Brassard y P. Bratley

Page 21: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 3 Curso 2007/08 Página 3 de 8

Este tema al igual que ha pasado con los anteriores resumirlo cuesta, ya que en muchas ocasiones se añaden extras para completar el tema que cuesta integrarlos. Por ello, habrá algunas cosas que no queden del todo claro, para ello se debería mirar en el libro, aunque de nuevo se ha tratado de poner los conceptos más importantes.

3.1. Introducción. Recuérdese que deseamos determinar matemáticamente la cantidad de recursos que necesita el algoritmo en función del tamaño (o a veces del valor) de los casos considerados. Dado que no existe una computadora estándar con la cual se puedan comparar las medidas de tiempo de ejecución, vimos también en el apartado 2.3 que nos contentaremos con expresar el tiempo requerido por el algoritmo salvo una constante multiplicativa (denominada constante oculta). Esta notación se denomina asintótica porque trata acerca del comportamiento de funciones en el límite, esto es, para valores suficientemente grandes de su parámetro.

3.2. Una notación para “el orden de”

Tendremos una función arbitraria de los números naturales en los reales no negativos, tal como 푡(푛) = 27 ∗ 푛 + 355/133 ∗ 푛 + 12. Se puede pensar que n representa el tamaño del ejemplar sobre el cual es preciso que se aplique un algoritmo dado y en 푡(푛) como representante de la cantidad de un recurso dado que se invierte en ese ejemplar por una representación particular de este algoritmo.

Podría ser que la implementación invirtiera 푡(푛) ms. o quizá 푡(푛) represente la cantidad de espacio. Tal como se ha visto, la función 푡(푛) puede muy bien depender de la implementación más que únicamente del algoritmo. Recuerde el principio de invarianza que dice que la razón de los tiempos de ejecución de dos implementaciones diferentes del mismo algoritmo siempre está acotada por encima y por debajo mediante constantes predeterminadas.

Consideremos una función 푓:ℕ → ℝ como 푓(푛) = 푛 . Diremos que 푡(푛) está en el orden de 푓(푛) si 푡(푛) está acotada superiormente por un múltiplo real positivo de 푓(푛) para todo n suficientemente grande. Matemáticamente, esto significa que existe una constante real y positiva c y un umbral 푛 , tal que 푡(푛) ≤ 푐 ∗ 푓(푛), siempre que 푛 ≥ 푛 .

Por ejemplo, considérese las funciones 푓(푛) y 푡(푛) definidas anteriormente. Está claro que tanto 푛 ≤ 푛 como 1 ≤ 푛 , siempre que 푛 ≥ 1. Por tanto, siempre y cuando 푛 ≥ 1 tendremos:

푡(푛) = 27 ∗ 푛 + ∗ 푛 + 12 ≥ 27 ∗ 푛 + ∗ 푛 + 12 ∗ 푛 = 42 ∗ ∗

푛 = 42 ∗ ∗ 푓(푛).

Tomando 푐 = 42 ∗ 16/113 푦 푛 = 1. Concluiremos que 푡(푛) es del orden de 푓(푛) por cuanto 푡(푛) ≤ 푐 ∗ 푓(푛), siempre que 푛 ≥ 푛 .

Page 22: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 3 Curso 2007/08 Página 4 de 8

De esta manera, si la implementación de un algoritmo requiere en el caso peor un tiempo de 27 ∗ 푛 + 355/133 ∗ 푛 + 12 ms. Para resolver un caso de tamaño n, podríamos simplificar diciendo que el tiempo está en el orden de 푛 . Hay algo más importante: el orden de no solamente indica el tiempo requerido por una implementación particular del algoritmo, sino también (por el principio de invarianza) el requerido por cualquier implementación. Por tanto, tenemos derecho a afirmar que el algoritmo en sí requiere un tiempo que está en el orden de n2 o que requiere un tiempo cuadrado (independiente de la implementación).

Es conveniente disponer de un símbolo matemático para representar el orden de. Sea 푓:ℕ → ℝ una función arbitraria de los números naturales en los reales no negativos. Le indicará mediante 푂 푓(푛) el conjunto de todas las funciones 푡:ℕ → ℝ tales que 푡(푛) ≤ 푐 ∗ 푓(푛), para todo 푛 ≥ 푛 para una constante positiva c y un umbral entero 푛 . En otras palabras:

푂 푓(푛) ≡ {푡:ℕ → ℝ |∃ 푐 ∈ ℝ ,푛 ∈ ℕ,∀푛 ≥ 푛 |푡(푛) ≤ 푐 ∗ 푓(푛)}

Gráficamente sería:

푡 푓(푛)

푡(푛)

푛 (umbral) 푛

siendo: 푛 : Cierto umbral del tamaño del problema. 푓(푛): Acota superiormente a la función 푡(푛).

Deduciendo del grafico anterior, podremos decir que habrá un cierto umbral que permitirá acotar superiormente el problema.

Ejemplo: Para representar que n2 es del orden de n3 lo haremos así: 푛 ∈ 푂(푛 )

Como en teoría de conjuntos, se usa el símbolo ∈. El umbral de 푛 suele resultar útil para simplificar argumentos, pero nunca es necesario para funciones estrictamente positivas. Sean 푓, 푡:ℕ → ℝ dos funciones de los números naturales en los reales estrictamente positivos. La regla del umbral afirma que 푡(푛) ∈ 푂 푓(푛) si y sólo si existe una constante real positiva, tal que 푡(푛) ∈ 푂 푓(푛) para cada número natural n.

Page 23: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 3 Curso 2007/08 Página 5 de 8

Una regla útil para demostrar que una función es del orden de otra es la regla del máximo. Sea 푓, 푔:ℕ → ℝ dos funciones arbitrarias de los números naturales en los reales no negativos, La regla del máximo dice que 푂 푓(푛),푔(푛) = 푂 máx 푓(푛),푔(푛) .

Más específicamente, sean 푝, 푞:ℕ → ℝ definidas para todo número natural n mediante 푝(푛) = 푓(푛) + 푔(푛) y 푞(푛) = máx 푓(푛),푔(푛) y consideremos una función arbitraria 푡:ℕ → ℝ , la regla del máximo dice que 푡(푛) ∈ 푂 푓(푛) si y sólo si (⟺) 푡(푛) ∈ 푂 푔(푛) .

Ejemplo: Consideremos un algoritmo que se realiza en tres pasos: Inicialización, procesamiento y finalización. Supongamos que estos pasos requieren un tiempo de 푂(푛 ),푂(푛 ) y 푂(푛 ∗ log(푛)), respectivamente. Queda claro que el algoritmo completo requiere un tiempo 푂(푛 + 푛 + 푛 ∗ log(푛)). Por la regla del máximo tendríamos:

푂(푛 + 푛 + 푛 ∗ log(푛)) = 푂 푚á푥(푛 , 푛 , 푛 ∗ log(푛)) = 푂(푛 )

Aún cuando el tiempo requerido por el algoritmo sea lógicamente la suma de los tiempos requeridos por sus partes separadas, veremos que lo determinará la parte que más consuma, siempre y cuando el número de partes sean constantes, independientemente del tamaño de la entrada.

Demostración: Demostraremos la regla del máximo para el caso de dos funciones. Observamos que: 푓(푛) + 푔(푛) = 푚푖푛 푓(푛),푔(푛) + 푚á푥 푓(푛),푔(푛) y 0 ≤ 푚푖푛 푓(푛),푔(푛) ≤ 푚á푥 푓(푛),푔(푛)

Se sigue entonces que:

푚á푥 푓(푛),푔(푛) ≤ 푓(푛) + 푔(푛) ≤ 2 ∗ 푚á푥 푓(푛),푔(푛)

De lo que podemos deducir que:

- La cota inferior indica que una de las dos funciones será máxima con respecto a la otra. En este caso, sería la función máxima 푓(푛) 표 푔(푛) .

- La cota superior es cuando las dos funciones son máximas.

Considérese ahora cualquier 푡(푛) ∈ 푂 푓(푛) + 푔(푛) . Sea c una constante adecuada tal que 푡(푛) ≤ 푐 ∗ 푓(푛) + 푔(푛) para todo n suficientemente grande. Se sigue que 푡(푛) ≤ 2푐 ∗ máx 푓(푛) + 푔(푛) . Por tanto, 푡(푛) está acotado

superiormente por un múltiplo real y positivo (2푐) de máx 푓(푛) + 푔(푛) , para todo n suficientemente grande, lo cual demuestra que 푡(푛) ∈ máx 푓(푛) + 푔(푛) .

Page 24: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 3 Curso 2007/08 Página 6 de 8

Mostraremos varios ejemplos de uso incorrecto de la regla del máximo:

1. El siguiente razonamiento es erróneo:

푂(푛) = 푂(푛 + 푛 − 푛 ) = 푂 máx(n, n , n ) = 푂(푛 )

Hemos usado la regla del máximo con alguna de las funciones cuando son negativas con infinita frecuencia.

2. Tenemos esta función 푡(푛) = 12푛 ∗ log(푛)− 5푛 + 푙표푔 (푛) + 36

Razonando incorrectamente tendremos:

푂 푡(푛) = 푂 푚á푥(12푛 ∗ log(푛), − 5푛 , log (푛) + 36) =푂 푛 ∗ log (푛) .

No usamos esta regla correctamente por ser −5푛 negativo. El razonamiento correcto sería:

푂 푡(푛) = 푂(11푛 ∗ log(푛) + 푛 ∗ log(푛) − 5푛 + log (푛) +36) = 푂 푚á푥(11푛 ∗ log(푛) , 푛 ∗ log(푛) , −5푛 + log (푛) +36) = 푂 푛 ∗ log(푛) .

Por último, falta por decir que no especificamos la base del algoritmo dentro de la notación asintótica, porque podremos emplear las operaciones de los logaritmos para cambiar la base sin que afecte al tiempo.

Propiedades de el orden de:

Reflexiva: 푓(푛) ∈ 푂 푓(푛) para toda función 푓:ℕ → ℝ .

Por ejemplo: 푓(푛) ≤ 2 ∗ 푓(푛)

Transitiva: 푓(푛) ∈ 푂 푔(푛) 푦 푔(푛) ∈ 푂 ℎ(푛) ⇒ 푓(푛) ∈ 푂 ℎ(푛) .

La demostración será:

∃ 푐 ∈ ℝ , 푛 ∈ ℕ ∀ 푛 > 푛 , 푓(푛) = 푐 ∗ 푔(푛) Por definición.

∃ 푑 ∈ ℝ , 푛 ∈ ℕ ∀ 푛 > 푛, , 푔(푛) = 푑 ∗ ℎ(푛) Por definición.

푓(푛) ≤ 푐 ∗ 푔(푛) ≤ 푐 ∗ 푑 ∗ ℎ(푛) 푓(푛) ∈ 푂 ℎ(푛)

El primer ≤ se cumple por la expresión 푛 ≥ 푛 y el segundo por la siguiente expresión 푛 ≥ max (푛, 푛, ). Deducimos que existirá, por tanto, un umbral tal que quede acotado el valor.

Page 25: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 3 Curso 2007/08 Página 7 de 8

Para demostrar que una función dada no pertenece al orden de otra función 푓(푛) tendremos estas formas: Demostración por contradicción: Es la forma más sencilla. Consiste en

demostrar la veracidad de una sentencia demostrando que su negación da lugar a una contradicción.

La regla del umbral generalizado: Implica la existencia de una constante real y positiva 푐 tal que 푡(푛) ≤ 푐 ∗ 푓(푛) para todos los 푛 ≥ 1 (tomaremos 푛 como 1, nos interesa más la definición dada por la regla del umbral sin generalizar).

La regla del límite: Lo definiremos completamente tras analizar la cota superior y el coste exacto.

3.3. Otra notación asintótica. La notación Omega: Necesitamos tener una notación dual para cotas inferiores. Esto es la notación 훺. Considérese una vez más dos funciones 푡, 푓:ℕ → ℝ de los números naturales en los números reales no negativos. Diremos que 푡(푛) está en Omega (훺) de 푓(푛), lo cual se denota como 푡(푛) ∈ 훺 푓(푛) , si 푡(푛) está acotada inferiormente por un múltiplo real positivo de 푓(푛) para todo n suficientemente grande.

Matemáticamente, esto significa que existe una constante real positiva 푑 y un umbral entero 푛 tal que 푡(푛) ≥ 푑 ∗ 푓(푛) siempre que 푛 ≥ 푛 .

훺 푓(푛) ≡ {푡:ℕ → ℝ |∃ 푑 ∈ ℝ ,푛 ∈ ℕ,∀ 푛 ≥ 푛 |푡(푛) ≥ 푑 ∗ 푓(푛)}

Gráficamente sería:

푡 푡(푛) ≈ 푛 푡(푛) ∈ 훺 푓(푛) : Cota inferior.

푓(푛) ∈ 푂 푡(푛) : Cota superior.

푓(푛)

푛 (umbral) 푛

La regla de la dualidad se definirá así:

푡(푛) ∈ 훺 푓(푛) ⇔ 푓(푛) ∈ 푂 푡(푛)

Demostramos la implicación de la derecha ( ⇒)

⇒∃ 푐 ∈ ℝ , 푛 ∈ ℕ,∀ 푛 ∈ 푛 . 푡(푛) ≥ 푐 ∗ 푓(푛)

⇒ ∗ 푡(푛) ≥

푓(푛) ⇒푓(푛) ≤ ∗ 푡(푛).

Por la definición, deducimos que 푓(푛) ∈ 푂 푡(푛) .

Page 26: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 3 Curso 2007/08 Página 8 de 8

La notación Theta: Diremos que 푡(푛) está en Theta de 푓(푛), o lo que es igual que 푡(푛) está en el orden exacto de 푓(푛) y lo denotamos 푡(푛) ∈ 휃 푓(푛) , si 푡(푛) pertenece tanto a 푂 푓(푛) como a Ω 푓(푛) .

La definición formal de 휃 es:

휃 푓(푛) = 푂 푓(푛) ∩ Ω 푓(푛) .

Por tanto,

휃 푓(푛) ≡ {푡:ℕ → ℝ |∃ 푎,푏 ∈ ℝ , 푛 ∈ ℕ,∀푛 ≥ 푛 |

푎 ∗ 푓(푛) ≤ 푡(푛) ≤ 푏 ∗ 푓(푛)}. Decimos que el conjunto del orden exacto está acotado tanto inferior como superiormente por 푓(푛). Podemos probarlo tanto por la definición como por la regla del límite, aunque preferiremos en los ejercicios hacerlo por este segundo método.

La regla del límite: Nos permite comparar dos funciones en cuanto a la notación asintótica se refiere. Tendremos que calcular el siguiente límite:

lim →( )( )

.

Al resolver el limite se nos darán 3 posibles resultados:

1. lim →( )( )

= 푐 ∈ 푅 ⇒

푓(푛) ∈ 푂 푔(푛) 푓(푛) ∈ 훺 푔(푛) 푓(푛) ∈ 휃 푔(푛) 푔(푛) ∈ 푂 푓(푛) 푔(푛) ∈ 훺 푓(푛) 푔(푛) ∈ 휃 푓(푛)

.

Estas funciones se comportan igual, diferenciándose en una constante multiplicativa.

2. lim →( )( )

= ∞ ⇒

푓(푛) ∉ 푂 푔(푛) 푓(푛) ∈ 훺 푔(푛) 푓(푛) ∉ 휃 푔(푛) 푔(푛) ∈ 푂 푓(푛) 푔(푛) ∉ 훺 푓(푛) 푔(푛) ∉ 휃 푓(푛)

.

Por muy alta que sea la constante multiplicativa de 푔(푛) nunca superará a 푓(푛).

3. lim →( )( )

= 0 ⇒

푓(푛) ∈ 푂 푔(푛) 푓(푛) ∉ 훺 푔(푛) 푓(푛) ∉ 휃 푔(푛) 푔(푛) ∉ 푂 푓(푛) 푔(푛) ∈ 훺 푓(푛) 푔(푛) ∉ 휃 푓(푛)

.

푔(푛) crece más exponencialmente que 푓(푛), por lo que sería su cota superior.

Page 27: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Alumna: Alicia Sánchez Centro: UNED-Las Rozas (Madrid)

Resumen de programación 3

Tema 4. Análisis de algoritmos.

Page 28: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 4 Curso 2007/08 Página 2 de 12

Índice:

4.1. Introducción ……………………………………………………... 3 4.2. Análisis de las estructuras de control ……………………………. 3

4.2.1. Secuencias .………………………………………………… 3 4.2.2. Sentencia condicional (if) ………………………………….. 4 4.2.3. Bucles “para” (desde) ……………………………………… 4 4.2.4. Llamadas recursivas ………………………………………. . 7 4.2.5. Bucles “mientras” (while) y “repetir” (repeat) …………….. 9

4.3. Uso de un barómetro ………………………………………….… 10 4.5. Análisis del caso medio …………………………………………. 11 4.6. Resolución de recurrencias ……………………………………… 12

Bibliografía: Se ha tomado apuntes de los libros:

Fundamentos de algoritmia. G. Brassard y P. Bratley Estructuras de Datos y Algoritmos. R. Hernández

Page 29: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 4 Curso 2007/08 Página 3 de 12

Este tema es el último de los pertenecientes a costes y de los que más ejercicios cortos han entrado en exámenes. Por ello, merece la pena pararse sobre todo en las fórmulas para hallar el coste de la recursividad. Nos saltamos el apartado 4.4, que trata de ejemplos adicionales, ya que los iremos incluyendo en el resto del tema. 4.1. Introducción.

El objetivo principal de este libro es enseñar a diseñar algoritmos eficientes. Para resolver el mismo problema con varios algoritmos es preciso decidir cuál de ellos es el más adecuado para la aplicación considerada, recordemos que eso lo hemos visto en el tema 1. Una herramienta esencial para este propósito es el análisis de los algoritmos, aunque no tendremos una fórmula mágica para hallar la eficiencia de los algoritmos.

Existen técnicas básicas que suelen resultar útiles, tales como saber la forma de enfrentarse a estructuras de control y a ecuaciones de recurrencia, que será lo que tratemos en este capítulo. Añadiremos en este capítulo el análisis del bucle “if” para completarlo aun más.

4.2. Análisis de las estructuras de control.

El análisis de los algoritmos suele efectuarse desde dentro hacia fuera. Seguiremos estos pasos:

- En primer lugar, se determina el tiempo requerido por las instrucciones individuales (suele estar acotado por una constante).

- Después se combinan estos tiempos de acuerdo con las estructuras del programa.

En esta sección ofreceremos unos principios generales que resultan útiles en aquellos análisis relacionados con las estructuras de control de uso más frecuente, así como ejemplos de la aplicación de estos principios (para mientras,…).

4.2.1. Secuencias.

Sean 푃 y 푃 dos fragmentos de un algoritmo (instrucciones o subalgoritmos). Sean 푡 y 푡 los tiempos requeridos por 푃 y 푃 , respectivamente. Estos tiempos pueden depender de distintos parámetros tales como el tamaño del caso.

La regla de la composición secuencial dice que el tiempo necesario para calcular "푃 ; 푃 ", esto es, primero 푃 y después 푃 , es simplemente 푡 + 푡 . Por la regla del máximo este tiempo está en 휃 푚á푥(푡 , 푡 ) , es decir, como vimos previamente el coste del algoritmo lo determinará el más ineficiente. Ejemplo:

푃 → 푡 (푛)푃 → 푡 (푛) 푡(푛) = 푡 (푛) + 푡 (푛)

siendo: 푡 (푛): Lo que cuesta la primera función.

푡 (푛): Lo que cuesta la segunda función.

Page 30: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 4 Curso 2007/08 Página 4 de 12

El coste del algoritmo, siguiendo la regla del máximo será: 푡(푛) ∈ 푂 푚á푥 푡 (푛), 푡 (푛)

4.2.2. Sentencia condicional (if). Este añadido es del autor. Tenemos la siguiente sentencia condicional:

si B entonces 푆 si no 푆 fsi

Tenemos estos costes:

si 퐵 → 휃 푓 (푛) si 푆 → 휃 푓 (푛) si 푆 → 휃 푓 (푛)

La cota superior será: 푂 푚á푥 푓 (푛), 푓 (푛), 푓 (푛) (camino máximo).

La cota inferior será: 훺 푚푖푛 푓 (푛), 푓 (푛), 푓 (푛) (camino mínimo).

4.2.3. Bucles “para” (desde).

Los bucles (lazos) “para” (desde) son los más fáciles de analizar. Considérese el bucle siguiente:

para 푖 ← 1 hasta m hacer 푃(푖)

Se nos dan varios casos:

a. El tiempo 푡(푛) requerido por 푃(푖) no depende realmente de i, aún cuando pudiera depender del tamaño del ejemplar o del ejemplar en sí. Es el caso más sencillo.

En este caso, el tiempo total requerido por el bucle es simplemente 푙 = 푚 ∗ 푡 o bien 푇(푛) = 푚 ∗ 푡(푛). Se harían n iteraciones y cada uno con el mismo coste. Sería algo así como este bucle “mientras”:

i ← 1; mientras 푖 < 푚 hacer

푃(푖); 푖 ← 푖 + 1;

Asignaremos costes unitarios (operaciones elementales) a la comprobación 푖 < 푚, a las instrucciones i ← 1 e 푖 ← 푖 + 1 y a las instrucciones de salto implícitas en el bucle “mientras”.

Se demuestra que este tiempo al hacer las operaciones está acotado superiormente por 푚 ∗ 푡, tal como habíamos escrito antes.

Page 31: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 4 Curso 2007/08 Página 5 de 12

b. El tiempo 푡(푛) por 푃(푖) varía como función de i. Si despreciamos el tiempo requerido por el bucle de control, para 푚 ≥ 1, entonces ese mismo bucle “para” requiere un tiempo que está dado por una suma ∑ 푡(푖). Sería entonces 푇(푛) = ∑ 푡(푖, 푛) el coste del bucle.

Ejemplo: Analizamos el coste de este bucle “para”:

funcion fibiter(n) 푖 ← 1; 푗 ← 0;

para 푘 ← 1 hasta 푛 hacer 푗 ← 푖 + 푗; 푖 ← 푗 − 푖;

fpara devolver j

ffuncion Se nos darán dos casos en función de los costes de las operaciones aritméticas:

1. Las operaciones aritméticas se consideran como de coste unitario. Las instrucciones de dentro del bucle “para” requieren un tiempo constante. Suponemos que el tiempo requerido por estas instrucciones está acotado superiormente por alguna constante c. El tiempo requerido por el bucle “para” está acotado superiormente por n veces esta constante: n*c.

El algoritmo tiene coste 휽(풏).

2. Las operaciones aritméticas no se consideran como de coste unitario. Podemos llegar a ver como al paso del bucle se vuelve costosa una instrucción tal como 푗 ← 푖 + 푗, por haber operandos muy grandes.

Sea c una constante tal que este tiempo está acotado superiormente por 푐 ∗ 푘 para todo 푘 ≥ 1. Si despreciamos el tiempo requerido por el control del bucle y las instrucciones que preceden al bucle concluiremos que el tiempo requerido por el algoritmo está acotado superiormente por:

∑ 푐 ∗ 푘 = 푐 ∗ ∑ 푘 = 푐 ∗ ∗( ) ∈ 푂(푛 )

Un razonamiento similar indica que este tiempo se encuentra en Ω(푛 ) y se deduce, por tanto, que está en 휽(풏ퟐ).

El análisis del bucle “para” que empiezan en un valor que no sea 1 o que avanzan con pasos mayores, debería resultar evidente.

Ejemplo: Se nos da el siguiente bucle:

para 푖 ← 5 hasta m paso 2 hacer 푃(푖)

Aquí, 푃(푖) se ejecuta (푚 − 5) ÷ 2 + 1 veces siempre que 푚 ≥ 3.

Page 32: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 4 Curso 2007/08 Página 6 de 12

Ejemplo completo de análisis de un algoritmo con bucle “para”. Para ello, analizaremos la ordenación por selección:

procedimiento seleccionar 푇(1. . 푛) para 푖 ← 1 hasta 푛 − 1 hacer

푚푖푛푗 ← 1; 푚푖푛푘 ← 푇[푖]; para 푗 ← 푖 + 1 hasta 푛 hacer

si 푇[푗] < 푚푖푛푥 entonces 푚푖푛푗 ← 푗; 푚푖푛푘 ← 푇[푗];

fsi fpara

푇[푚푖푛푗] ← 푇[푖]; 푇[푖] ← 푚푖푛푥; fpara

fprocedimiento Seguiremos estos pasos:

1. Análisis de su funcionamiento: Dentro del bucle para “exterior” tendremos uno “interior” que nunca sabremos si realiza las mismas instrucciones con el mismo coste. Estaremos en el caso b) de los vistos anteriormente.

i

Parte ordenada Parte desordenada

La idea básica es seleccionar el menor elemento de una parte desordenada del vector y colocarlo en la posición del primer elemento no ordenado. En un primer paso, se recorre el vector hasta encontrar el elemento menor. Para ello se coloca el primer elemento en una variable temporal y se va comparando con los demás elementos del vector tal que si se encuentra uno menor se asigna a la variable temporal. Recorrido todo el vector, el elemento de la variable temporal (que será el menor) se intercambia con el de la primera posición (el primero escogido de la parte desordenada). Seguidamente, se considera únicamente la parte del vector no ordenado y se repite el proceso de búsqueda del menor, y así sucesivamente. Se puede describir de la siguiente manera:

Seleccionar el elemento menor de la parte del vector no ordenada.

Colocarlo en la primera posición de la parte no ordenada del vector.

El coste es independiente de cómo vienen ordenados los datos, es decir, del contenido de los datos.

Page 33: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 4 Curso 2007/08 Página 7 de 12

2. Análisis del coste:

Veremos el número de instrucciones y analizaremos el coste del algoritmo en el caso peor, como es habitual:

procedimiento seleccionar 푇(1. . 푛)

(1) para 푖 ← 1 hasta 푛 − 1 hacer

푚푖푛푗 ← 1; 푚푖푛푘 ← 푇[푖];

(2) para 푗 ← 푖 + 1 hasta 푛 hacer

si 푇[푗] < 푚푖푛푥 entonces

(3) 푚푖푛푗 ← 푗;

푚푖푛푘 ← 푇[푗]; fsi

fpara

푇[푚푖푛푗] ← 푇[푖]; 푇[푖] ← 푚푖푛푥;

fpara fprocedimiento

Suponemos que las operaciones de suma, asignación,… son elementales, por tanto, no las consideraremos en el coste total del algoritmo. Pasamos a ver el número de instrucciones en cada paso:

(1) n iteraciones ≈ coste n (2) (푛 − 푖) iteraciones

Tendremos que el bucle “para” interior y el “si” tendrán el siguiente número de instrucciones:

(2) + (3) 푇(푛) ≈ ∑ (푛 − 푖).

Resolviendo la sucesión, tendremos que

푇(푛) ≈ ∑ (푛 − 푖) = (푛 − 1) ∗ (푛 − 2) ∗ … ∗ 1 ≈ 푛 .

Como conclusión, el coste en todos los casos es 푶(풏ퟐ).

4.2.4. Llamadas recursivas.

El análisis de algoritmos recursivos suele ser sencillo. Una inspección sencilla del algoritmo suele dar lugar a una ecuación de recurrencia que imita el flujo de control dentro del algoritmo. Una vez que se ha obtenido la ecuación de recurrencia, se pueden aplicar las técnicas generales para resolverlas.

Page 34: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 4 Curso 2007/08 Página 8 de 12

Ejemplo: Considérese el algoritmo recursivo siguiente:

funcion fibrec (n) si 푛 < 2 entonces devolver n si no devolver fibrec (푛 − 1) + fibrec (푛 − 2)

Sea 푇(푛) el tiempo requerido por una llamada a fibrec (푛):

- Si 푛 < 2, el algoritmo devuelve simplemente n, lo cual requiere un tiempo constante a.

- En caso contrario, la mayor parte del trabajo se invierte en dos llamadas recursivas, que requieren un tiempo 푇(푛 − 1) y 푇(푛 − 2), respectivamente.

Sea ℎ(푛) el trabajo implicado en esta suma y en este control, es decir, el tiempo requerido por una llamada a fibrec(푛) ignorando los tiempos invertidos dentro de las dos llamadas recursivas. Por definición de 푇(푛) y de ℎ(푛), obtenemos la siguiente recurrencia:

푎 Si 푛 = 0 ó 푛 = 1 (푛 < 2)

푇(푛) = 푇(푛 − 1) + 푇(푛 − 2) + ℎ(푛) En caso contrario

Trataremos, por tanto, estos casos:

- Si contamos las sumas con coste unitario, ℎ(푛) está acotado por una constante y la ecuación de recurrencia es similar a la ya encontrada antes. Tenemos que 푇(푛) ∈ 휃 푓(푛) , razonando de manera similar para la cota inferior. Concluimos, entonces, que fibrec (푛) requiere un tiempo exponencial en n.

- Si no se cuentan las adiciones con un coste unitario, ℎ(푛) ya no queda acotado por una constante. ℎ(푛) está dominado por la adición de 푓 y 푓 para n suficientemente grande. La adición requiere un tiempo ℎ(푛) ∈ 휃 푓(푛) .

Deducimos que el resultado es el mismo independientemente de si ℎ(푛) es constante o lineal: 푇(푛) ∈ 휃 푓(푛) . La única diferencia es la constante multiplicativa oculta en la notación 휃.

Page 35: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 4 Curso 2007/08 Página 9 de 12

4.2.5. Bucles “mientras” (while) y “repetir” (repeat).

Para analizar estos bucles no existe una forma evidente a priori de saber cuántas veces tendremos que pasar por el bucle. Podremos emplear dos técnicas:

- Es la técnica estándar. Es hallar una función de las variables implicadas cuyo valor se decremente en cada pasada.

- Para determinar el número de veces que se repite el bucle necesitamos conocer mejor la forma en que disminuye el valor de esta función. Para analizar el bucle “mientras” de manera alternativa consiste en tratarlo como un algoritmo recursivo.

Ejemplo: Estudiaremos con detalle el algoritmo de búsqueda binaria.

funcion busq_binaria(푇[1. . 푛],푥) 푖 ← 1; 푗 ← 푛; mientras 푖 < 푗 hacer

푘 ← (푖 + 푗) ÷ 2; caso_de 푥 < 푇[푘]: 푗 ← 푘 − 1; 푥 = 푇[푘]: 푖, 푗 ← 푘; {푑푒푣표푙푣푒푟 푘} 푥 > 푇[푘]: 푖 ← 푘 + 1; fcaso

fmientras dev i;

ffuncion

Analizaremos el algoritmo siguiendo los pasos anteriores: 1. Análisis del funcionamiento:

j i

k

La idea básica que subyace a la búsqueda binaria es comparar x con el elemento k que está en la posición media de T. El objetivo de la búsqueda binaria es hallar un elemento x de un vector 푇[1. .푛] que está ordenado de modo no decreciente (sólo podremos aplicar la búsqueda binaria si este vector está ordenado así). Supongamos por sencillez que está garantizado que x aparece al menos una vez en T. Se nos pide buscar un entero i tal que 1 ≤ 푖 ≤ 푛 y 푇[푖] = 푥.

Como el elemento k está en el centro del vector se dan tres casos: - Elemento x está a la izquierda de k. - Elemento x coincide con k. - Elemento x está a la derecha de k.

Page 36: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 4 Curso 2007/08 Página 10 de 12

Mediante sucesivas divisiones por 2 llegaremos hasta que 푖 = 푗. En el primer caso, en el que x sea menor que la mitad moveremos el puntero j a la mitad izquierda. En el último caso, moveremos el puntero i. El caso intermedio, sería ya la solución, encontrando el elemento x.

Para resolverlo definiremos d como el número de posiciones donde podrá ir el elemento:

푑 = 푗 − 푖 + 1 En el paso inicial, consideramos las n posiciones:

푑 = − 1 − 푖 + 1 = ≈ .

Hemos sustituido j por su mitad, es decir, 푗 = 푘 − 1 = para el caso 푥 < 푇[푘] (elemento en la mitad izquierda).

El significado de las variables hasta el momento es el siguiente:

d: Representa los valores de 푗 − 푖 + 1 antes del bucle “mientras”. 푑: Representa el número de iteraciones tras finalizar el bucle, antes de la siguiente iteración.

Para el caso 푥 > 푇[푘] (elemento en la mitad derecha), tenemos que 푘 = . Sustituyendo, como vimos previamente:

푑 = 푗 − − 1 + 1 = ≈ .

El coste será 푶 풍풐품(풏) , porque siempre se divide por 2 para buscar el elemento.

4.3. Uso de un barómetro.

Definición: Una instrucción barómetro es aquélla que se ejecuta por lo menos con tanta frecuencia como cualquier otra instrucción del algoritmo.

Siempre que el tiempo requerido por cada instrucción esté acotado por una constante, el tiempo requerido por el algoritmo completo es del orden exacto del número de veces que se ejecuta la instrucción barómetro.

Page 37: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 4 Curso 2007/08 Página 11 de 12

Ejemplo: analizaremos el siguiente algoritmo:

funcion fibiter(n) 푖 ← 1; 푗 ← 0;

para 푘 ← 1 hasta 푛 hacer 푗 ← 푖 + 푗; 푖 ← 푗 − 푖;

fpara devolver j

ffuncion

Podremos considerar que la instrucción “푗 ← 푖 + 푗” se puede tomar como un barómetro, por ser ejecutada un número de veces igual a n. Se ve entonces que el algoritmo requiere un tiempo que está en 휃(푛).

Cuando un algoritmo contiene varios bucles anidados, toda instrucción del bucle más interno puede utilizarse en general como barómetro. Sin embargo, hay que hacer esto con cuidado, porque hay casos en los que es preciso tener en consideración el control implícito del bucle. Esto sucede cuando algunos de los bucles se ejecutan cero veces.

4.5. Análisis del caso medio. Requiere suponer a priori una distribución de probabilidad para los casos en que se pedirá que resuelva nuestro algoritmo.

Page 38: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 4 Curso 2007/08 Página 12 de 12

4.6. Resolución de recurrencias.

Tendremos dos tipos:

- Reducción por sustracción:

La ecuación de la recurrencia es la siguiente:

푐 ∗ 푛 si 0 ≤ 푛 < 푏 푇(푛) =

푎 ∗ 푇(푛 − 푏) + 푐 ∗ 푛 si 푛 ≥ 푏 La resolución de la ecuación de recurrencia es: 휃(푛 ) si 푎 < 1 푇(푛) = 휃(푛 ) si 푎 = 1

휃 푎 si 푎 > 1

- Reducción por división:

La ecuación de la recurrencia es la siguiente:

푐 ∗ 푛 si 1 ≤ 푛 < 푏 푇(푛) =

푎 ∗ 푇(푛/푏) + 푐 ∗ 푛 si 푛 ≥ 푏

La resolución de la ecuación de recurrencia es: 휃(푛 ) si 푎 < 푏 푇(푛) = 휃 푛 ∗ 푙표푔(푛) si 푎 = 푏

휃 푛 si 푎 > 푏

siendo:

a: Número de llamadas recursivas. b: Reducción del problema en cada llamada. 풄 ∗ 풏풌: Todas aquellas operaciones que hacen falta además de las de recursividad.

Page 39: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Alumna: Alicia Sánchez Centro: UNED-Las Rozas (Madrid)

Resumen de programación 3

Tema 5. Estructuras de datos.

Page 40: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 2 de 41

Índice:

5.1. Matrices (arrays), pilas y colas. …………………………………... 3 5.2. Registros y punteros (apuntadores) .……………………………… 7 5.3. Listas ………………………………………….………………….. 8 5.4. Grafos …………………………………………………………… 11 5.5. Árboles …………………………………………………………... 16 5.6. Tablas asociativas ……………………………………………….. 19 5.7. Montículos (heaps) ………………………………………………. 20 5.8. Montículos binomiales …………………………………………... 33 5.9. Particiones ……………………………………………………….. 34

Bibliografía: Se han tomado apuntes de los libros:

Fundamentos de algoritmia. G. Brassard y P. Bratley Estructuras de Datos y Algoritmos. R. Hernández Esquemas algorítmicos: Enfoque metodológico y problemas resueltos. J. González y M.

Rodríguez

Page 41: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 3 de 41

El uso de las estructuras de datos suele ser un factor crucial para el diseño de algoritmos eficientes. Por ello veremos los más usados más adelante. De modo amplio escribiremos las distintas operaciones de las estructuras de datos, así como el análisis de costes de las mismas. Lo ponemos aunque no haya que estudiarlos con detenimiento.

5.1. Matrices (arrays), pilas y colas. Definición: Una matriz es una estructura de datos que consta de un número fijo de ítems (o elementos) del mismo tipo. En una matriz monodimensional, el acceso a todo ítem particular se efectúa especificando un solo índice. A lo largo de todo este resumen (y en temas anteriores también) escribiremos indistintamente tanto matriz como vector, fijándonos que los índices sean uno en el caso de poner el primero. Ejemplo:

tab: matriz [1. .50] de enteros Aquí, tab es una matriz de 50 enteros indexados desde 1 hasta 50. Por tanto, tab[1] es el primer elemento de la matriz y tab[50] alude al último.

La propiedad esencial de esta matriz es que podemos calcular la dirección de cualquier elemento dado en un tiempo constante.

Las operaciones de las matrices son: El tiempo necesario para leer el valor de un solo elemento o para cambiar

ese valor se encuentra en 푂(1) (operaciones elementales). Toda operación que implique a todos los elementos de una matriz tendera

a requerir más tiempo a medida que crezca el tamaño de la matriz. Una operación como dar valor inicial a todos los elementos o buscar el mayor elemento tendrá coste 휃(푛) o bien O(n) .

Las matrices monodimensionales permiten implementar eficientemente la estructura de datos llamada pila. Veremos está estructura con más detenimiento, aunque insisto que no hay que sabérselo todo de memoria. Vemos la estructura de datos pila:

Definición: Una pila es una lista dinámica LIFO. La forma de insertar y recuperar elementos hace que el primero en entrar sea el último en salir.

Utilización: Es útil cuando sea importante conservar el orden de inserción y extracción según la propiedad LIFO. No es útil si el acceso a los elementos es indiscriminado o si debe existir algún tipo de orden en ella dependiendo del valor de los elementos.

Page 42: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 4 de 41

Implementaciones:

1. Lista enlazada: Colección de registros que contiene el elemento de información y un apuntador al siguiente.

2. Vectores: Un vector contiene los elementos de información. Se utiliza si podemos estimar el número máximo de elementos que albergamos.

Operaciones:

Creación: 1. fun pila-vacía () dev p:pila: Devuelve una pila vacía.

Modificación: 1. fun apila (e:elemento, p:pila) dev p:pila: Añade el elemento a la

pila. Se denomina “push”. 2. fun desapila (p:pila) dev e:elemento: Devuelve el elemento

situado en la cima de la pila y lo borra de ésta. Se denomina “pop”.

Consulta: 1. fun vacía (p:pila) dev b:booleano: Comprueba si en la pila hay

algún elemento. 2. fun llena (p:pila) dev b:booleano: Comprueba si en la pila está

llena, es decir, no caben más elementos. 3. fun altura (p:pila) dev n:natural: Número de elemento en la pila.

El coste asociado según la implementación es:

Vectores Punteros

pila-vacía cte cte apila cte cte desapila cte cte vacía cte cte llena cte cte altura cte cte

La estructura de datos llamada cola también se puede implementar de forma bastante eficiente en una matriz monodimensional. Pasamos a verlo con más detalle como hicimos con la pila:

Definición: Una cola es una lista con dinámica FIFO. Los elementos se insertan en la cola y la recuperación se efectúa en el mismo orden de inserción. El primero en entrar es el primero en salir. Utilización: Una cola es útil cuando resulta relevante que el orden de inserción y extracción en la estructura se realice según la propiedad FIFO. Análogamente al caso de las pilas, no es de utilidad si el acceso a los elementos es indiscrimado. Implementaciones: Son las mismas que en el caso de las pilas.

Page 43: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 5 de 41

Operaciones: Tiene las suyas propias aunque podríamos usar las de las listas.

Creación: 1. fun cola-vacía () dev p:pila: Devuelve una cola vacía.

Modificación: 1. fun encolar (e:elemento, c:cola) dev c:cola: Inserta el elemento

en la cola. 2. fun desencolar (c:cola) dev e:elemento: Devuelve el elemento

situado al comienzo de la cola y lo borra de ésta.

Consulta: 1. fun vacía (c:cola) dev b:booleano: Comprueba si en la cola hay

algún elemento. 2. fun llena (c:cola) dev b:booleano: Comprueba si en la cola está

llena, es decir, no caben más elementos.

El coste asociado según la implementación es:

Vectores Punteros cola-vacía cte cte borrar cte 푂(푛) encolar cte cte desencolar cte cte vacía cte cte llena cte cte

Tanto para las pilas como para las colas hay una desventaja del uso de las matrices para la implementación y es que normalmente hay que reservar espacio para el máximo número de elementos que se prevea. Si reservamos mucho espacio es un desperdicio y si el espacio no es suficiente es difícil reservar más.

Los elementos de una matriz pueden ser de cualquier tipo de longitud fija: entero, booleano, etc.

Ejemplo:

lettab: matriz [′푎 . . ′푧′] de valor

No se permite indexar una matriz empleando números reales, ni tampoco estructuras tales como las cadenas o conjuntos.

Podemos declarar matrices con dos o más índices de forma similar a las unidimensionales, las cuales denominaremos bidimensionales o matrices, indistintamente, que será las que tratemos en la asignatura.

Ejemplo:

matriz: matriz [1. .20,1. .20] de complejo

Una referencia a cualquier elemento requiere ahora dos índices. Ej. matriz [5,7].

Page 44: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 6 de 41

Las operaciones de matrices bidimensionales:

Lectura o modificación del valor de la matriz tiene coste 푂(1), como las operaciones elementales.

Dar valor inicial a todos los elementos de la matriz o buscar el elemento mayor requieren un tiempo 휃(푛 ) por tener dos dimensiones.

Dijimos que el tiempo necesario para dar un valor inicial a todos los elemento de un vector de tamaño n es 휃(푛). Si suponemos que no queremos dar valor inicial a todos los valores, sino que lo único que se precisa saber es si se le ha dado o no valor inicial y obtenemos su valor en tal caso.

Si estamos dispuestos a emplear más espacio, la técnica denominada inicialización virtual nos permite evitar el tiempo de dar valores a todas las entradas del vector. Tendremos estos componentes:

푇[1. . 푛]: Vector que hay que inicializar virtualmente. 푎[1. . 푛] y 푏[1. .푛]: Vector auxiliares. 푐푡푟: Contador.

Ejemplo: veremos un ejemplo paso a paso para que se vea más claramente estas componentes:

Al comenzar, damos simplemente a 푐푡푟 el valor 0 y dejamos los vectores a, b y T con aquellos valores que pudieran contener, o lo que es igual no inicializamos ningún valor en ninguno de estos vectores. En el paso primero, inicializamos el valor en la posición 4 en T, supongamos 20:

4

20

1

4

1

siendo: T: El primer vector, que por el diseño no podemos poner el nombre. Indica que se ha inicializado un valor en la posición 4. 푐푡푟 = 1: El contador se pone a 1, por ser el primer valor. 푎[1] = 4: Es la segunda matriz de nuestro dibujo. Indica la posición inicializada en primer lugar. 푏[4] = 1: Es la última matriz. Nos dice qué elemento de T está inicializado en qué posición. En este caso, el 4ª elemento de T es el primero en inicializarse.

Page 45: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 7 de 41

En el paso segundo, inicializamos el valor 15, por ejemplo, en la posición 1 en T:

1 4

15 20

1

4 1

2 1

siendo: T: El primer vector, que por el diseño no podemos poner el nombre. Indica que se ha inicializado otro valor en la posición 1. 푐푡푟 = 2. 푎[1] = 2: Es la segunda matriz de nuestro dibujo. Indica la posición inicializada en segundo lugar. 푏[2] = 1: Es la última matriz. Nos dice qué elemento de T está inicializado en qué posición. En este caso, el 1 elemento de T es el segundo en inicializarse.

Para determinar si se ha asignado un valor a 푇[푖], comprobamos primero si 1 ≤ 푏[푖] ≤ 푐푡푟. Si no se cumple, no ha sido inicializado. Si se cumple, verificamos si realmente ha sido asignado si 푎 푏[푖] = 푖, siendo afirmativo cuando 푇[푖] ha sido iniciado y si no, no.

5.2. Registros y punteros (apuntadores). Definición: Un registro es una estructura de datos que consta de un número fijo

de elementos, que suelen llamarse campos en este contexto y que son de tipos posiblemente distintos.

Ejemplo: tipo persona = registro nombre: cadena edad: entero peso: real varon: booleano

Podremos referenciar una variable usando la notación de punto, como puede ser Juan.edad. Las matrices pueden aparecer como elementos de un registro y los registros se pueden almacenar en matrices.

Ejemplo:

clase: matriz [1. .50] de persona

Page 46: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 8 de 41

Las operaciones de registro son:

La dirección de todo elemento particular se puede calcular en un tiempo constante, así que las consultas o modificaciones del valor de un campo se pueden considerar como operaciones elementales.

Se pueden utilizar los registros en conjunción con los punteros.

Ejemplo: tipo jefe = ^ persona

donde jefe es un puntero de un registro cuyo tipo es persona. Para hacer alusiones a los campos de este registro utilizaremos la siguiente notación jefe^.nombre, fijándonos en que la flecha se pone después del nombre del tipo. Por último, decir que si un puntero tiene el valor especial nulo “nil” entonces no apunta a ningún registro, esto es importante para verlo después en otras estructuras de datos.

5.3. Listas.

Definición: Una lista es una colección de elementos de información dispuestos en un cierto orden.

A diferencia de las matrices y registros, el número de elementos de la lista no suele estar fijado ni suele estar limitado por anticipado (recordemos que era el inconveniente de estas estructuras). Podemos determinar cuál es el primer elemento, cuál es el último, cuál el predecesor y sucesor de esta estructura. En una máquina, el espacio correspondiente a cualquier elemento dado suele denominarse un nodo. La información asociada a cada nodo se muestra dentro del cuadro correspondiente y las flechas muestran los enlaces que van desde el nodo a su sucesor.

Utilización: El uso más general es el de almacenar elementos de información sin poder estimar el número máximo de elementos de que disponemos. Si la forma de acceso está bien definida es posible que podamos optar por una pila o una cola.

Implementaciones:

1. Mediante vectores: Un vector contiene los elementos de información. Se utiliza si podemos estimar el número máximo de elementos que albergamos. Usaremos esta declaración:

tipo lista = registro contador: 0..longmax valor: matriz [1. . 푙표푛푔푚푎푥] de información

Los ítems (o elementos) de una lista ocupan las posiciones desde valor[1] hasta valor[푐표푛푡푎푑표푟] y el orden de sus elementos es el mismo que el orden de sus índices dentro de la matriz.

alpha beta delta gamma

Page 47: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 9 de 41

La lista no tiene tamaño definido pero se limita según la memoria disponible en el equipo.

Características de esta implementación: - Asignación estática. - Inserción costosa: Hay que insertar el elemento y luego desplazar

el resto a la derecha. En el caso peor, hay que mover todos los elementos. Sería coste lineal.

- Las búsquedas son eficientes.

Operaciones de esta implementación: a) Se puede hallar rápidamente los elementos primero y último de la

lista, así que como también el predecesor y sucesor de cualquier elemento dado. El coste es constante 푂(1) .

b) Insertar un nuevo elemento o borrar uno requiere un número de operaciones que, en el caso peor, está en el orden del tamaño actual de la lista. Tendría coste lineal 푂(푛) .

Desventaja: - Todo el almacenamiento precisado está reservado a lo largo de

toda la vida del programa.

2. Mediante punteros: Empleamos esta declaración: tipo lista = ^ nodo tipo nodo = registro valor: información siguiente: ^ nodo Todos los nodos salvo el último incluyen un puntero explicito a su sucesor. El puntero del último nodo tiene el valor especial nulo (“nil” o NIL) para indicar que no apunta a ningún nodo.

NIL

Page 48: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 10 de 41

Características de esta implementación:

- Asignación dinámica: Según el número de elementos que hagan falta.

- Inserciones eficientes: Sólo hace falta cambiar dos punteros. - Búsqueda costosa: Pasa al revés que antes.

Operaciones de esta implementación: a) Examinar el k-ésimo elemento, para un k arbitrario, se necesita

seguir k punteros. Coste 푂(푘). b) Inserción de un nuevo nodo o el borrado de un nodo. Coste

constante.

Operaciones de las listas en general:

Creación: 1. fun lista-vacía () dev l:lista: Devuelve una lista vacía.

Modificación: 1. fun añadir (e:elemento, l:lista) dev l:lista: Añade el elemento a la

lista. 2. fun resto (l:lista) dev l:lista: Elimina el primer elemento y devuelve

el resto de la lista.

Consulta: 1. fun vacía (l:lista) dev b:booleano: Comprueba si en la lista hay algún

elemento. 2. fun miembro (e:elemento, l:lista) dev b:booleano: Comprueba si el

elemento forma parte de la lista. 3. fun elemento (p:posición, l:lista) dev e:elemento: Consulta el

elemento de la posición p de la lista, sin modificarlo. 4. fun primero (l:lista) dev e:elemento: Extrae el primer elemento de la

lista sin modificarla.

Vectores Punteros lista-vacía cte cte añadir cte cte resto cte cte vacía cte cte miembro 푂(푛) 푂(푛) elemento cte 푂(푛) primero cte cte

Page 49: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 11 de 41

5.4. Grafos.

Daremos una breve introducción de los grafos, donde se nos darán nociones básicas del mismo, antes de meternos con la definición formal. Este apartado es de los más importantes, sin descartar lo anterior. Intuitivamente, un grafo es un conjunto de aristas unidas por un conjunto de líneas o flechas (llamadas aristas). Ejemplo:

Conceptos básicos de grafos:

Camino: Sucesión de vértices y aristas que comunican un vértice con otro.

Peso: Valor asociado a una arista. Indica el coste o el valor de uso de dicha arista.

Ciclo: Camino propio que empieza y termina en el mismo vértice. Tendremos estos tipos de grafos, igualmente pasamos a definirlos:

1. Dirigidos/no dirigidos: Grafos no dirigidos: Un grafo es no dirigido si la unión entre cualesquiera dos vértices adyacentes es simétrica. Se ve claramente al unirse los nodos mediante líneas sin flechas.

Ejemplo:

Veremos el conjunto de nodos y de aristas del grafo:

푁 = {푎, 푏, 푐}.

퐴 = {푎, 푏}, {푎, 푐} .

Dirigidos: Los nodos se unen mediante líneas con flecha asociada. En el ejemplo anterior, la única diferencia es que el conjunto de aristas lo denotaremos por (푎, 푏).

Se pueden formar caminos y ciclos con estos tipos de grafos.

alpha beta

gamma delta

a

c b

Page 50: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 12 de 41

2. Conexo/no conexo:

Grafo conexo: Un grafo es conexo cuando siempre hay al menos un camino entre cualesquiera dos vértices, es decir, si todos los nodos están conectados por alguna arista. Ejemplo:

Grafo no conexo: No todos los nodos están conectados. Ejemplo:

Un grafo dirigido es fuertemente conexo si se puede pasar desde cualquier nodo hasta cualquier otro siguiendo una secuencia de aristas, pero respetando esta vez el sentido de las flechas.

a b

c

e d

c

e d

a b

Page 51: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 13 de 41

3. Cíclico/acíclico:

Cíclico: Puede crear un ciclo desde un nodo al otro. En este ejemplo será un grafo no dirigido, pero puede ser el dirigido según el sentido de las flechas. Ejemplo:

Acíclico: Es justo lo contrario, no se crearía ningún ciclo.

Definición formal de grafo: Un grafo es una pareja 퐺 = ⟨푁,퐴⟩ en donde N es un conjunto de nodos y A es un conjunto de aristas. Pondremos un ejemplo que nos servirá a lo largo del tema:

푁 = {푎푙푝ℎ푎, 푏푒푡푎, 푔푎푚푚푎, 푑푒푙푡푎}.

퐴 =(푎푙푝ℎ푎, 푏푒푡푎), (푎푙푝ℎ푎,푔푎푚푚푎), (푏푒푡푎, 푑푒푙푡푎), (푔푎푚푚푎,푎푙푝ℎ푎),

(푔푎푚푚푎,푏푒푡푎), (푔푎푚푚푎, 푑푒푙푡푎) .

Utilización: Se utiliza para la representación de los elementos de información y de la relación entre ellos. Un ejemplo puede ser la representación de las ciudades (vértices) y las carreteras (aristas) así como las distancias (peso) que hay entre ellas.

c

a b

alpha beta

gamma delta

Page 52: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 14 de 41

Implementaciones: Tendremos dos tipos:

1. Matriz de adyacencia: Un registro contiene, por un lado, un vector con los elementos de información contenido en los vértices y, por el otro lado, una matriz que puede tener valores lógicos indicando existencia o no de aristas, o bien un valor que indique el peso o coste de dicha arista.

tipo grafoadya = registro valor: matriz [1. . 푛푢푚푛표푑표푠] de información

adyacente: matriz [1. . 푛푢푚푛표푑표푠, 1. . 푛푢푚푛표푑표푠] de boolean

Si existe arista de i a j entonces adyacente [푖, 푗] = verdadero, en caso contrario adyacente [푖, 푗] = falso.

En un grafo dirigido, tal y como dijimos en su definición anteriormente, adyacente [푖, 푗] = adyacente [푗, 푖], es decir, la matriz de adyacencia es simétrica (una mitad es igual a la otra). Operaciones de esta implementación:

- Para saber si existe arista entre i y j hay que buscar un valor de la matriz. Coste constante, 푂(1).

- Si deseamos examinar todos los nodos que están conectados con algún nodo dado hay que recorrer toda una fila completa en el caso de un grafo no dirigido o bien tanto una fila completa como una columna completa en el caso de un grafo dirigido. Tiene coste 휃(푛푢푚푛표푑표푠).

- El espacio requerido para representar un grafo es cuadrático. Coste 휃(푛푢푚푛표푑표푠 ).

2. Array de registros (lista de adyacencia): Una matriz de vértices contiene el valor de estos y una lista de sus vértices sucesores.

tipo grafolista = matriz [1. . 푛푢푚푛표푑표푠] de registro

valor: información adyacente: lista

Aquí se asocia a cada nodo i una lista formada por sus vecinos, esto es, una lista formada por aquellos nodos j, tales que exista una arista de i a j (para grafo dirigido) o entre i y j (para grafo no dirigido). Ejemplo:

1 n

3 1 5 7 4 0

Page 53: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 15 de 41

Operaciones de esta implementación:

- Puede ser que sea posible examinar todos los vecinos de un nodo dado en menos de numnodos operaciones. 푂(푛푢푚푛표푑표푠).

- Determinar si existe o no una conexión directa entre dos nodos dados i y j nos obliga a recorrer la lista de vecinos del nodo i (y también del j, si fuera un grafo dirigido), lo cual es menos eficiente que buscar un valor booleano en una matriz. Equivale a buscar el nodo. El coste es 푂(푛푢푚푛표푑표푠), que es peor que antes.

Para pocas aristas ocupa menos espacio que la implementación anterior.

Operaciones de grafos en general:

Creación: 1. fun grafo-vacío () dev g:grafo: Devuelve un grafo vacío.

Modificación:

1. fun sucesores (v:vértice, g:grafo) dev l:lista: Devuelve una lista con los vértices adyacentes a v.

2. fun peso (v1,v2:vértice, g:grafo) dev p:peso: Peso asociado a la arista que une los vértices dados.

3. fun añadir-arista (v1,v2:vértice, p:peso, g:grafo) dev g:grafo: Añade una arista entre los vértices dados y le asigna el peso p.

4. fun añadir-vértice (v:vértice, g:grafo) dev g:grafo: Añade el vértice v al grafo g.

5. fun borrar-arista (v1,v2:vértice, g:grafo) dev g:grafo: Elimina la arista que une los vértices dados.

6. fun borrar-vértice (v:vértice, g:grafo) dev g:grafo: Borra el vértice del grafo y todas las aristas que partan o lleguen de él.

Consulta: 1. fun adyacente (v1,v2:vértice, g:grafo) dev b:boolean: Comprueba si

los vértices v1 y v2 son adyacentes.

Matriz de adyacencia Lista de adyacencia grafo-vacío cte cte sucesores 푂(푛) cte peso cte 푂(푛) añadir-arista cte cte borrar-arista cte 푂(푛) añadir-vértice cte cte

borrar-vértice 푂(푛) 푂(푛) adyacente cte 푂(푛)

Page 54: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 16 de 41

5.5. Arboles.

Un árbol es un grafo acíclico, conexo y no dirigido. Se puede definir como un grafo no dirigido en el cual existe exactamente un camino entre todo par de nodos dado.

Se emplean las mismas implementaciones que un grafo.

Propiedades de los árboles:

Un árbol con n nodos contiene exactamente 푛 − 1 aristas. Si se añade una única arista a un árbol, entonces el grafo resultante

contiene un único ciclo. Si se elimina una única arista de un árbol, entonces el grafo resultante ya

no es un conexo.

Nos interesamos por los árboles con raíz, en los cuales hay un nodo llamado raíz, que es especial. La raíz la dibujaremos en la parte superior y luego su descendencia, como un árbol genealógico. Usaremos el término árbol en todas las ocasiones y en todo el resto de asignatura.

Ejemplo: Padre

Hijo Hijo

Una de las propiedades del árbol la pasamos a ver con más detenimiento:

Un árbol con n nodos contiene exactamente 푛 − 1 aristas. Para comprobarlo usaremos la demostración por inducción, que dimos brevemente en el tema 1, aunque aquí lo trataremos con más detenimiento:

Hipótesis de inducción: Suponemos que los nodos están conectados por las aristas.

푛 − 1 aristas

Page 55: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 17 de 41

Si se añade un nodo más, entonces se añade una sola arista (u), ya que si añadimos otra más (v) crea un ciclo. Se pasaría a 푛 + 1 nodos y n aristas.

u

v

Implementaciones:

1. Con punteros al hijo mayor y al hermano siguiente. Se usan nodos del tipo:

tipo nodoarbol1 = registro valor: información hijo_mayor, hermano_siguiente: ^ nodoarbol1

Ejemplo:

La ventaja es que todos los nodos se pueden representar utilizando la misma estructura registro, independientemente del número de hijos y hermanos que posean.

2. Con punteros al padre. Su representación es esta: tipo nodoarbol2 = registro valor: información padre: ^ nodoarbol2

Cada nodo contiene un único puntero que lleva a su padre. Esta representación es la más económica en términos de espacio, pero es poco eficiente a no ser que todas las operaciones del árbol impliquen comenzar de un nodo y subir, sin descender nunca.

Si queremos acelerar operaciones que queremos efectuar a base de añadir punteros suplementarios. Por el contrario, se incrementa el espacio.

alpha

beta gamma

zeta delta epsilon

Page 56: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 18 de 41

Tendremos ocasión de usar árboles binarios, de 0, 1 y 2 hijos, distinguiendo entre hijo izquierdo y derecho.

3. Árbol k-ario. Generalizando si en cada nodo del árbol no puede haber más de k hijos, se trata de un árbol k-ario. Esta representación emplea estos nodos:

tipo nodo-k-ario = registro valor: información hijo: matriz [1. . 푘] de ^ nodo-k-ario

En el caso de un árbol binario (de 0 a 2 hijos) tenemos: tipo nodo-binario = registro

valor: información hijo-izquierdo, hijo-derecho: ^ nodo-binario

Un árbol binario es un árbol de búsqueda si el valor contenido en todos los nodos internos es mayor o igual que los valores contenidos en su hijo izquierdo o en cualquiera de los descendientes de ese hijo y menor o igual que los valores contenidos en su hijo derecho o en cualquiera de los descendientes de ese hijo. Sólo mostraremos brevemente los conceptos del árbol de búsqueda sin entrar en más detalle.

Operaciones del árbol de búsqueda: - Borrar un nodo o añadir un nuevo valor es fácil pero luego hay

que recuperar la propiedad del montículo. Se vuelve un árbol desequilibrado con ramas largas y delgadas, cuya búsqueda será entonces lineal.

- Para equilibrar el árbol tendremos coste 푂(log푛) en el caso peor, siendo n el número de nodos que hay en el árbol.

Conceptos sobre árboles: Altura de un nodo: Es el número de aristas que hay en el camino más

largo que vaya desde el nodo en cuestión hasta una hoja. Profundidad de un nodo: Es el número de aristas que hay en el

camino que va desde el nodo raíz hasta el nodo en cuestión. Nivel de un nodo: Es igual a la altura de la raíz del árbol menos la

profundidad del nodo estudiado.

Page 57: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 19 de 41

Una aplicación de estos conceptos puede ser el siguiente ejemplo:

Nodo Altura Profundidad Nivel

alpha 2 0 2 beta 1 1 1 gamma 0 1 1 delta 0 2 0 épsilon 0 2 0 zeta 0 2 0

Los valores que se nos dan son todos sacados de la definición, aunque nos pararemos en deducir el nivel, en este caso veremos el de alpha y beta con más calma.

Nivel de alpha = altura de alpha – profundidad de alpha = 2 – 0 = 2. Nivel de beta = altura de alpha – profundidad de beta = 2 – 1 = 1.

5.6. Tablas asociativas. Brevemente daremos las nociones básicas en este apartado.

Definición: Una tabla asociativa es igual a una matriz, salvo que su índice no está restringido a encontrarse entre dos cotas predeterminadas.

Para implementarlo usaremos una lista: tipo lista_tabla = ^ nodo_tabla tipo nodo_tabla = registro indice: tipo_indice valor: información siguiente: ^ nodo_tabla

Esta implementación es ineficiente en el caso peor, requiriendo un tiempo que se encuentra en Ω(푛 ). Todos los compiladores utilizan una tabla asociativa para implementar la tabla de símbolos, que contiene los identificadores que se utilizan en el programa que hay que compilar.

alpha

beta gamma

delta epsilon zeta

Page 58: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 20 de 41

Veremos varios conceptos:

- Función de dispersión: Es una función ℎ:푈 → {0,1,2, . . ,푁 − 1} que debe dispersar todos los índices probables: ℎ(푥).

- Colisión: ocurre cuando 푥 ≠ 푦 pero ℎ(푥) = ℎ(푦). - Factor de carga: Es m/N, donde m es el número de índices distintos que

se han almacenado en la tabla y N es el tamaño de la matriz que se emplea para implementarla.

- Redispersión: Lo usaremos para mantener valores pequeños del factor de carga. Consiste en volver a hacer la dispersión con otra función distinta.

5.7. Montículos (heaps).

Esta es una estructura de las más importantes que daremos a lo largo del curso, por lo que nos centraremos en las distintas propiedades y operaciones.

Definición: Un montículo es un tipo especial de árbol que tiene la propiedad particular que se puede implementar en una matriz sin punteros explícitos.

Tiene estas características:

- Es binario, significa que tiene de 0 a 2 hijos cada nodo. - Cada uno de los nodos incluye un elemento de información llamado valor

del nodo, siendo éste mayor o igual que los valores de sus hijos. - Es esencialmente completo: Todo nodo interno, con la posible excepción

de un nodo especial, tiene exactamente dos hijos. Pasamos esta última característica con más detenimiento:

Árbol binario esencialmente completo: Es un árbol binario en el que todos los nodos finales hasta el penúltimo están completos (con mayor número de nodos posibles) y el último está incompleto, aunque ordenado. Se irá rellenando de arriba a abajo y de izquierda a derecha.

Ejemplo:

Page 59: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 21 de 41

Calculamos el número de nodos que tendrá un árbol binario, como sigue:

Nivel 푘 1 nodo 2

Nivel 푘 − 1 2 nodos 2

Nivel 푘 − 2 4 nodos 2

Nivel 1

Nivel 0 1 ≤ 푛표푑표푠 ≤ 2

Se van rellenando los nodos hasta completar el nivel 0. Si contamos todos los nodos hasta el penúltimo nivel:

∑ 2 = = 2 − 1 nodos.

Para resolver la serie tendremos que seguir esta fórmula, que ya hemos aplicado previamente:

∑ 푟 = .

Por tanto, para el árbol contiene estos nodos 2 ≤ 푛 ≤ 2 .

La altura del árbol que contiene n nodos es 푘 = ⌊log 푛⌋, lo que significa que es un redondeo por abajo, quedándonos con la parte entera.

Ejemplo: si tenemos tres nodos, la altura sería ⌊log 3⌋ = 1.

Es importantísimo tener bien claro la propiedad del montículo:

El nodo i es el padre de 2 ∗ 푖2 ∗ 푖 + 1 .

El nodo i es el hijo del nodo i div 2 (o i ÷ 2).

Ejemplo: tenemos este montículo:

1

2 3

4 5 6 7

8 9 10 11 12 13 14

Page 60: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 22 de 41

Queremos verificar si cumple estas condiciones, para ello lo representamos en forma de vector:

1 2 3 4 5 6 7 8 9 10 11 12 13 14

Las flechas indican cuál es el padre y cuál es el hijo, es decir, los hijos del nodo 1 son las posiciones 2 y 3.

El número del nodo indica el orden de inserción en el árbol.

Por tanto, el nodo en la posición 푇[푖] es el padre de 푇[2 ∗ 푖]푇[2 ∗ 푖 + 1] , que son

posiciones en el vector.

El nodo en la posición 푇[푖] es el hijo del nodo 푇[푖 푑푖푣 2] o 푇[푖 ÷ 2].

Estas son la propiedad del montículo para los vectores. No haría falta representarlo con punteros, ya que lo haríamos con un vector en la que cada posición indica cual es el padre e hijo. En resumen y para los vectores sería:

푇[푖] ≥ 푇[2 ∗ 푖]푇[푖] ≥ 푇[2 ∗ 푖 + 1] Hijos con respecto a padres.

푇[푖] ≤ 푇[푖 푑푖푣 2]. Padres con respecto a hijos.

Consideramos montículos de máximos (padre es mayor o igual que el hijo), aunque hay también de mínimos, que es justo lo contrario.

Utilización: La principal utilidad de un montículo es la de estar permanentemente organizado de forma que nos proporcione de manera eficiente el elemento de mayor (o menor) valor, que en este caso es la raíz del árbol binario. A este estado se le conoce como propiedad del montículo y debe restaurarse después de cualquier modificación del montículo.

Un ejemplo típico de uso de montículos es la implementación de las colas de prioridad.

Page 61: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 23 de 41

Insistimos en el asunto de las propiedades del montículo, ya que es básico su dominio. Pondremos unos ejemplos para verificar si el vector dado es montículo o no. Seguiremos este procedimiento:

1. Imaginemos que nos dan este vector:

10 7 9 4 7 5 2 2 1 El árbol del mismo sería:

Vemos que cada padre es mayor o igual que sus hijos. Por tanto, es montículo.

2. Ahora nos da este otro: 3 7 9 4 7 5 2 2 1

Observamos que el nodo 1 es menor que sus hijos, por lo que no es montículo. Para arreglarlo lo hundimos (de arriba abajo), para ello lo intercambiamos con el hijo mayor, que es 9, en este caso.

7 9

4 7 5 2

2 1

10

7 9

4 7 5 2

2 1

3

Page 62: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 24 de 41

Sigue sin cumplirse la propiedad del montículo, por lo que hundimos de nuevo el nodo 3, intercambiándolo con el hijo mayor, quedando así:

Con este último intercambio ya cumple la propiedad del montículo.

Agregamos nodo nuevo en el vector siguiente, nos fijamos que es la última posición y el valor es un 8:

7 3

4 7 5 2

2 1

9

7 5

4 7 3 2

2 1

9

7 5

4 7 3 2

2 8

9

Page 63: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 25 de 41

Flotamos el nodo último para que cumpla la propiedad del montículo y lo intercambiamos con el padre:

A continuación, hay que hacer otro cambio para que cumpla de nuevo la propiedad del montículo:

Para hacer los dos cambios hay que hacer una única operación de hundir o flotar. Ya con estos cambios se cumple la propiedad del montículo.

Operaciones del montículo: Hemos visto anteriormente una aplicación de flotar y hundir en esos

ejemplos. Pasamos a escribir las operaciones, con los algoritmos y a explicarlos. Las más importantes y en las que se basaran el resto de algoritmos es el de hundir y flotar. Empezamos por la modificación de montículo, que usa ambos, que más abajo lo describiremos. Es IMPORTANTE el saber estos algoritmos (razonando y no de memoria), teniendo en cuenta la propiedad del montículo.

7 5

8 7 3 2

2 4

9

8 5

7 7 3 2

2 4

9

Page 64: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 26 de 41

procedimiento modificar-montículo (푇[1. . 푛], 푖, 푣) { 푇[1. . 푛] es un montículo. 푇[푖] recibe el valor v y se vuelve a establecer la propiedad del montículo. Suponemos 1 ≤ 푖 ≤ 푛 }

푥 ← 푇[푖]; 푇[푖] ← 푣;

si 푣 < 푥 entonces hundir (푇, 푖); si no flotar (푇, 푖);

Este procedimiento restaura la propiedad del montículo, o bien hundiendo o bien flotando el nodo. El coste de este algoritmo es el de hundir o flotar, que son 휃(log푛).

procedimiento hundir (푇[1. . 푛], 푖) { Este procedimiento hunde el nodo i para establecer la propiedad del montículo en 푇[1. .푛]. suponemos que T seria un montículo si 푇[푖] fuera lo suficientemente grande. También suponemos que 1 ≤ 푖 ≤ 푛 } 푘 ← 푖; repetir

푗 ← 푘; { Buscar el hijo mayor del nodo j } si 2 ∗ 푗 ≤ 푛 y 푇[2 ∗ 푗] > 푇[푘] entonces 푘 ← 2 ∗ 푗 si 2 ∗ 푗 ≤ 푛 y 푇[2 ∗ 푗 + 1] > 푇[푘] entonces 푘 ← 2 ∗ 푗 + 1 intercambiar 푇[푗] y 푇[푘] { si 푗 = 푘, entonces el nodo ha llegado a su posición final }

hasta que 푗 = 푘

El coste es de 휃(log푛)

procedimiento flotar (푇[1. .푛], 푖) { Este procedimiento flota el nodo i para establecer la propiedad del montículo en 푇[1. .푛]. suponemos que T seria un montículo si 푇[푖] fuera lo suficientemente grande. También suponemos que 1 ≤ 푖 ≤ 푛 } 푘 ← 푖; repetir

푗 ← 푘; si 푗 > 1 y 푇[푗 ÷ 2] < 푇[푘] entonces 푘 ← 푗 ÷ 2 intercambiar 푇[푗] y 푇[푘] { si 푗 = 푘, entonces el nodo ha llegado a su posición final }

hasta que 푗 = 푘

El coste es de 휃(log푛) , como pasa con el algoritmo de hundir un nodo.

Debido a su importancia recordamos de nuevo la utilización del montículo, ya que lo usaremos en muchas aplicaciones prácticas. Un montículo es una estructura ideal para hallar el mayor de un conjunto, para eliminarlo, para añadir un nodo nuevo o para modificar un nodo.

Una aplicación muy utilizada es la lista de prioridad dinámica (como dijimos antes cola de prioridad), en la que el valor del nodo de la prioridad del suceso

Page 65: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 27 de 41

correspondiente, el suceso de prioridad más alta se encuentra siempre en la raíz del montículo y la prioridad de un suceso se puede modificar dinámicamente en cualquier momento.

Seguimos con más operaciones del montículo, no menos importantes que las anteriores:

funcion buscar-max (푇[1. .푛]) { Proporciona el mayor elemento del montículo 푇[1. .푛] } devolver 푇[1];

Esta función devuelve un valor de un elemento en una matriz, que recordemos es una interpretación muy utilizada de los montículos. En este caso, al ser montículo de máximo, en la raíz estará el mayor elemento. Recapitulando teníamos que el coste de acceso al vector es constante, 푂(1).

funcion borrar-max (푇[1. .푛]) { Elimina el mayor elemento del montículo 푇[1. .푛] y restaura la propiedad del montículo en 푇[1. . 푛 − 1] } 푇[1] ← 푇[푛]; hundir (푇[1. . 푛 − 1], 1);

Si queremos borrar el máximo (el primer elemento) del montículo tendremos que restaurar la propiedad del mismo, para eso hundimos la raíz hasta que se haga eso. Veremos paso a paso este algoritmo, para que así quede más claro: 1er paso. Intercambio con el último elemento. Siguiendo con el ejemplo anterior tendremos:

8 5

7 7 3 2

2 4

9

Page 66: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 28 de 41

Quitamos el primer elemento e intercambiamos el último elemento con el primero, el que previamente hemos quitado. Por tanto, nos quedaría así:

2º paso. Hundimos (de arriba abajo, recuérdese como regla nemotécnica el del buzo y el agua) el primer elemento, que como vimos en los ejemplos anteriores no cumple la condición de montículo (de máximos). Nos evitamos hacer los pasos, aunque los describiremos, para saber porque el montículo será ese:

1. Intercambiamos 4 con 8, que es su hijo mayor (푇[1] con 푇[2]). 2. Intercambiamos 4 con 7 (푇[2] con 푇[4]).

En este segundo intercambio ya es montículo, por lo que ponemos como quedaría:

Un asunto importante es que estos intercambios (es decir, hundir dos veces dos nodos) se hace en una misma llamada hasta que acabe de recorrerse todo el montículo.

procedimiento añadir-nodo (푇[1. . 푛],푣) { Añade un elemento cuyo valor es v y restaura la propiedad del montículo en 푇[1. . 푛 − 1] } 푇[푛 + 1] ← 푣;

flotar (푇[1. . 푛 + 1],푛 + 1)

8 5

7 7 3 2

2

4

7 5

4 7 3 2

2

8

Page 67: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 29 de 41

Si quisiéramos añadir un nodo habría que flotar, como resaltamos de nuevo aunque de tres saltos se hace en una llamada. El coste de nuevo lo determina flotar, que es 휃(log푛).

procedimiento crear-montículo-lento (푇[1. .푛]) { Este procedimiento transforma la matriz 푇[1. .푛] en un montículo, aunque de forma más bien ineficiente }

para 푖 ← 2 hasta n hacer flotar (푇[1. . 푖], 푖) Iríamos recorriendo el montículo desde la posición 2 hasta flotar (de arriba abajo) todos los elementos y que se restaure la propiedad del montículo. Al ser un procedimiento lento, veremos otro más rápido, que es más eficiente. A continuación los compararemos:

procedimiento crear-montículo (푇[1. .푛]) { Este procedimiento transforma la matriz 푇[1. .푛] en un montículo }

para 푖 ← ⌊푛/2⌋ bajando hasta 1 hundir (푇, 푖)

Observando ambos algoritmos vemos que mientras el primero flota haciendo un bucle mayor (de 2 a n) el segundo hunde de ⌊푛/2⌋ bajando a 1, por lo que se reduce a priori el número de operaciones. Veremos este segundo y comprenderemos porque es más eficiente. Ejemplo del segundo algoritmo, más eficiente:

- Inicialmente nos dan la situación siguiente: 1 2 3 4 5 6 7 8 9 10 1 6 9 2 7 5 2 7 4 10

Pasamos este vector a un árbol binario (montículo):

- Primer paso: Convertimos en montículos aquellos subárboles cuyas

raíces se encuentran en el nivel 1. Recordamos que se intercambiarán con su hijo mayor.

6 9

2 7 5 2

7 4

1

10

2

7 4

7

10

5 2

Page 68: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 30 de 41

- Segundo paso: Los subárboles de nivel inmediatamente superior se transforman en montículos, hundiendo (de arriba abajo) una vez más sus raíces. Se cuentan con las modificaciones del paso anterior. En todos los niveles salvo el último, hundimos la raíz de dicho subárbol.

Como antes vemos los intercambios en el subárbol izquierdo que se han realizado:

1. Intercambiamos el nodo con valor 6 con el 10 (su hijo mayor). 2. Intercambiamos el nuevo nodo con valor 6 con el 7.

Quedará así:

Observamos que el subárbol derecho ya está ordenado, por lo que no lo modificamos. Si no fuera así, lo haríamos igual.

- Último paso: Nos queda hundir la raíz, por lo que una vez hecho lo anterior, ya estamos dispuestos a hacerlo. Ya cumple la propiedad del montículo.

7 10

2 4 7

6

7 7

2 4 6

10

10 9

7 7 5 2

2 4

1

6

Page 69: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 31 de 41

De nuevo, no pintaremos los grafos, pero diremos que hemos hecho (recordemos de nuevo que eso se hace en una misma llamada a hundir):

1. Intercambiamos nodo posición 1 (valor 1) con el posición 2 (valor 10).

2. Intercambiamos nuevo nodo valor 1 con valor 7 (posición 4). 3. Intercambiamos nodo 1 (posición 4) con valor 4 (posición 9).

Nuestro resultado en forma de matriz será:

1 2 3 4 5 6 7 8 9 10 10 7 9 4 7 5 2 2 1 6

Para finalizar las operaciones del montículo analizaremos el coste del algoritmo de crear montículo. Queda decir que ambos dos tendrán coste lineal, lo único que les diferenciaran es que la constante multiplicativa del “lento” es mucho mayor (hace más operaciones) que la del rápido. Para analizar el coste tendremos esta afirmación: El algoritmo construye un montículo en tiempo lineal. La demostración es:

Sea 푡(푛) el tiempo requerido para construir un montículo de altura k como máximo. Para construir el montículo, el algoritmo transforma primero los dos subárboles asociados a la raíz en árboles de altura 푘 − 1 como máximo:

Entonces, el algoritmo hunde la raíz por una ruta cuya longitud es k como máximo, lo cual requiere un tiempo 푠(푘) ∈ 푂(푘) en el caso peor.

Page 70: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 32 de 41

Tenemos, por tanto, la recurrencia siguiente:

푡(푘) ≤ 2 ∗ 푡(푘 − 1) + 푠(푘). siendo: a: Número de llamadas recursivas = 2 b: Reducción del subproblema en cada llamada = 1 풄 ∗ 풏풌: Coste de llamadas externas a la recursividad, que sería:

푐 ∗ 푛 = 1 ⇒ 푘 = 0.

Recordemos cuales son los casos de la resolución de la recurrencia por sustracción:

휃(푛 ) si 푎 < 1 푇(푛) = 휃(푛 ) si 푎 = 1

휃 푎 si 푎 > 1

Estamos en el tercer caso, porque a = 2, por lo que el coste es 휃 푎 =휃(2 ).

Necesitamos saber el coste para el número de nodos, para ello teníamos 푘 = ⌊log푛⌋, sustituyendo tendremos 휃 2 = 휽(풏). Se verá la demostración de este coste en los ejercicios del tema 7, el esquema de divide y vencerás. Con esto queda demostrado que el coste de crear montículo es lineal, y como dijimos antes tiene más constante multiplicativa el algoritmo lento que el que hemos visto paso a paso.

Por último, para acabar el apartado de montículos veremos el algoritmo de ordenación por montículo (heapsort), cuyo algoritmo es el siguiente:

procedimiento ordenación por montículo (푇[1. . 푛]) { T es la matriz que hay que ordenar }

crear-montículo (T); para 푖 ← 푛 bajando hasta 2 hacer

intercambiar 푇[1] y 푇[푖] hundir (푇[1. . 푖 − 1], 1)

Para analizar el coste realizaremos estos pasos: 1. Análisis del funcionamiento. 2. Análisis del coste propiamente dicho.

Page 71: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 33 de 41

Analizaremos el funcionamiento como sigue:

a) Pasamos el último elemento al primero, es algo así como quitar la raíz que veíamos antes.

b) Hundimos la raíz hasta que se cumpla la propiedad del montículo. Esto quiere decir que ese último elemento ya estaría ordenado, por ser el mayor de todos los elementos.

desordenado 1ª raíz

c) De nuevo pasamos el penúltimo elemento al primero (a la raíz). Luego hundimos estos elementos. Ya estarían las dos últimas posiciones ordenadas en el vector (montículo).

d) Sucesivamente haremos esto, hasta ordenarlos. Nos fijamos que acaba en el segundo elemento, ya que no haría falta continuar más.

Una vez visto el funcionamiento, pasamos a analizar el coste, que será lo siguiente:

- Crear montículo (T): 휃(푛). - Hacer el bucle “para”: realiza n veces hundir la raíz a lo largo de un

camino una longitud log(푛), que es el coste en el caso peor. Por tanto, el coste es 휃(푛 ∗ log(푛)).

Por lo visto anteriormente, el coste del algoritmo de ordenar montículo es 휃(푛 ∗ log(푛)).

Recordemos que en algunas ocasiones usaremos un montículo invertido (o de mínimos, o la raíz es el mínimo) en el que el nodo interno es menor o igual que los valores de sus hijos. Nuestro montículo habitual es el montículo de máximos. Al haber operaciones que no resultan adecuadas para manejar listas dinámicas de prioridad usaremos montículos binomiales, que los daremos en el siguiente apartado.

5.8. Montículos binomiales.

En un montículo ordinario que contenga n elementos, buscar el mayor de ellos requiere un tiempo que está en 푂(1). Borrar el mayor elemento o insertar uno nuevo requiere un tiempo que está en 푂(log(푛)). Sin embargo, fusionar dos montículos que tengan entre los dos n elementos, requiere un tiempo que está en 푂(푛).

En un montículo binomial, la búsqueda del mayor elemento sigue necesitando un tiempo que está en 푂(1) y el borrado del mayor elemento 푂(log(푛)), igual que antes. Sin embargo, la fusión de dos de estos montículos sólo requiere un tiempo en 푂(log(푛)) y la inserción de un nuevo elemento sólo requiere un tiempo en 푂(1).

Page 72: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 34 de 41

Definición de árbol binomial: El i-ésimo árbol binomial Bi con 푖 ≥ 0, se define recursivamente como aquél que consta de un nodo raíz con i hijos, en donde el j-ésimo hijo, 1 ≤ 푗 ≤ 푖, es a su vez la raíz de un árbol binomial Bj-1.

Ejemplo de árbol binomial:

B0 B1 B2 B3

5.9. Particiones. Supongamos que se tienen N objetos numerados de 1 a N. deseamos agrupar estos objetos en conjuntos disjuntos, de tal manera que en todo momento cada objeto se encuentre exactamente en un conjunto.

Ejemplo de particiones:

Cada conjunto lleva su rótulo asociado.

Page 73: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 35 de 41

Operaciones:

- Buscar: Busca en qué conjunto está contenido un elemento. Devolvemos el rótulo de este conjunto.

- Fusionar: Se le pasan dos rótulos y construye un único conjunto con un rótulo nuevo. Ej. Fusionar (rótulo1, rótulo2).

rótulo3

rótulo1

=

rótulo2

Se nos da que inicialmente los N objetos se encuentran en N conjuntos diferentes, cada uno contiene exactamente un objeto:

Pretendemos que queden todos los objetos en un único conjunto mediante operaciones de búsquedas y fusiones, para ello necesitamos N búsquedas y 퐍− ퟏ fusiones. Esto es importante, ya que de ello depende lo que a continuación daremos en este apartado.

Page 74: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 36 de 41

La partición resultante quedaría así:

Tendremos distintas implementaciones, que en ocasiones serán mejoras de la anterior. Es decir, aunque asumimos que son implementaciones distintas en el fondo son mejoras de las anteriores. NOTA: Tomaremos la cota superior de todas las funciones al analizar el coste, ya que en principio tendremos el mismo resultado que al poner el coste exacto (recordemos que es la cota superior y la cota inferior).

Las implementaciones serán: 1. La primera implementación: El rótulo de un conjunto es el menor de sus

elementos.

La notación que usaremos será conjunto[푖], que es el rótulo que contiene al elemento i. Operaciones:

funcion buscar1 (x) { Busca el rótulo del conjunto que contiene x } devolver conjunto[푥];

Esta función tiene coste 푂(1), al devolver una posición en un array.

procedimiento fusionar1 (a,b) { Fusiona los conjuntos rotulados como a y b, suponemos que

푎 ≠ 푏 } 푖 ← 푚푖푛(푎, 푏); 푗 ← 푚á푥(푎,푏); para 푘 ← 1 hasta N hacer

si conjunto[푘] = 푗 entonces conjunto[푘] ← 푖

Page 75: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 37 de 41

Al fusionar los dos conjuntos tendremos que saber cuál de los dos rótulos a ó b es el menor y le asignamos el valor al nuevo rótulo después de fusionarlo. Gráficamente tendríamos algo así tras fusionar:

i

j

El coste máximo será 푂(푁), determinado por el bucle “para”.

Según las conclusiones anteriores el coste de 1 búsqueda es 푂(1) y el de 1 fusión es 푂(푁), por lo que para nuestro problema de N búsquedas y 푁 − 1 fusiones tendremos:

N búsquedas 푂(1) 푂(푁) 푁 − 1 fusiones 푂(푁) 푂(푁 )

Aplicando la regla del máximo, visto en temas anteriores, tendremos que el coste del problema de esta implementación es 푶(푵ퟐ).

2. La segunda implementación: Cada conjunto es un árbol, en el cual cada

nodo contiene un puntero a su padre, como nodoarbol2. Tendremos este convenio:

Si conjunto[푖] = 푖 ⇒ 푖 es rótulo de un conjunto y raíz.

Si conjunto[푖] = 푗 ≠ 푖 ⇒ 푗 es el padre de i en algún árbol.

Ejemplo:

1 2 3 4 5 6 7 8 9 10 1 2 3 2 1 3 4 3 3 4

Buscamos conjunto 1: Conjunto 1 es una raíz de un conjunto, es su rótulo por ser conjunto[푖] = 푖. Buscamos conjunto 2: Ocurre igual que el 1, es su rótulo.

Buscamos conjunto 3: Igual que los anteriores.

Buscamos conjunto 4: Observamos que conjunto[4] = 2, por lo que el padre de 4 es el 2.

Y así sucesivamente, hasta finalizar el recorrido.

Page 76: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 38 de 41

En forma de árbol, el resultado nos quedaría así:

Las operaciones de buscar y fusionar serán:

funcion buscar2 (x) { Busca el rótulo del conjunto que contiene x } 푟 ← 푥; mientras conjunto[푟] ≠ 푟 hacer 푟 ← conjunto[푟] devolver r

Iremos buscando en todos los conjuntos hasta encontrar el padre para llegar a la raíz.

En el caso peor, tiene coste lineal 푂(푁) , porque llegaría a ser un árbol de altura n, buscando el último elemento del árbol.

procedimiento fusionar2 (a,b) { Fusiona los conjuntos rotulados como a y b, suponemos que

푎 ≠ 푏 } si 푎 < 푏 entonces conjunto[푏] ← 푎

si no conjunto [푎] ← 푏 Bastaría con cambiar un conjunto y su rótulo. Entonces, el coste sería constante, 푂(1).

5

1

4

2

6

3

8 10

7 10

Page 77: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 39 de 41

Ejemplo:

Hijo de 1

Con estas modificaciones quedaría así el nuevo vector:

1 2 3 4 5 6 7 8 9 10 1 1 3 2 1 3 4 3 3 4

Según las conclusiones anteriores el coste de 1 búsqueda es 푂(푁) y el de 1 fusión es 푂(1), por lo que para nuestro problema de 푁 búsquedas y 푁 − 1 fusiones tendremos:

푁 búsquedas 푂(푁) 푂(푁 ) 푁 − 1 fusiones 푂(1) 푂(푁)

Como hemos visto antes, el coste según la regla del máximo es 푶(푵ퟐ).

3. La tercera implementación: Se mejora, ya que en vez de tener como

rótulo el menor de los elementos en cada paso. Tenemos el de menor altura, que sería el hijo del de mayor altura. No se varía buscar2, pero sí fusionar2. Ambas funciones serían:

funcion buscar2 (x) { Busca el rótulo del conjunto que contiene x } 푟 ← 푥; mientras conjunto[푟] ≠ 푟 hacer 푟 ← conjunto[푟] devolver r

procedimiento fusionar3 (a, b)

{ Fusiona los conjuntos rotulados como a y b, suponemos que 푎 ≠ 푏 } si altura[푘] = altura[푏] entonces

altura[푎] ← altura[푎]+1 conjunto[푏] ← 푎

si no si altura[푎] > altura[푏] entonces

conjunto[푏] ← 푎 si no conjunto[푎] ← 푏;

5

7 10

1

4

2

Page 78: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 40 de 41

Al cabo de una secuencia arbitraria de búsquedas y fusiones, con árbol de k nodos tiene una altura máxima ⌊log푘⌋. Para ejecutar una secuencia arbitraria de n operaciones buscar2 y N-1 operaciones fusionar3 comenzando a partir de la situación inicial es 푂(푛 ∗ log푛). En el libro nos comentan como coste exacto, como pusimos anteriormente tomaremos cota superior, porque es lo mismo.

4. La cuarta implementación: Añadimos compresión de caminos (cuidado que es de comprimir, no de comprender).

Cuando se está intentando determinar el conjunto que contiene un cierto elemento x; primero se recorren las aristas del árbol que suben desde x hasta la raíz. Una vez que se conoce la raíz, podemos recorrer una vez más las mismas aristas, modificando esta vez cada nodo encontrado por el camino de tal manera que su puntero señale ahora directamente a la raíz. Modificaremos buscar2 como sigue:

funcion buscar3 (x) { Busca el rótulo del conjunto que contiene el elemento x } 푟 ← 푥; mientras conjunto[푟] ≠ 푟 hacer 푟 ← conjunto[푟]

{ r es la raíz del árbol } Mientras 푖 ≠ 푟 hacer

푗 ← conjunto[푖] conjunto[푖] ← 푟 푖 ← 푗;

devolver r

Explicamos brevemente la compresión de caminos:

Encontramos ese nodo y vemos que el nodo padre sería el del nivel superior y el padre de este es el nodo raíz.

Page 79: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 5 Curso 2007/08 Página 41 de 41

Se cambiaría el puntero de este nodo:

Si hiciéramos muchas compresiones de caminos, o lo que es igual, muchas búsquedas el árbol llegaría a algo así:

Llegaría la búsqueda casi a coste lineal 푂(푁), en vez de 푂(log푛).

Page 80: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

 

Page 81: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Alumna: Alicia Sánchez Centro: UNED-Las Rozas (Madrid)

Resumen de programación 3

Tema 6. Algoritmos voraces.

Page 82: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 2 de 39

Índice:

6.1. Dar la vuelta (1) …………………………………………………... 3 6.2. Características generales .………………………………………… 4 6.3. Grafos: árboles de recubrimiento mínimo ………………………... 6

6.3.1. Algoritmo de Kruskal …………………………………….. 9 6.3.2. El algoritmo de Prim ……………………………………... 14

6.4. Grafos: caminos mínimos ……………………………………….. 18 6.5. El problema de la mochila (1) …………………………………… 24 6.6. Planificación ……………………………………………………... 28

6.6.1. Minimización del tiempo en el sistema …………………... 28 6.6.2. Planificación con plazo fijo ………………………………. 30

Bibliografía: Se han tomado apuntes de los libros:

Page 83: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 3 de 39

Fundamentos de algoritmia. G. Brassard y P. Bratley

Empezaremos a ver los algoritmos voraces, ya que son los más fáciles de ver. Resultan fáciles de inventar e implementar y cuando funcionan son muy eficientes. Sin embargo, hay muchos problemas que no se pueden resolver usando el enfoque voraz. Los algoritmos voraces se utilizan típicamente para resolver problemas de optimización. Por ejemplo, la búsqueda de la recta más corta para ir desde un nodo a otro a través de una red de trabajo o la búsqueda del mejor orden para ejecutar un conjunto de tareas en una computadora. Un algoritmo voraz nunca reconsidera su decisión, sea cual fuere la situación que pudiera surgir más adelante. Veremos en el siguiente apartado un ejemplo cotidiano para la que esta táctica funciona bien. En los siguientes apartados, se seguirán los apartados del temario, en los que ha sido imposible resumir las demostraciones, sobre todo, por lo que realmente este tema es una copia casi exacta del libro. Nuestro convenio es ver primero el funcionamiento del algoritmo, luego ejemplos (uno o varios), demostración de optimalidad y, por último, costes del algoritmo. 6.1. Dar la vuelta (1)

Se nos dan estas monedas: 100, 25, 10, 5 y 1 pts. Nuestro problema consiste en diseñar un algoritmo para pagar una cierta cantidad a un cliente, utilizando el menor número posible de monedas. Por ejemplo, si tenemos que pagar 289 pts., habría que usar estas monedas: 2 de 100, 3 de 25, 1 de 10 y 4 de 1 pts.

Usamos de modo inconsciente un algoritmo voraz: empezaremos por nada y en cada fase vamos añadiendo a las monedas que ya están seleccionadas una moneda de la mayor denominación posible, pero que no deben llevarnos más allá de la cantidad que haya que pagar.

El algoritmo formalizado es: funcion devolver cambio (n): conjunto de monedas

{ Da el cambio de n unidades utilizando el menor número posible de monedas. La constante C especifica las monedas disponibles }

const 퐶 = {100,25,10,5,1} 푆 ← ∅ { S es un conjunto que contendrá la solución }

푠 ← 0 { s es la suma de los elementos de S } mientras 푠 ≠ 푛 hacer x ← el elemento de C tal que 푠 + 푥 ≤ 푛

si no existe ese elemento entonces Devolver “no encuentro la solución” 푆 ← 푆 ∪ {푢푛푎 푚표푛푒푑푎 푑푒 푣푎푙표푟 푥}; 푠 ← 푠 + 푥;

devolver S Es fácil convencerse (aunque difícil de probar formalmente) que este algoritmo siempre produce una solución óptima para nuestro problema. En algunos casos, puede seleccionar un conjunto de monedas que no sea óptimo (más monedas que las necesarias), mientras que en otros casos no llegue a encontrar solución aún cuando exista, por lo que el algoritmo voraz no funciona adecuadamente.

Page 84: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 4 de 39

El algoritmo es “voraz”, porque en cada paso selecciona la mayor de las monedas que puede encontrar, sin preocuparse por lo correcto de la decisión. Además, nunca cambia de opinión. Una vez que una moneda se ha incluido en la solución, la moneda se queda allí para siempre.

6.2. Características generales Generalmente, los algoritmos voraces y los problemas que éstos resuelven se caracterizan por la mayoría de propiedades siguientes:

Son adecuadas para problemas de optimización, tal y como vimos en el ejemplo anterior.

Para construir la solución de nuestro problema disponemos de un conjunto (o lista) de candidatos. Por ejemplo, para el caso de las monedas, los candidatos son las monedas disponibles, para construir una ruta los candidatos son las aristas de un grafo, etc. A medida que avanza el algoritmo tendremos estos conjuntos:

- Candidatos considerados y seleccionados. - Candidatos considerados y rechazados.

Las funciones empleadas más destacadas de este esquema son: 1. Función de solución: Comprueba si un cierto conjunto de candidatos

constituye una solución de nuestro problema, ignorando si es o no óptima por el momento. Puede que exista o no solución.

2. Función factible: Comprueba si el candidato es compatible con la solución parcial construida hasta el momento; esto es, si existe una solución incluyendo dicha solución parcial y el citado candidato.

3. Función de selección: Indica en cualquier momento cuál es el más prometedor de los candidatos restantes, que no han sido seleccionados ni rechazados. Es la más importante de todas.

4. Función objetivo: Da el valor de la solución que hemos hallado: el número de monedas utilizadas para dar la vuelta, la longitud de la ruta calculada, etc. Está función no aparece explícitamente en el algoritmo voraz.

Para resolver nuestro problema, buscamos un conjunto de candidatos que constituyan una solución y que optimice (maximice o minimice, según los casos) el valor de la función objetivo. Los algoritmos voraces avanzan paso a paso:

Inicialmente, el conjunto de elementos seleccionados está vacío y el de solución también lo está.

En cada paso, se considera añadir a este conjunto el mejor candidato sin considerar los restantes, estando guiada nuestra selección por la función de selección. Se nos darán estos casos:

1. Si el conjunto ampliado de candidatos seleccionados ya no fuera factible (no podemos completar el conjunto de solución parcial dado por el momento), rechazamos el candidato considerado por el momento y no lo volvemos a considerar.

2. Si el conjunto aumentado sigue siendo factible, entonces añadimos el candidato actual al conjunto de candidatos seleccionados. Cada vez que se amplía el conjunto de candidatos

Page 85: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 5 de 39

seleccionados comprobamos si este constituye una solución para nuestro problema. Se quedará en ese conjunto para siempre.

Cuando el algoritmo voraz funciona correctamente, la primera solución que se encuentra es la óptima. El esquema voraz es el siguiente:

funcion voraz (C: Conjunto): conjunto { C es el conjunto de candidatos } 푆 ← ∅ { Construimos la solución en el conjunto S } mientras 퐶 ≠ 0 y ¬solución (푆) hacer 푥 ← 푠푒푙푒푐푐푖표푛푎푟 (퐶)

퐶 ← 퐶\{푥}; si factible (푆 ∪ {푥}) entonces 푆 ← 푆 ∪ {푥}

si solución (푆) entonces devolver S si no devolver “no hay solución”

La función de selección suele estar relacionada con la función objetivo. Por ejemplo, si estamos intentando maximizar nuestros beneficios, es probable que seleccionemos aquel candidato restante que posea mayor valor individual. En ocasiones, puede haber varias funciones de selección plausibles.

Veremos una forma de adecuar las características generales de los algoritmos voraces a las particulares del problema de devolver cambio, aunque previamente hemos visto el esquema de este problema en particular:

Los candidatos son un conjunto de monedas. Por ejemplo, 100, 25, 10, 5 y 1 pts.

La función de solución comprueba si el valor de las monedas seleccionadas es exactamente el valor que hay que pagar.

Un conjunto de monedas será factible si su valor no sobrepasa la cantidad que haya que pagar.

La función de selección toma la moneda de valor más alto que quede en el conjunto de candidatos.

La función objetivo cuenta el número de monedas utilizadas en la solución.

Está claro que es más eficiente rechazar todas las monedas restantes de 100 pts. (por ejemplo) cuando el valor restante que haya que pagar caiga por debajo de ese valor. El uso de la división entera para calcular cuántas monedas de un cierto valor hay que tomar también es más eficiente que actuar por sustracciones sucesivas.

Page 86: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 6 de 39

6.3. Grafos: árboles de recubrimiento mínimo

Es el primer tipo de problemas que veremos que se pueden resolver con un algoritmo voraz.

Sea 퐺 = ⟨푁,퐴⟩ un grafo conexo no dirigido en donde N es el conjunto de nodos y A el de aristas. Cada arista posee una longitud negativa. El problema consiste en hallar un subconjunto T de las aristas de G, tal que utilizando sólo las aristas de T, todos los nodos deben quedar conectados y además la suma de las longitudes de las aristas de T debe ser tan pequeña como sea posible (forman árbol de recubrimiento mínimo). Existirá, al menos, una solución y si hubiera dos soluciones de igual longitud total, escogemos la de menor número de aristas. En lugar de hablar de longitudes, podemos asociar un coste a cada arista. Entonces, el problema consistirá en hallar un subconjunto T de las aristas cuyo coste total sea el menos posible.

Sea 퐺 ′ = ⟨푁,푇⟩ el grafo parcial formado por todos los nodos de G y las aristas de T (푇 ⊆ 퐴: Contenido en A o incluso igual a A) y supongamos que en N hay n nodos. Un grafo conexo con n nodos debe tener, al menos 푛 − 1 aristas como mínimo.

Por tanto, si G’ es conexo y T tiene más de 푛 − 1 aristas (al menos tiene un ciclo), se puede eliminar al menos una arista sin desconectar G’ siempre y cuando seleccionemos una arista que forme parte de un ciclo, dándose estos casos:

1. Disminuye la longitud total de las aristas de T. 2. La longitud total queda intacta a la vez que disminuye el número de

aristas en T.

El grafo G’ se denomina árbol de recubrimiento mínimo para el grafo G si tiene 푛 − 1 aristas tales que G’ es conexo y el coste global de las aristas que quedan es el mínimo posible. Ejemplo: Supongamos que los nodos de G representan unidades y sea el coste de una arista {푎, 푏} el de tender una línea telefónica desde a hasta b. Entonces, un árbol de recubrimiento mínimo de G se corresponde con la red más barata posible para dar servicio a todas las ciudades en cuestión, siempre y cuando sólo se pueda utilizar conexiones directas entre ciudades.

Esto es un añadido del autor. Puede ser que el algoritmo voraz no de una única solución, puede ser que haya más, como ejemplos podremos poner los siguientes. Es importante, ya que suele caer preguntas similares en exámenes:

1

2 3 c

a b

Page 87: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 7 de 39

El coste de llegar de b a c es similar en ambos caminos (b-a y a-c) y directo. Se quitaría la arista de coste 3, por ser la mayor de todas. Con esto además, se demuestra que puede haber dos árboles de recubrimiento mínimo distintos en el mismo grafo, porque forman un ciclo. El segundo ejemplo podría ser:

1

1 1

En este caso, podremos quitar cualquier arista y llegar a todos los nodos con el mismo coste. Igualmente, hay varios posibles árboles de recubrimiento mínimo.

Podremos tener dos tácticas para resolver el problema que posteriormente veremos con más detalle:

1ª táctica: Consiste en comenzar por un conjunto vacio T y seleccionar en cada etapa la arista más corta que todavía no haya sido seleccionada o rechazada, independientemente de donde se encuentra esa arista en G. 2ª táctica: Implica seleccionar un nodo y construir un árbol a partir de él, seleccionando en cada etapa la arista más corta posible que pueda extender el árbol hasta un nodo adicional.

Las componentes para el problema del recubrimiento mínimo serán:

Los candidatos son las aristas de G. Un conjunto de aristas es una solución si constituye un árbol de

recubrimiento para los nodos de N (no tiene que ser recubrimiento mínimo).

Un conjunto de aristas es factible si no contiene ningún ciclo. La función de selección que utilizamos varía con el algoritmo (según las

tácticas antes expuestas). La función objetivo que hay que minimizar es la longitud total de las

aristas de la solución.

El lema siguiente es crucial para demostrar la corrección de los próximos algoritmos:

퐿푒푚푎 6.3.1 Sea 퐺 = ⟨푁,퐴⟩ un grafo conexo no dirigido en el cual está dado la

longitud de todas las aristas. Sea 퐵 ⊂ 푁 un subconjunto estricto de los nodos de

G. Sea 푇 ⊆ 퐴 un conjunto prometedor de aristas, tal que no haya ninguna arista

c

a b

Page 88: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 8 de 39

de T que salga de B. Sea v la arista más corta que sale de B (o una de las más

cortas si hay empates). Entonces 푇 ∪ {푣} es prometedor.

Demostración: Sea U un árbol de recubrimiento mínimo de G tal que 푇 ⊆ 푈. Este U tiene que existir puesto que T es prometedor por hipótesis. Se nos darán estos casos:

1. Si 푣 ∈ 푈, entonces no hay nada que probar (es ya árbol de recubrimiento mínimo).

2. Si no, cuando añadimos v a U creamos exactamente un ciclo, en el que debe existir otra arista llamada u, ya que sería quien cerrara dicho ciclo.

La situación hasta el momento es la siguiente (es una interpretación de un alumno):

u de U N/B B

V de longitud mínima

Si ahora eliminamos u, el ciclo desaparece y obtenemos un nuevo árbol V que abarca G (recordemos que era un grafo conexo no dirigido en el cual está dada la longitud de todas las aristas). Sin embargo, la longitud de v, por definición, no es mayor que la de u (v tiene longitud mínima) y, por tanto, la longitud total de las aristas de U. Por tanto, V es también un árbol de recubrimiento mínimo de G y contiene a v.

De nuevo, la situación sería la siguiente:

N/B B

Page 89: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 9 de 39

V de longitud mínima

Para completar la demostración sólo hay que comentar que 푇 ⊆ 푉 porque la arista u que se ha eliminado sale de B y, por tanto, no podría haber sido una arista de T (recordemos que es el conjunto prometedor de aristas, tal que no haya arista de T que sale de B).

6.3.1. Algoritmo de Kruskal. Para este tipo de problema se nos darán estos conjuntos:

- T: Conjunto de aristas seleccionadas. Este algoritmo hará lo siguiente:

- El conjunto de aristas T está inicialmente vacio. - A medida que progresa el algoritmo, se añaden aristas a T, que

forman componentes conexas separadas. - Para construir componentes conexas más y más grandes,

examinaremos las aristas de G por orden creciente de longitud. Se nos dan dos casos:

1. Si una arista une a dos componentes distintas, se la añadimos a T. Consiguientemente, las dos componentes conexas forman ahora una única componente.

2. En caso contrario, se rechaza la arista: une a dos nodos de la misma componente conexa y, por tanto, no se puede añadir a T sin formar un ciclo.

- El algoritmo se detiene cuando sólo queda una componente conexa.

Ejemplo del algoritmo de Kruskal:

1 2

4 6 4 5 6

3 8

4 7 3

1 2 3

4 5 6

7

Page 90: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 10 de 39

Seguiremos estos pasos para resolverlo:

1er paso. Ordenamos aristas por orden creciente de costes:

Nodo Coste {1,2} 1 {2,3} 2 {4,5} 3

{6,7} 3 {1,4} 4 {2,5} 4 {4,7} 4 {3,5} 5 {2,4} 6 {3,6} 6 {5,7} 7 {5,6} 8

2º paso. Seleccionamos aristas de la lista por orden. Vemos si unen componentes conexas distintas, si es así fusionamos esas componentes conexas para unirlas en una misma partición.

Paso Arista seleccionada Componentes conexas Inicialización - {1}, {2}, {3}, {4}, {5}, {6}, {7}

1 {1,2} {1,2}, {3}, {4}, {5}, {6}, {7} 2 {2,3} {1,2,3}, {4}, {5}, {6}, {7} 3 {4,5} {1,2,3}, {4,5}, {6}, {7} 4 {6,7} {1,2,3}, {4,5}, {6,7} 5 {1,4} {1,2,3,4,5}, {6,7} 6 {2,5} Rechazada, por formar ciclo. Están en el mismo conjunto. 7 {4,7} {1,2,3,4,5,6,7}

El coste del árbol de recubrimiento mínimo es 17.

En cada paso, el algoritmo de Kruskal va cogiendo aristas prometedoras (aquéllas que se extienda la solución a la óptima) hasta llegar a 푛 − 1 prometedor como este ejemplo.

Page 91: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 11 de 39

푇푒표푟푒푚푎 6.3.2 El algoritmo de Kruskal halla un árbol de recubrimiento

mínimo

Demostración: se hace por inducción matemática sobre el número de aristas que hay en el conjunto T. Mostraremos que si T es prometedor, entonces sigue siendo prometedor en cualquier fase del algoritmo cuando se le añade una arista adicional. Cuando se detiene el algoritmo, T da una solución de nuestro problema; puesto que también es prometedora, la solución es óptima.

Base: El conjunto vacio es prometedor porque G es conexo y, por tanto, tiene que existir una solución.

Paso de inducción: Supongamos que T (recordemos que es el conjunto de aristas seleccionadas) es prometedor antes de que el algoritmo añada una nueva arista 푒 = {푢,푣}. Las aristas de T dividen a los nodos de G en dos o más componentes conexas; el nodo u se encuentra en una de estas componentes y v está en otra componente conexa. Sea B el conjunto de nodos de esa componente que contiene a u. Ahora:

o El conjunto de B es un subconjunto estricto de los nodos de G, puesto que no incluye a v, por ejemplo. Gráficamente, quedaría algo así (es una interpretación de un alumno), siendo estos conjuntos subconjuntos de G, que por problemas con el dibujo es imposible hacerlo. Por tanto, sería:

B o T es un conjunto prometedor de aristas tal que ninguna arista

de T sale de B, porque una arista de T tiene o bien ambas aristas en B, o ninguna arista en B, así que por definición, no sale de B.

o e es una de las aristas más cortas que salen de B, porque todas las aristas estrictamente más cortas ya se han examinado o bien se han añadido a T (conjunto de aristas seleccionadas) o bien se han rechazado porque tenían los dos extremos en la misma componente conexa.

Page 92: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 12 de 39

Otra apreciación del alumno sería que en este punto, quedaría algo así:

e Arista más corta v

B aun no considerada

Recordemos que por ser Kruskal cogería la arista con el coste menor, que sería e.

Concluimos que el conjunto 푇 ∪ {푒} también es prometedor.

Para implementar el algoritmo, es preciso efectuar rápidamente las operaciones buscar (x), que nos dice en qué componente conexa se encuentra el nodo x y fusionar (A, B) para fusionar dos componentes conexos. Por eso, utilizamos la estructura de partición. Para el algoritmo es necesario representar el grafo como un vector de aristas con sus longitudes asociadas. El algoritmo es:

funcion Kruskal (퐺 = ⟨푁,퐴⟩: grafo, longitud: 퐴 → 푅 ): conj. aristas { Iniciación } Ordenar A por longitudes crecientes 푛 ← el número de nodos que hay en N

푇 ← ∅ { Contendrá las aristas del árbol de recubrim. minimo } Iniciar n conjuntos cada uno de los cuales contiene un elemento distinto de N { Bucle voraz } repetir 푒 ← {푢,푣} ← arista más corta aun no considerada

compu ← buscar (u) compv ← buscar (v) si compu ≠ compv entonces

fusionar (compu, compv) 푇 ← 푇 ∪ {푒};

hasta que T contenga 푛 − 1 aristas devolver T

Page 93: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 13 de 39

Coste del algoritmo: Calculamos el tiempo de ejecución del algoritmo en la forma siguiente. El número de operaciones es:

- 휃(푎 ∗ log(푎)) : Coste para ordenar las aristas. - 휃(푛): Para iniciar los n conjuntos disjuntos. - 휃 2 ∗ 푎 ∗ 훼 (2 ∗ 푎, 푛) : Para las operaciones de fusionar (tendrá

como máximo 2 ∗ 푎 operaciones buscar y 푛 − 1 operaciones fusionar).

- 푂(푎): Para el caso peor de las operaciones restantes.

Vamos a detallar más el coste de ordenar las aristas, que es 휃(푎 ∗ log(푎)). Veremos la parte logarítmica, teniendo en cuenta que el número de aristas seguirá está formula: 푛 − 1 ≤ 푎 ≤ ∗( ), por eso se nos darán estos casos:

- El grafo es disperso, es decir, 푎 ≈ 푛, que corresponde con la parte izquierda de la formula. Por tanto,

log(푎) ≈ log(푛)

- El grafo es denso, por lo que 푎 ≈ 푛 , que será más cercano a la parte derecha de la formula. Por tanto,

log(푎) ≈ log(푛 ) ≈ log(푛).

Por la propiedad de los logaritmos, log(푛 ) = 2 ∗ log(푛). En conclusión, asintóticamente, tanto si el grafo es disperso como denso es 휃(푎 ∗ log(푛)).

En cuanto a la parte multiplicativa, razonaremos de la misma manera:

- El grafo es disperso, por lo que el coste es 휃(푛 ∗ log(푛)). - El grafo es denso, por lo que el coste es 휃(푛 ∗ log(푛)).

Una mejora del algoritmo, que es bastante importante el saberla es usar un montículo invertido, en el que la raíz es el mínimo elemento. Se nos darán estos costes:

o Crear un montículo: 휃(푎) o Restaurar la condición del montículo (hundir la raíz como vimos

en el tema de estructura de datos): 휃(log(푎)) ≈ 휃(log(푛)). Al repetirse un número de a veces la restauración de la condición del montículo, en el caso peor será: 휃(a ∗ log(푛)).

En el caso mejor, será coste lineal 휃(푛), ya que tendríamos ordenadas las aristas de menor a mayor (es otra apreciación del alumno, no aparece en el libro, ni en otros libros que he consultado).

Page 94: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 14 de 39

6.3.2. El algoritmo de Prim.

Recordemos que en el algoritmo de Kruskal se toman las aristas por orden creciente sin preocuparse por su conexión y teniendo cuidado de no crear ciclos. El bosque crecerá al azar hasta formar un árbol de recubrimiento mínimo con todos los componentes conexos.

En el algoritmo de Prim, por otra parte, el árbol de recubrimiento mínimo crece de forma natural, comenzando por una raíz arbitraria. En cada fase, se añade una nueva rama al árbol ya construido. El algoritmo se detiene cuando se han alcanzado todos los nodos.

Veremos con más detalle el algoritmo. Se nos dan estos conjuntos: B: Conjunto de nodos que contiene la solución (inicialmente contiene al nodo de partida). T: Conjunto de aristas solución (inicialmente vacio).

El algoritmo hará lo siguiente:

- Inicialmente, B contiene un único nodo arbitrario y T está vacío. - En cada paso, el algoritmo de Prim busca la arista más corta posible

{푢, 푣}, tal que 푢 ∈ 퐵 y 푣 ∈ 푁/퐵. Entonces añade v a B y {푢,푣} a T. De esta manera, las aristas de T forman en todo momento un árbol de recubrimiento mínimo para los nodos de B. Continuamos mientras 퐵 ≠ 푁.

Un enunciado informal del algoritmo es:

funcion prim (퐺 = ⟨푁,퐴⟩: grafo, longitud: 퐴 → 푅 ): conj. aristas { Iniciación } 푇 ← ∅; 퐵 ← { un miembro arbitrario de N }

mientras 퐵 ≠ 푁 hacer buscar 푒 = {푢,푣} de longitud mínima tal que 푢 ∈ 퐵 y 푣 ∈ 푁/퐵

푇 ← 푇 ∪ {푒}; 퐵 ← 퐵 ∪ {푣};

devolver T

Page 95: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 15 de 39

Ejemplo de algoritmo de Prim:

1 2

4 6 4 5 6

3 8

4 7 3

Lo resolveremos realizando estos pasos: 1er paso: seleccionamos un nodo como raíz arbitraria. En este caso, es el nodo 1.

1 2

4 6 4 5 6

3 8

4 7 3

2o paso: En cada paso, el algoritmo de Prim busca la arista más corta posible {푢,푣} tal que 푢 ∈ 퐵 y 푣 ∈ 푁/퐵. Entonces añade v a B y {푢, 푣} a T.

En nuestro ejemplo tenemos:

Paso Arista seleccionada B Inicialización - {1}

1 {1,2} {1,2} 2 {2,3} {1,2,3} 3 {1,4} {1,2,3,4} 4 {4,5} {1,2,3,4,5} 5 {4,7} {1,2,3,4,5,7} 6 {6,7} {1,2,3,4,5,6,7}

La longitud (o el coste) total es 17, como en el caso de Kruskal.

1 2 3

4 5 6

7

1 2 3

4 5 6

7

Page 96: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 16 de 39

Cuando haya soluciones distintas, ante empate de costes podemos escoger el que queramos. No es habitual escoger los nodos en orden, pero como en este caso se puede dar. Vemos que hasta el paso 4 se añaden en orden, dependerá en todo caso de los costes de las aristas.

Veremos la demostración del algoritmo de Prim aunque es parecida a la de Kruskal.

푇푒표푟푒푚푎 6.3.3 El algoritmo de Prim halla un árbol de recubrimiento

mínimo.

Demostración: Es por inducción matemática sobre el número de aristas que hay en el conjunto T (conjunto de aristas). Demostraremos que si T es prometedor en alguna fase del algoritmo entonces al añadir una arista adicional sigue siendo prometedor. Cuando se detiene el algoritmo, T da una solución para nuestro problema, puesto que también es prometedor la solución es óptima.

Base: El conjunto vacio es prometedor. Paso de inducción: Suponga que T es prometedor inmediatamente

antes de que el algoritmo añada una nueva arista 푒 = {푢, 푣}. Ahora: o B es un subconjunto estricto de N. o T es un conjunto de aristas prometedor, por hipótesis de

inducción. o e es por definición una de las aristas más cortas que salen de

B.

La situación será la siguiente. Hay que destacar que aunque sea el dibujo algo distinto al que vimos en Kruskal, resaltamos que la idea es similar y la demostración igualmente. Quedaría:

B N/B

e

siendo: T: Contiene las aristas seleccionadas. G: Grafo completo (conjunto de nodos)

B: Conjunto de esos componentes que contiene a u (conjunto de nodos).

Page 97: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 17 de 39

La segunda implementación, que es más sencilla será:

funcion prim (L[1..n,1..n]): conj. aristas { Iniciación: solo el nodo 1 se encuentra en B } 푇 ← ∅; { Contendrá las aristas del árbol de recubrim. mínimo }

para 푖 ← 2 hasta n hacer mas próximo [푖] ← 1 distmin [푖] ← 퐿[푖, 1]

{ Bucle voraz } repetir 푛 − 1 veces min ← ∞

para 푗 ← 2 hasta n hacer si 0 ≤ distmin[푗] < min entonces min←distmin[푗]

푘 ← 푗 푇 ← 푇 ∪ { más próximo [푘], k } distmin [푘] ← −1 para 푗 ← 2 hasta n hacer si 퐿[푗,푘] ≤distmin[푗] entonces distmin[푘] ← 퐿[푗,푘]

más próximo [푗] ← 푘 devolver T

Supongamos que los nodos de G que están numeradas de 1 a n, así que 푁 = {1,2, . . , 푛}, siendo:

L: Matriz simétrica que da la longitud de todas las aristas, con 퐿[푖, 푗] =∞, si no existe la arista correspondiente.

mas próximo [i]: Proporciona el nodo de B que está más próximo a i. distmin [i]: Da la distancia desde i hasta el nodo más próximo. Si

distmin[푖] = −1, sabremos si un nodo está o no en B.

mas próximo[1] y distmin[1] no se utilizan, por ser el nodo inicial el 1.

Análisis del coste del algoritmo:

El bucle “para”, que es interno, requiere un tiempo 휃(푛). El bucle “repetir”, que es exterior, se repite 푛 − 1 veces, por lo que

requiere 휃(푛).

El coste del algoritmo de Prim requiere un tiempo que está en 휃(푛 ).

Comparación de costes de Prim y Kruskal:

Prim Kruskal General 휃(푛 ) 휃(푎 ∗ log(푛))

Grafo denso (푎 ≈ 푛 ) 휃(푛 ) 휃(푛 ∗ log(푛))

Grafo disperso (푎 ≈ 푛) 휃(푛 ) 휃(푛 ∗ log(푛))

Page 98: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 18 de 39

Como hemos visto anteriormente, para un grafo denso, a tiende a ∗( ), el algoritmo de Kruskal requiere un tiempo de 휃(푛 ∗ log(푛)), por lo que es más eficiente el algoritmo de Prim. Para un grafo disperso, a tiende a n, por lo que el algoritmo de Kruskal mejora.

Mejora del algoritmo: Si usamos montículos invertidos, tendremos que al igual que pasaba antes el coste es 휃(푎 ∗ log(푛)). Si el grafo es denso o disperso se ve como antes.

6.4. Grafos: caminos mínimos Es el segundo tipo de problemas que veremos.

Considérese ahora un grafo dirigido 퐺 = ⟨푁,퐴⟩ en donde N es el conjunto de nodos de G y A es el de aristas dirigidas. Podría ser el caso de aristas no dirigidas considerándose el grafo siguiente:

Cada arista posee una longitud no negativa. Se toma uno de los nodos como nodo origen. El problema consiste en determinar la longitud del camino mínimo que va desde el origen hasta cada uno de los demás nodos del grafo. Este problema se puede resolver mediante un algoritmo voraz que recibe el nombre de algoritmo de Dijkstra. Emplea estos conjuntos:

S: Contiene aquellos nodos que ya han sido seleccionados, cuya distancia es conocida para todos los nodos de este conjunto.

C: Contiene todos los demás nodos, cuya distancia mínima desde el origen todavía no es conocida y que son candidatos a ser seleccionados posteriormente.

La unión de ambos conjuntos sería 푁 = 푆 ∪ 퐶, o lo que es igual, ambos conjuntos formarán el conjunto total.

El funcionamiento del algoritmo es el siguiente:

En un primer momento, S contiene nada más que el origen; cuando se detiene el algoritmo, S contiene todos los nodos del grafo y el problema está resuelto.

En cada paso, seleccionamos aquel nodo de C cuya distancia al origen sea mínima y se lo añadimos a S.

Page 99: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 19 de 39

Camino especial: Diremos que un camino desde el origen a algún otro nodo es especial si todos los nodos intermedios a lo largo del camino pertenecen a S.

Gráficamente, sería:

Origen

S

En cada fase del algoritmo, hay una matriz D que contiene la longitud del camino especial más corto que va hasta cada nodo del grafo. En el momento en que se deseé añadir un nuevo nodo v a S, el camino especial más corto hasta v es también el más corto de los caminos posibles hasta v (minimiza D). Al acabar el algoritmo, todos los caminos desde el origen hasta algún nodo son especiales. Consiguientemente, los valores que hay en D dan la solución del problema de caminos mínimos. Suponemos una vez más que los nodos de G están numerados de 1 a n, por tanto, 푁 = {1,2, . . , 푛}. Podemos suponer que el nodo uno es el origen. Supongamos que la matriz L da la longitud de todas las aristas dirigidas: 퐿[푖, 푗] ≥ 0 si la arista (푖, 푗) ∈ 퐴 y 퐿[푖, 푗] = ∞, en caso contrario. Con estos datos, tendremos este algoritmo:

funcion Dijkstra (L[1..n, 1..n]): matriz [2..n] matriz D[2..n] { Iniciación }

퐶 ← {2, 3,.., n} { 푆 = 푁/퐶 sólo existe implícitamente } para 푖 ← 2 hasta n hacer 퐷[푖] ← 퐿[1, 푖] { Bucle voraz } repetir n-2 veces 푣 ← algún elemento de C que minimiza 퐷[푣] 퐶 ← 퐶\{푣} { e implícitamente 푆 ← 푆 ∪ {푣} } para cada 푤 ∈ 퐶 hacer

퐷[푤] ← 푚푖푛(퐷[푤],퐷[푤] + 퐿[푣,푤]); devolver D

siendo:

D[i]: Vector de distancias, que indica la distancia hasta el nodo i. L[i,j]: Matriz de longitud, que indica longitud del nodo y al j. w: Arista del conjunto C, que contiene todos los demás nodos, cuya distancia mínima desde el origen todavía no es conocida y que son candidatos a ser seleccionados posteriormente.

Page 100: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 20 de 39

El bucle principal se repite 푛 − 2 veces, ya que D no cambiaria si hiciéramos una iteración más para eliminar el último elemento de C. Añadimos una segunda matriz P[2. . 푛] llamada matriz de precedencias o vector de predecesores, en donde P[푣] contiene el número del nodo que precede a v dentro del camino más corto. Para hallarlo se tendría que seguir los punteros hacia atrás desde el destino hacia el origen.

Por tanto, el algoritmo definitivo incluyendo esta matriz de precedencia o vector de predecesores (según se guste más, en algunos exámenes he encontrado la segunda acepción, pero es igual) quedaría así:

funcion Dijkstra (L[1..n, 1..n]): matriz [2..n] matriz D[2..n] matriz P[2..n] { Iniciación }

퐶 ← {2, 3,.., n} { 푆 = 푁/퐶 sólo existe implícitamente } para 푖 ← 2 hasta n hacer 퐷[푖] ← 퐿[1, 푖] para 푖 ← 2 hasta n hacer 푃[푖] ← 1 { Bucle voraz } repetir 푛 − 2 veces 푣 ← algún elemento de C que minimiza 퐷[푣] 퐶 ← 퐶\{푣} { e implícitamente 푆 ← 푆 ∪ {푣} } para cada 푤 ∈ 퐶 hacer

si 퐷[푤] > 퐷[푣] + 퐿[푣,푤] entonces 퐷[푤] ← 퐷[푣] + 퐿[푣,푤]; 푃[푤] ← 푣;

devolver D

Un ejemplo del algoritmo de Dijkstra será el siguiente:

10 50

100 30

10 20 5

50

El nodo de partida es el 1. Será importante identificarlo bien, ya que para resolverlo no es lo mismo empezar por el nodo 3 que por el 1 o cualquier otro nodo distinto. No saldrá la misma solución.

1

2

3 4

5

Page 101: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 21 de 39

Tendremos estos pasos para resolverlo este ejemplo. Añadimos el vector de precedencias y a continuación los explicamos:

Paso v C D P 2 3 4 5

Inicialización - {2,3,4,5} [50,30,100,10] [1,1,1,1] 1 5 {2,3,4} [50,30,20,10] [1,1,5,1]

2 4 {2,3} [40,30,20,10] [4,1,5,1] 3 3 {2} [35,30,20,10] [3,1,5,1]

Inicialización: El conjunto de candidatos C es de 4 nodos, sin incluir el origen, que es el nodo 1. Ponemos todos los costes del nodo 1 al resto de nodos. Si no hubiera conexión directa la distancia será ∞. 1er paso: Se modifica el valor para llegar a 4 a través de 5. 2o paso: Modificamos el valor de 2 a través de 4. 3er paso y último: Modificamos el valor de 2, ahora a través de 3. NOTA: Es fundamental decir que se parará en este último paso, aunque quede un candidato por escoger, ya que tenemos todos los caminos mínimos posibles.

La demostración que el algoritmo funciona es por inducción matemática.

푇푒표푟푒푚푎 6.4.1 El algoritmo de Dijkstra halla los caminos mínimos desde un

único origen hasta los demás nodos del grafo.

Demostración: Demostraremos por inducción matemática que:

a) Si un nodo 푖 ≠ 1 está en S, entonces D[푖] da la longitud del camino más corto desde el origen hasta i, y

b) Si un nodo y no está en S, entonces D[푖] da la longitud del camino especial más corto desde el origen hasta i.

Base: Inicialmente, sólo el nodo 1, que es el origen, se encuentra en S, así que la situación a) es cierta sin más demostración. Para los demás nodos, el único camino especial desde el origen es el camino directo y D recibe valores iniciales en consecuencia. Por tanto, la situación b) es también cierta cuando comienza el algoritmo.

Hipótesis de inducción: La hipótesis de inducción es que tanto la situación a) como la b) son válidas inmediatamente antes de añadir un nodo v a S (conjunto de nodos seleccionados). Detallamos los pasos de inducción por separado para ambas situaciones.

Paso de inducción para la situación a): Para todo nodo que ya esté en S antes de añadir v no cambia nada, así que la situación a) sigue siendo válida. En cuanto al nodo v, ahora pertenecerá a S. Antes de añadirlo a S, es preciso comprobar que D[푣] proporcione la longitud del camino más corto que va desde el origen hasta v. Por hipótesis de inducción, nos da ciertamente la longitud del camino más corto. Por tanto, hay que verificar

Page 102: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 22 de 39

Origen

que el camino más corto desde el origen hasta v no pase por ninguno de los nodos que no pertenecen a S. Supongamos lo contrario; supongamos que cuando se sigue el camino más corto desde el origen hasta v, se encuentran uno o más nodos (sin contar el propio v) que no pertenecen a S. Sea x el primer nodo encontrado con estas características. Ahora el segmento inicial de esa ruta, hasta llegar a x, es una ruta especial, así que la distancia hasta x es D[푥], por la parte b) de la hipótesis de inducción. Claramente, la distancia total hasta v a través de x no es más corta que este valor, porque las longitudes de las aristas son no negativas. Finalmente, D[푥] no es menor que D[v], porque el algoritmo ha seleccionado a v antes que a x. Por tanto, la distancia total hasta v a través de x es como mínimo D[푣] y el camino a través de x no puede ser más corto que el camino especial que lleva hasta v.

Gráficamente, sería: x El camino más corto

S v

El camino especial más corto

Paso de inducción para la situación b): Considérese ahora un nodo w, distinto de v, que no se encuentre en S. Cuando v se añade a S, hay dos posibilidades para el camino especial más corto desde el origen hasta w:

1. O bien no cambia. 2. O bien ahora pasa a través de v.

En el segundo caso, sea x el último nodo de S visitado antes de llegar a w. La longitud de este camino es 퐷[푥] + 퐿[푥,푤]. Parece a primera vista que para calcular el nuevo valor de 퐷[푤] deberíamos comparar el valor anterior de 퐷[푤] con 퐷[푥] + 퐿[푥,푤] para todo nodo x de S (incluyendo a v). Sin embargo, para todos los nodos x de S salvo v, esta comparación se ha hecho cuando se añadió x a S y 퐷[푥] no ha variado desde entonces. Por tanto, el nuevo valor de 퐷[푤] se puede calcular sencillamente comparando el valor anterior con 퐷[푣] + 퐿[푣,푤]. Puesto que el algoritmo hace esto explícitamente, asegura que la parte b) de la inducción siga siendo cierta también cuando se añade a S un nuevo nodo v.

Para completar la demostración de que el algoritmo funciona, obsérvese que cuando se detenga el algoritmo, todos los nodos menos uno estarán en S. En ese momento queda claro que el camino más costo desde el origen hasta el nodo restante es un camino especial.

Page 103: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 23 de 39

Coste del algoritmo: Recordemos que hemos visto estas dos implementaciones, por el momento:

1. La primera teníamos dos vectores L y D. 2. Añadimos el vector de precedencia o matriz de predecesores P.

El coste de ambas implementaciones lo determinara el bucle “mientras”, en el que se irán seleccionando una arista cada vez. Tendremos lo siguiente:

(푛 − 1) + (푛 − 2) + ⋯+ 1 ≈ ∑ 푖 ≈ 푛

Por tanto, el coste del algoritmo es 휃(푛 ), ya que aunque se añada un vector más en la segunda implementación las operaciones son elementales, por lo que se quedaría igual.

Mejora del algoritmo: Usaremos un montículo invertido, que contiene un nodo para cada elemento v de C que minimiza D[푣] que se encuentra siempre en la raíz.

El coste como hemos visto en los algoritmos anteriores corresponderá con:

Inicialización: 휃(푎) Flotar o hundir la raíz n veces: 휃(푎 ∗ log(푛)).

Igualmente, tendremos dos casos, según el tipo de grafo:

Si es grafo disperso (푎 ≈ 푛): 휃(푛 ∗ log(푛)). Si es grafo denso (푎 ≈ 푛 ): 휃(푛 ∗ log(푛)).

Se concluye, por tanto, que es más conveniente si fuera grafo disperso.

Page 104: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 24 de 39

6.5. El problema de la mochila (1).

Nos dan n objetos y una mochila. Para 푖 = 1,2, … , 푛, el objeto i tiene un peso positivo 푤 (푤 > 0)y un valor positivo 푣 (푣 > 0). La mochila puede llevar un peso que no sobrepase W.

Podremos fraccionar los objetos (es muy importante): 0 ≤ 푥 ≤ 1, de manera que podamos decidir llevar solamente una fracción del objeto 푥 .

Nuestro objetivo es llenar la mochila de tal manera que se maximice el valor de los objetos transportados, respetando la limitación de la capacidad impuesta (sin sobrepasar W). El problema, por tanto, se puede enunciar de la siguiente manera:

Maximizar ∑ 푥 ∗ 푣 con la restricción ∑ 푥 ∗ 푤 ≤ 푊

donde (푣 > 0), (푤 > 0) y 0 ≤ 푥 ≤ 1 para 1 ≤ 푖 ≤ 푛.

Utilizaremos un algoritmo voraz para resolverlo, para lo cual tendremos estas componentes:

Candidatos: Son los propios objetos. Solución: Es un conjunto (푥 ,푥 , … , 푥 ) que indica que fracción de cada

objeto hay que incluir. La solución será factible cuando se respeten las restricciones indicadas

antes. La función objetivo es el valor total de los objetos que están en la

mochila. La función de selección es la que quedaría por ver. Tendremos tres

posibles funciones de selección: - Seleccionar el objeto más valioso, argumentando que esto

incrementa el valor de la carga más rápido posible. - Seleccionar el objeto más pequeño restante, basándonos en que de

este modo la capacidad se agota de forma más lenta posible. - Seleccionar aquel objeto cuyo valor por unidad de peso sea el

mayor posible 푚푎푦표푟 .

Page 105: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 25 de 39

El algoritmo es:

funcion mochila (w[1..n], v[1..n], W): matriz [1..n] { Iniciación } para 푖 ← 1 hasta n hacer 푥[푖] ← 0

peso ← 0 { Bucle voraz } mientras 푝푒푠표 < 푊 hacer

푖 ← el mejor objeto restante { Si el objeto se puede incluir entero } si 푝푒푠표 + 푤[푖] ≤ 푊 entonces 푥[푖] ← 1 푝푒푠표 ← 푝푒푠표 + 푤[푖] { Si no se puede incluir entero, se fracciona } si no 푥[푖] ← ( )

[ ]

푝푒푠표 ← 푊 devolver x

Ejemplo de problema de la mochila: Se nos dan 5 objetos distintos, con estos pesos y valores. Además, el peso máximo será de 100. La tabla es la siguiente:

푛 = 5, 푊 = 100 w (pesos) 10 20 30 40 50 v (valores) 20 30 66 40 60

2.0 1.5 2.2 1.0 1.2 Resolveremos este algoritmo usando las tres funciones de selección antes puestas. Nos quedará: Seleccionar: xi Valor Max. vi 0 0 1 0.5 1 146 Min. wi 1 1 1 1 1 156 Max. 1 1 1 0 0.8 164 Observamos que la solución óptima es la tercera, puesto que es el de mayor valor, que era lo que nos pedían en el problema. Se ve que las otras funciones de selección no son óptimas. Si quisiéramos probarlo lo haríamos mediante un contraejemplo (importante para exámenes, que lo suelen pedir). Veremos el teorema que corrobora que esta última función de selección es óptima.

Page 106: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 26 de 39

푇푒표푟푒푚푎 6.5.1 Si se seleccionan los objetos por orden decreciente de entonces el algoritmo de la mochila encuentra una solución óptima. Demostración: Supongamos que los objetos disponibles están ordenados por orden decreciente de coste por unidad de peso:

≥ ≥ ⋯ ≥

Nuestro método para averiguarlo lo denominaremos reducción de diferencias.

Sea 푋 = (푥 ,푥 , … , 푥 ) la solución hallada por el algoritmo voraz. Se nos darán estos casos:

Si todos los 푥 son iguales a 1, entonces esta solución es claramente óptima.

En caso contrario, supongamos que j denota el menor índice tal que 푥 < 1. Examinando la forma en que funciona el algoritmo, está claro que:

1. 푥 = 1, cuando 푖 < 푗 2. 푥 = 0, cuando 푖 > 푗

y que ∑ 푥 ∗ 푤 = 푊.

Tendremos que 푉(푋) = ∑ 푥 ∗ 푣 es el valor de la solución X.

Ahora, sea 푌 = (푦 , 푦 , … , 푦 ) cualquier solución factible. Como Y es factible, ∑ 푦 ∗ 푤 ≤ 푊 y, por tanto, ∑ (푥 − 푦 ) ∗ 푤 ≥ 0. Tendremos que 푉(푌) =∑ 푦 ∗ 푣 es el valor de la solución Y. Multiplicando y dividiendo por 푤 , nos queda la resta de ambas soluciones:

푉(푋) − 푉(푌) = ∑ (푥 − 푦 ) ∗ 푣 = ∑ (푥 − 푦 ) ∗ 푤 ∗ .

Vemos de nuevo los casos posibles:

1. Cuando 풊 < 풋, 푥 = 1 y, por tanto, (푥 − 푦 ) es positivo o nulo, mientras que ≥ .

2. Cuando 풊 > 풋, 푥 = 1 y, por tanto, (푥 − 푦 ) es negativo o nulo, mientras que ≤ .

3. Cuando 풊 = 풋, = .

Por tanto, en todos los casos se tiene que (푥 − 푦 ) ∗ ≥ 푥 − 푦 ∗

Con lo que se deduce que:

푉(푋) − 푉(푌) ≥ ∗ ∑ (푥 − 푦 ) ∗ 푤 ≥ 0.

Por tanto, hemos demostrado que ninguna solución factible puede tener un valor mayor que V(푋), por lo que la solución X es óptima.

NOTA DEL AUTOR: Es una interpretación escrita de otra manera a la del libro, pero tomando el texto. Se distinguen casos para que sea más entendible.

Page 107: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 27 de 39

Análisis del coste: como en ocasiones anteriores, tendremos estos costes:

Calcular los tiene coste 푂(푛).

Ordenar de mayor a menor relación valor-peso : 푂(푛 ∗ log(푛)), pudiendo usar cualquier algoritmo para ello, heapsort, quicksort, etc.

El esquema voraz tiene coste 푂(푛), en el caso peor. Por tanto, la ordenación determina el coste del algoritmo, que sería por norma general 푶(풏 ∗ 퐥퐨퐠(풏)).

Mejora del algoritmo: Usaremos montículos de máximos, estando el mayor valor en la raíz. Las operaciones, como en ocasiones anteriores serán:

Crear montículo tiene coste 푂(푛). Para flotar o hundir requeriremos 푂(log(푛)), por lo que la propiedad del

montículo debe ser restaurada como máximo n veces (tantas como nodos haya).

En el caso peor, el coste será de 푂(푛 ∗ log(푛)).

En el caso mejor, es más rápido si solo se necesitan unos pocos objetos para llenar la mochila, será coste casi lineal (estimación del autor, no del libro): 푂(푛).

Page 108: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 28 de 39

6.6. Planificación.

Presentaremos dos problemas que conciernen a la forma óptima de planificar tareas en una sola máquina:

1. El problema consiste en minimizar el tiempo que invierte cada tarea en el sistema.

2. Las tareas tienen un plazo fijo de ejecución y cada tarea aporta unos ciertos beneficios sólo si está acabada al llegar al plazo: nuestro objetivo es maximizar la rentabilidad.

Pasamos a ver estos problemas con más detenimiento.

6.6.1. Minimización del tiempo en el sistema.

Un único servidor, como, por ejemplo, un dentista, un surtidor de gasolina o un cajero de un banco, tiene que dar servicio a n clientes. El tiempo requerido por cada cliente se conoce de antemano: el cliente i requerirá un tiempo 푡 para 1 ≤ 푖 ≤ 푛. Deseamos minimizar el tiempo medio requerido por cada cliente en el sistema. A ser el número de clientes predeterminado equivale a minimizar el tiempo total invertido en el sistema por todos los clientes. Por tanto, deseamos minimizar

푇 = ∑ (푡푖푒푚푝표 푒푛 푒푙 푠푖푠푡푒푚푎 푝푎푟푎 푒푙 푐푙푖푒푛푡푒 푖)

Ejemplo de minimización del tiempo en el sistema: Tenemos tres clientes 푡 = 5, 푡 = 10 y 푡 = 3. Existen 6 órdenes de servicio posibles:

Orden Tiempo total invertido en el sistema

1 2 3 5 + (5 + 10) + (5 + 10 + 3) = 38 1 3 2 5 + (5 + 3) + (5 + 3 + 10) = 31 2 1 3 10 + (10 + 5) + (10 + 5 + 3) = 43 2 3 1 10 + (10 + 3) + (10 + 3 + 5) = 41 3 1 2 3 + (3 + 5) + (3 + 5 + 10) = 29 Óptimo 3 2 1 3 + (3 + 10) + (3 + 10 + 5) = 34

En el primer orden, se sirve al cliente 1, el cliente 2 espera mientras se sirve al cliente 1 y entonces le llega el turno y el cliente 3 espera mientras se sirve a los clientes 1 y 2 y se le sirve en el último lugar. El tiempo total invertido en el sistema por los 3 clientes es de 38.

La planificación óptima se obtiene por orden creciente de tiempos de servicio: el cliente 3, que es el de menor tiempo de servicio es servido el primero, mientras que el cliente 2, que es el de mayor tiempo de servicio, es servido en último orden.

Page 109: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 29 de 39

Para ver esta idea de que puede ser óptimo planificar los clientes por orden creciente de tiempos de servicio, imaginemos un algoritmo voraz que construya la solución óptima elemento a elemento. Supongamos que después de planificar el servicio para los clientes 푖 , 푖 , … , 푖 se añade un cliente j. El incremento de tiempos en esta fase es igual a la suma de los tiempos de servicio para los clientes desde 푖 hasta 푖 más 푡 , que es el tiempo necesario para servir al cliente j. Para minimizar esto, debemos minimizar 풕풋. Nuestro algoritmo voraz es bastante sencillo: en cada paso se añade al final de la planificación al cliente que requiere el menor servicio entre los restantes.

푇푒표푟푒푚푎 6.6.1 El algoritmo voraz es óptimo.

Demostración: Sea 푃 = 푝 푝 … 푝 cualquier permutación de enteros del 1 al n y sea 푠 = 푡 . Si se sirven clientes en el orden P, entonces el tiempo requerido por el j-ésimo cliente que haya que servir será 푠 y el tiempo transcurrido en el sistema por todos los clientes es:

푇(푃) = 푠 + (푠 + 푠 ) + (푠 + 푠 + 푠 ) + ⋯+ (푠 + 푠 + 푠 + ⋯+ 푠 ) = = 푛 ∗ 푠 + (푛 − 1) ∗ 푠 + ⋯+ 2 ∗ 푠 + 푠 =

= ∑ (푛 − 푘 + 1) ∗ 푠 . Supongamos ahora que P no organiza a los clientes por orden de tiempos crecientes de servicio. Entonces, se pueden encontrar dos enteros a y b con 푎 < 푏 y 푠 > 푠 . Es decir, se sirven al cliente a-ésimo antes que al b-ésimo, aun cuando el primero necesite más tiempo de servicio que el segundo. Sería algo así: 1…a-1 a a+1…b-1 b b+1…n P

Si intercambiamos la posición de esos dos clientes, obtendremos un nuevo orden de servicio o permutación P’, que es simplemente el orden P después de intercambiar 푝 y 푝 : 1…a-1 a a+1…b-1 b b+1…n P

P’

El tiempo total transcurrido pasado en el sistema por todos los clientes si se emplea la planificación P’ es:

푇(푃′) = (푛 − 푎 + 1) ∗ 푠 + (푛 − 푏 + 1) ∗ 푠 + ∑ (푛 − 푘 + 1),

∗ 푠

La nueva planificación es preferible a la vieja, porque:

푇(푃) − 푇(푃 ) = (푛 − 푎 + 1) ∗ (푠 − 푠 ) + (푛 − 푏 + 1) ∗ (푠 − 푠 ) = = (푏 − 푎) ∗ (푠 − 푠 ) > 0

Page 110: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 30 de 39

Se observa tras el intercambio que los clientes salen en su posición adecuada, ya que 푠 < 푠 por nuestra suposición inicial, estando el resto ordenados. Por tanto, P’ es mejor que P en conjunto.

De esta manera, se puede optimizar toda planificación en la que se sirva a un cliente antes que requiera menos servicio. Las únicas planificaciones que quedan son aquellas que se obtienen poniendo a los clientes por orden creciente de tiempo de servicio. Todas las planificaciones son equivalentes y, por tanto, todas son óptimas. La implementación de este algoritmo es sencilla.

Análisis del coste: tendremos este coste del algoritmo:

- Ordenar los elementos por orden de tiempos creciente (no decreciente, usando cualquier algoritmo de ordenación (montículo, quicksort, …): 푂(푛 ∗ log(푛))

- Coste del algoritmo voraz: 푂(푛).

Podemos mejorarlo usando montículos invertidos (de mínimos):

Crear montículo: 푂(푛) Restaurar la condición del montículo n veces: 푂(푛 ∗ log(푛))

Por tanto, el coste asintóticamente coincidirá en ambas y será 푶(풏 ∗ 퐥퐨퐠(풏)).

6.6.2. Planificación con plazo fijo. Tenemos que ejecutar un conjunto de n tareas, cada una de las cuales requiere un tiempo unitario. En cualquier instante 푇 = 1, 2, … , 푛 podemos ejecutar únicamente una tarea. La tarea i nos produce unos beneficios 푔 > 0 sólo en el caso en que sea ejecutada en un instante anterior a 푑 .

En resumen: n: Número de tareas de tiempo unitario. Por ejemplo, una hora, días,… 푇 = 1, 2, … ,푛 En cada instante solo podemos realizar una tarea. 품풊: Beneficio asociado a la tarea i.

풅풊: Plazo máximo de la tarea i.

El problema consiste en maximizar el beneficio total.

Un ejemplo de este algoritmo será: i 1 2 3 4 푔 50 10 15 30

푑 2 1 2 1

Page 111: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 31 de 39

Las planificaciones que hay que considerar y los beneficios correspondientes son: Secuencia Beneficio Secuencia Beneficio

1 50 2, 1 60 2 10 2, 3 25 3 15 3, 1 65 4 30 4, 1 80 Óptima 1,3 65 4, 3 45

Puede haber tareas que se queden sin realizar. Tendremos, por tanto:

Conjunto de candidatos: Las tareas. Conjunto factible: Se dice que un conjunto de tareas es factible si

existe, al menos, una sucesión de sus tareas que permite que todas ellas se ejecute dentro de plazo.

Función de selección: Un algoritmo voraz evidente consiste en construir la planificación paso a paso, añadiendo en cada paso la tarea que tenga el mayor valor de 푔 (ganancia) y cuando el conjunto de tareas seleccionadas siga siendo factible.

Nuestra solución óptima es ejecutar las tareas en el orden 4, 1. Queda por demostrar que este algoritmo siempre encuentra una planificación óptima, y además hay que buscar una forma eficiente de implementarlo.

Sea J un conjunto de tareas. Necesitamos probar las 푘! permutaciones de estas tareas para ver si J es factible. Vemos un lema que nos indicará que esto no es así:

퐿푒푚푎 6.6.2 Sea J un conjunto de k tareas. Supongamos que las tareas están

numeradas de tal forma que 푑 ≤ 푑 ≤ ⋯ ≤ 푑 . Entonces el conjunto J es

factible si y sólo si la secuencia 1, 2, … ,푘 es factible.

Demostración (por contradicción): El “si” ( ⇒) es evidente. Para el “sólo

si” ( ⇐), supongamos que la secuencia 1, 2, … , 푘 no es factible. Entonces, al

menos, una de estas tareas se planifica después del plazo. Sea r cualquiera de estas tareas, de tal manera que 푑 ≤ 푟 − 1. Dado que las tareas se planifican por orden no decrecientes (crecientes) de plazos, esto significa que, al menos, r tareas tienen como fecha final 푟 − 1 o anterior. Sea cual fuere la forma en que se planifiquen, la última siempre llegará tarde, es decir, se saldrá del plazo. Esto demuestra que basta comprobar una sola secuencia, en orden no decreciente, para saber si un conjunto de tareas J es o no factible. NOTA DEL AUTOR: Lo de la demostración por contradicción, de nuevo, es un añadido, por el texto de la propia demostración.

Page 112: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 32 de 39

푇푒표푟푒푚푎 6.6.3 El algoritmo voraz esbozado anteriormente siempre

encuentra solución óptima.

Demostración: Supongamos que el algoritmo voraz decide ejecutar un conjuntos de tareas I, y supongamos que el conjunto J es óptimo. Sean 푆 y 푆 secuencias factibles, que posiblemente incluyan huecos, para los dos conjuntos de tareas en cuestión. Gráficamente, sería esto:

푆 Solución del algoritmo voraz

푆 Conjunto óptimo

Reorganizando las tareas de 푆 y 푆 , podemos obtener dos secuencias factibles 푆′ y 푆′ , que también pueden contener huecos tales que toda tarea común a I y a J se planifique en el mismo instante en ambas secuencias. Tras reorganizar las tareas quedaría así:

Si esta tarea es a

푆′

푆′

Esta será b Tareas comunes

Para ver esto, imaginemos que alguna tarea a aparece en las dos secuencias factibles 푆 y 푆 , en donde queda planificada en los instantes 푡 y 푡 , respectivamente. Se nos darán estos casos:

Si 풕푰 = 풕푱 no hay nada que hacer, ya que coinciden ambas tareas en tiempo (apreciación del autor).

En caso contrario, supongamos que 풕푰 < 풕푱 (es decir, se ejecuta antes la misma tarea para la secuencia 푆 que para la 푆 , apreciación del autor). Dado que la secuencia 푆 es factible, se sigue que el plazo para la tarea a no es anterior a 푡 . Se modifica la secuencia 푆 de la siguiente manera:

- Si hay un hueco en la secuencia 푆 en el instante 푡 , se atrasa la tarea a del instante del instante 푡 al hueco en el instante 푡 .

- Si hay una tarea b planificada en 푆 en el instante 푡 , se intercambian las tareas a y b en la secuencia 푆 .

La secuencia resultante sigue siendo factible, puesto que, en cualquier caso, a se ejecutará antes de su plazo y en el segundo caso el traslado de b a un instante anterior, no puede causar daños. Ahora, se planifica a en un instante 푡 en las dos secuencias modificadas 푆 y 푆 .

p y q x r

r s t p u v q w

x y p r q

u s t p r v q w

Page 113: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 33 de 39

Se puede aplicar un argumento similar cuando 풕푰 > 풕푱 salvo que, en este caso, es 푆 quien debe ser modificada.

Una vez que se ha tratado una tarea a de esta manera, está claro que nunca será preciso volver a trasladarla. Por tanto, si las secuencias 푆 y 푆 tiene m tareas en común, después de un máximo de m modificaciones de 푆 o de 푆 podemos asegurar que todas las tareas comunes a I y a J estarán planificadas al mismo tiempo en ambas secuencias. Las secuencias resultantes 푆′ y 푆′ pueden no ser iguales si 푖 ≠ 푗. Por tanto, supongamos que existe un instante en el cual la tarea planificada en 푆 es distinta de la planificada en 푆′ .

o Si alguna tarea a está planificada en 푆′ frente a un hueco de 푆′ , entonces a no pertenece a 퐽. El conjunto 퐽 ∪ {푎} es factible, porque podríamos poner a en el hueco y seria más rentable que 퐽. Esto es imposible, puesto que 퐽 es óptimo por hipótesis.

o Si alguna tarea b está planificada en 푆′ frente a un hueco 푆′ , el conjunto 퐼 ∪ {푏} sería factible, así que el algoritmo voraz habría incluido a b en I. Esto también es imposible, porque no lo hizo.

o La única posibilidad restante es que alguna tarea a esté planificada en 푆′ al lado de una tarea distinta b en 푆′ (como las tareas y y s en el dibujo anterior). En este caso, a no aparece en J y b no aparece en I. Aparentemente, hay 3 posibilidades:

Si 푔 > 푔 (la ganancia de la tarea a es mayor que la del b), se podría sustituir a por b en J y mejorarla. Esto es imposible, porque J es óptima.

Si 푔 < 푔 , el algoritmo voraz habrá seleccionado a b antes de considerar a a, puesto que (퐼\{푎}) ∪ {푏} sería factible. Esto es imposible, puesto que el algoritmo no incluyó a b en I.

La única posibilidad restante es que 푔 = 푔 .

Concluimos que para toda posición temporal las secuencias 푆′ y 푆′ , o bien:

No planifican bien las tareas. Planifican la misma tarea. Planifican dos tareas distintas que producen idéntico beneficio.

El beneficio total de I es igual al beneficio del conjunto óptimo J, así que J es óptimo.

Page 114: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 34 de 39

Implementaciones: Tendremos dos tipos:

1ª implementación: Suponemos que las tareas están numeradas de tal manera que 푔 ≥ 푔 ≥ ⋯ ≥ 푔 , además que 푛 > 0 y 푑 > 0, para 1 ≤ 푖 ≤ 푛. Añadimos una posición en el vector que será nuestro “centinela”, usados para evitar comprobaciones repetitivas de rangos que consumen mucho tiempo.

funcion secuencia (푑[0. . 푛]): k, matriz [1. . 푘] matriz j[0. .푛]

{ La planificación se construye paso a paso en la matriz j. La variable k dice cuantas tareas están ya en la planificación } 푑[0] ← 푗[0] ← 0 { Centinelas } 푘 ← 푗[1] ← 1 { La tarea 1 siempre se selecciona } { Bucle voraz } para 푖 ← 2 hasta n hacer { Orden decreciente de g } 푟 ← 푘

mientras 푑 푗[푟] > 푚푎푥(푑[푖], 푟) hacer 푟 ← 푟 − 1 si 푑[푖] > 푟 entonces

para 푚 ← 푘 paso −1 hasta 푟 + 1 hacer 푗[푚 + 1] ← 푗[푚];

푗[푟 + 1] ← 푖 푘 ← 푘 + 1; devolver 푘, 푗[1. .푘]

Las k tareas de la matriz j están por orden creciente de plazo. Cuando se está considerando la tarea i, el algoritmo comprueba si se puede insertar en j en lugar oportuno sin llevar alguna tarea que ya está en j más allá de su plazo. De ser así, i se acepta; en caso contrario, i se rechaza.

Las tareas están numeradas por orden decreciente de beneficios.

Un ejemplo de este algoritmo será: i 1 2 3 4 5 6 푔 20 15 10 7 5 3 푑 3 1 1 3 1 3

Page 115: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 35 de 39

Observamos que ya están ordenados por orden decreciente de beneficios, tal y como dijimos antes (es importante). Los pasos serán: 1 2 3 4 5 6 Inicialmente: Primer intento: Segundo intento: Sin cambios Tercero intento: Cuarto intento: Sin cambios Quinto intento: Sin cambios

La secuencia óptima es la 2, 1, 4 con valor 42 Análisis del coste: tendremos como en ocasiones anteriores estos pasos:

- Ordenación de las tareas: 푂(푛 ∗ log(푛)). Recordemos que se puede emplear cualquier algoritmo, como el de heapsort, quicksort, etc.

- Algoritmo voraz: En el caso peor, las tareas están clasificadas por orden decreciente de plazos. En este caso, cuando se está considerando la tarea i, el algoritmo examina las 푘 = 푖 − 1 tareas ya planificadas, para encontrar un lugar para el recién llegado y luego las desplaza todas un lugar.

El algoritmo requiere, por tanto, un tiempo está en Ω(푛 ). Es, por tanto, ineficiente, por ver donde “insertamos” la tarea en cuestión.

2ª implementación: Lo veremos mediante un lema, que será:

퐿푒푚푎 6.6.4 Un conjunto J de n tareas es factible si y sólo si se puede

construir una secuencia factible que incluya a todas las tareas de J en la

forma siguiente. Se empieza por una planificación vacía, de longitud n.

entonces para cada tarea 푖 ∈ 퐽 sucesivamente, se planifica i en el instante

푡 en donde t es el mayor entero tal que 1 ≤ 푡 ≤ 푚푖푛(푛,푑 ) y la tarea que

se ejecuta en el instante t no está decidida todavía.

En otras palabras, se empieza por una planificación vacía, se considera cada tarea sucesivamente, y se añade a la planificación que se está construyendo en el momento más tardío posible (mucho cuidado, que es básico para está implementación), pero no antes de su fecha final.

1

2 1

2 1 4

Page 116: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 36 de 39

Demostración: El “si” es evidente. Para el “sólo si”; obsérvese primero que si existe una secuencia factible, entonces existe una secuencia factible de longitud n (número de tareas). Puesto que sólo hay n tareas por planificar, toda secuencia más larga tendrá que contener huecos y siempre se puede trasladar una tarea a un hueco anterior sin afectar a la factibilidad de la secuencia. Cuando se intenta añadir una nueva tarea, la secuencia que se está construyendo contiene siempre al menos un hueco. Supongamos que no se puede añadir una tarea cuyo plazo sea d. Esto puede suceder solamente si todas las posiciones desde 푡 = 1 hasta 푡 = 푟 están ya reservadas, en donde 푟 = 푚푖푛(푛,푑). Sea 푠 > 푟 el menor entero tal que la posición 푡 = 푠 está vacía. La planificación que ya se ha construido incluye, por tanto, 푠 − 1 tareas, ninguna tarea con plazo exactamente a s y quizá otra más con plazos posteriores a s. Por tanto, J contiene al menos s tareas cuyos plazos son 푠 − 1 o anteriores. Sea cual fuere la forma en que se planifiquen, la última llegará tarde con certeza. La situación quedaría así:

s D D D

Hemos marcado con una D las tareas ya decididas. Pondremos la tarea lo más tarde posible. Por ejemplo, si tenemos que la tarea tiene plazo 5, lo pondremos en la posición 4 (en la flecha) y no en 1.

El lema sugiere que deberíamos considerar un algoritmo que intente llenar una por una las posiciones de una secuencia de longitud p, donde 푝 = 푚푖푛(푛,푚푎푥 푑 ). Para cualquier posición t, se define:

푛 = 푚á푥{푘 ≤ 푡| 푙푎 푝표푠푖푐푖표푛 푘 푛표 푒푠푡푎 푑푒푐푖푑푖푑푎 푡표푑푎푣푖푎} Se definen ciertos conjuntos de posiciones en la forma siguiente: Dos posiciones i y j están en el mismo conjunto si 푛 = 푛

Posiciones del mismo conjunto

D D D D 푛 = 푛

Al igual que antes, D indica que la posición está ocupada (la tarea ya decidida), mientras que la que está en blanco la posición está libre.

A medida que se asignan nuevas tareas a posiciones vacantes, los conjuntos se fusionan para formar conjuntos más grandes, para ello se usan estructuras de partición.

Para un conjunto dado K de posiciones, sea 퐹(퐾) el menor elemento de K. finalmente, se define una posición ficticia cero, que siempre está libre.

Page 117: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 37 de 39

El algoritmo será el siguiente:

i. Iniciación: Toda posición 0,1,2, … , 푝 está en un conjunto diferente y 퐹([푖]) = 푖, 0 ≤ 푖 ≤ 푝. 푝 = 푚푖푛 푛,푚푎푥(푑 ) . Mayor de los plazos Número de tareas

La posición 0 sirve para ver cuando la planificación está llena.

ii. Adición de una tarea con plazo d; se busca un conjunto que contenga a d; sea K este conjunto. Si 퐹(퐾) = 0 se rechaza la tarea, en caso contrario:

- Se asigna la nueva tarea a la posición 퐹(퐾). - Se busca el conjunto que contenga 퐹(퐾)− 1. Llamemos L

a este conjunto (no puede ser igual a K). - Se fusionan K y L. El valor de F para este nuevo conjunto

es el valor viejo de 퐹(퐿). Tendremos un enunciado más preciso del algoritmo rápido. Para simplificar la descripción, suponemos que la etiqueta del conjunto producido por una operación de fusionar es necesariamente la etiqueta de uno de los conjuntos que hayan sido fusionados. La planificación en primer lugar puede contener huecos; el algoritmo acaba por trasladar tareas hacia delante para llenarlos.

funcion secuencia2 (푑[1. . 푛]): k, matriz [1. . 푘] matriz j, F[0. .푛]

{ Iniciación } 푝 = 푚푖푛(푛,푚푎푥{푑[푖]|1 ≤ 푖 ≤ 푛}); para 푖 ← 0 hasta p hacer 푗[푖] ← 0 퐹[푖] ← 푖

Iniciar el conjunto {푖} { Bucle voraz } para 푖 ← 1 hasta n hacer { Orden decreciente de g } 푘 ← 푏푢푠푐푎푟 푚푖푛(푝, 푑[푖]) 푚 ← 퐹[푘]

si 푚 ≠ 0 entonces 푗[푚] ← 푖; 푙 ← 푏푢푠푐푎푟(푚 − 1) 퐹[푘] ← 퐹[푙] { El conjunto resultante tiene la etiqueta k o l } fusionar (푘, 푙) { Sólo queda comprimir la solución } 푘 ← 0

para 푖 ← 1 hasta p hacer si 푗[푖] > 0 entonces 푘 ← 푘 + 1 푗[푘] ← 푗[푖]

devolver 푘, 푗[1. .푘]

Page 118: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 38 de 39

Ejemplo de la segunda implementación, siendo el mismo ejemplo anterior:

i 1 2 3 4 5 6 푔 20 15 10 7 5 3 푑 3 1 1 3 1 3

De nuevo, están ordenados por orden decreciente de ganancias. Los pasos son los siguientes:

Inicialmente: 푝 = 푚푖푛 푛,푚푎푥(푑 ) = 푚푖푛(6,3) = 3. Por tanto, como máximo tendremos una planificación de 3 tareas: Primer intento: 푑 = 3. Se asigna la tarea 1 a la posición 3. 퐹(퐾) = 3, 퐹(퐿) = 퐹(퐾)− 1 = 2. Fusionamos K con L Segundo intento: 푑 = 1. Se asigna la tarea 2 a la posición 1. 퐹(퐾) = 1, 퐹(퐿) = 퐹(퐾)− 1 = 0. Fusionamos K con L

Tercer intento: 푑 = 1. No hay posiciones libres disponibles porque el valor de F es 0.

Cuarto intento: 푑 = 3. Se asigna la tarea 4 a la posición 3.

퐹(퐾) = 1, 퐹(퐿) = 퐹(퐾)− 1 = 0. Fusionamos K con L

Quinto y sexto intento: No hay posiciones libres disponibles.

La secuencia óptima es la 2, 4, 1 con valor 42.

0 1 3 2

0 1

3

2

0

1 3

2

0

1

3

2

Page 119: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 6 Curso 2007/08 Página 39 de 39

Análisis del coste: Usaremos operaciones de conjuntos disjuntos, en la que la hay que ejecutar, como máximo, 2 ∗ 푛 operaciones buscar y n operaciones fusionar, por lo que el tiempo requerido está en 푂 푛 ∗훼(2 ∗ 푛, 푛) , por tanto, tiene coste lineal 푂(푛) .

En el caso peor, en el que suponemos que todas las tareas están desordenadas, habría que ordenarlas, por lo que, de nuevo, el coste es 푶(풏 ∗ 퐥퐨퐠(풏)), que determinará el coste del algoritmo (apreciación del autor).

Page 120: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

 

Page 121: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Alumna: Alicia Sánchez Centro: UNED-Las Rozas (Madrid)

Resumen de programación 3

Tema 7. Divide y vencerás.

Page 122: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 2 de 30

Índice:

7.1. Introducción: multiplicación de enteros muy grande ……………. 3 7.2. El caso general …………………………………………………… 8 7.3. Búsqueda binaria …………………………………………………. 9 7.4. Ordenación ……………………………………………………… 12

7.4.1. Ordenación por fusión (mergesort) ………………………... 12 7.4.2. Ordenación rápida (quicksort) …………………………….. 16

7.5. Búsqueda de la mediana ………………………………………… 21 7.6. Multiplicación de matrices ……………………………………… 23 7.7. Exponenciación …………………………………………………. 24

Bibliografía:

Se han tomado apuntes de los libros:

Fundamentos de algoritmia. G. Brassard y P. Bratley Estructuras de Datos y Algoritmos. R. Hernández

Page 123: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 3 de 30

Previo a ver el capitulo explicaré una serie de características que tiene este resumen:

1. la primera de ellas es que la multiplicación, a diferencia de otros documentos pondremos la multiplicación o bien con ∗ o sin ningún signo intermedio. Es simplemente un apunte, sin ninguna importancia conceptual.

2. La segunda de ellas es que hemos omitido el último punto del temario, el 7.8, que trata de la encriptación, ya que lo veremos en la sección de ejercicios, por lo que omitiremos dar más detalles.

3. La tercera de ellas es que debido al evidente cansancio del autor al hacer los documentos y al intento por resumirlos se ha intentado explicar de modo adecuado el punto 7.7, el de la exponenciación, aunque no sé si habrá quedado claro.

4. Por último, decir que la parte de la búsqueda binaria, así como las distintas ordenaciones son muy importantes, por haber entrado en los últimos exámenes, de los años 2007-08, sin desatender las otras partes del tema.

Una vez comentado estos puntos pasamos a ver el resumen de este tema. Divide y vencerás es una técnica para diseñar algoritmos que consiste en:

- Descomponer el caso que haya que resolver en un cierto número de subcasos más pequeños del mismo problema.

- Resolver sucesiva e independientemente todos estos subcasos. - Combinar después las soluciones obtenidas de esta manera para obtener la

solución del caso original.

7.1. Introducción: multiplicación de enteros muy grandes Recordaremos el método clásico de multiplicación visto en el tema 1, la cual era simplemente la multiplicación lo más sencilla posible. Un ejemplo podrá ser:

a b c d e f g h

ℎ ∗ 푎 ℎ ∗ 푏 ℎ ∗ 푐 ℎ ∗ 푑 푔 ∗ 푑 + +

Como vemos, hemos hecho la multiplicación de h con el resto de los números de la primera fila. Iremos desplazando una posición a la izquierda al seguir multiplicando los demás números.

Por tanto, hacemos 푛 ∗ 푛 multiplicaciones y cerca de 2 ∗ 푛 sumas, considerándolas operaciones elementales, que es aquella cuyo tiempo de ejecución puede ser acotada superiormente por una constante que sólo dependerá de la implementación particular usada: de la máquina, del lenguaje de programación, etc. (recordatorio del resumen del tema 2).

Por ello, el coste del método clásico es 휽(풏ퟐ).

Page 124: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 4 de 30

Otro algoritmo para la multiplicación de enteros que discutimos en el tema 1 es que llamábamos técnica de “divide y vencerás”, que consistía en reducir la multiplicación de dos números de n cifras a 4 multiplicaciones de números de 푛/2 cifras. Un ejemplo de esto podrá ser la multiplicación de 0981 x 1234, que veremos paso a paso:

1. Descompondremos ambas cifras en números de longitud mitad, como sigue:

w = 09 y = 12 x = 81 z = 34

2. Una vez descompuestos los dos valores (nos fijaremos que los rellenaremos con ceros a la izquierda en caso de ser de cifras impares), tenemos lo siguiente:

0981 x 1234 = (9 ∗ 10 + 81) 푥 (12 ∗ 10 + 34) 3. Sustituiremos la descomposición anterior las variables antes deducidas

(w, y, x y z), por lo que tenemos:

0981 x 1234 = (9 ∗ 10 + 81) 푥 (12 ∗ 10 + 34) = = (푤 ∗ 10 + 푥) 푥 (푦 ∗ 10 + 푧)

4. Por último, desarrollaremos los paréntesis de esta manera: (푤 ∗ 10 + 푥) 푥 (푦 ∗ 10 + 푧) = = 푤 ∗ 푦 ∗ 10 + (푤 ∗ 푧 + 푥 ∗ 푦) ∗ 10 + 푥 ∗ 푧.

Queda por decir que los números elevados a 10 indican desplazamientos de las cifras, siendo 10 desplazamiento de 4 cifras (a la izquierda) y 10 de 2.

Tendremos, por tanto, la siguiente ecuación de recurrencia que resuelve este problema:

푡(푛) = 4 ∗ 푡(푛/2) + 푂(푛) Está ecuación de recurrencia indica que hay 4 subproblemas mitad (las correspondientes a x, y, w y z) y además, el coste de las sumas y los desplazamientos es lineal 푂(푛) . Por ello, las variables (del tema 4) para resolver la recurrencia, siendo de reducción por división serán:

a: Número de llamadas recursivas = 4 b: Reducción del problema en cada llamada = 2

풄 ∗ 풏풌: Coste de las operaciones extras a las llamadas recursivas. Será, en este caso, 푐 ∗ 푛 = 푛

⇒푘 = 1

Nuestra resolución de la reducción por división tiene 3 casos distintos, que son:

휃(푛 ) si 푎 < 푏 푇(푛) = 휃 푛 ∗ 푙표푔(푛) si 푎 = 푏

휃 푛 si 푎 > 푏

Page 125: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 5 de 30

Sustituimos las variables en la igualdad 푎 = 푏 , teniendo 4 > 2 , que equivaldría al tercer caso. Por ello, el coste del algoritmo es 푇(푛) ∈ 휃 푛 =휃 푛 = 휃(푛 ).

Para mejorar el algoritmo clásico debemos encontrar una forma de reducir la multiplicación original no a 4 multiplicaciones si no a 3 de números de tamaño mitad (푛/2). Para ello, tenemos:

푟 = (푤 + 푥) ∗ (푦 + 푧)

푝 = 푤 ∗ 푦

푞 = 푥 ∗ 푧 Desarrollamos r, como sigue:

푟 = (푤 + 푥) ∗ (푦 + 푧) = 푤 ∗ 푦 + 푤 ∗ 푧 + 푥 ∗ 푦 + 푥 ∗ 푧 =

= 푤 ∗ 푦 + (푤 ∗ 푧 + 푥 ∗ 푦) + 푥 ∗ 푧

Despejamos (푤 ∗ 푧 + 푥 ∗ 푦) y queda:

(푤 ∗ 푧 + 푥 ∗ 푦) = 푟 − 푤 ∗ 푦 − 푥 ∗ 푧

Sustituimos las variables anteriores y tendremos lo siguiente:

(푤 ∗ 푧 + 푥 ∗ 푦) = 푟 − 푝 − 푞

Para nuestro caso particular, lo veremos mediante un ejemplo:

푝 = 푤 ∗ 푦 = 09 ∗ 12 = 108

푞 = 푥 ∗ 푧 = 81 ∗ 34 = 2754

푟 = (푤 + 푥) ∗ (푦 + 푧) = 90 ∗ 46 = 4140 Y, finalmente, reescribiendo la formula anterior, la multiplicación quedaría:

0981 ∗ 1234 = 10 ∗ 푝 + 10 ∗ (푟 − 푝 − 푞) + 푞 Para verificar que efectivamente es correcta la multiplicación sería:

0981 ∗ 1234 = 10 ∗ 108 + 10 ∗ (4140− 108− 2754) + 2754 = = 1080000 + 127800 + 2754 = 1210554

Concluyendo en la misma solución que vimos con la multiplicación empleando el modo tradicional (el ya visto en las paginas anteriores).

Cuando los operandos son muy grandes, el tiempo requerido para las sumas es despreciable frente al tiempo que requiere una sola multiplicación. En este caso simple (el de nuestro ejemplo), nos dará igual quitar una multiplicación y hacer una suma.

Page 126: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 6 de 30

El tiempo para multiplicar 2 números de n cifras usando el algoritmo clásico empleando esta última técnica (de 3 multiplicaciones) será:

3 ∗ ℎ(푛/2) + 푔(푛) = 3 ∗ 푐 ∗ (푛/2) + 푔(푛) = ∗ 푐 ∗ 푛 + 푔(푛) =

= ∗ ℎ(푛) + 푔(푛).

siendo:

풉(풏): Tiempo de la implementación dada del algoritmo clásico = 푐 ∗ 푛 품(풏): Tiempo necesario para las sumas, desplazamientos y operaciones adicionales 푔(푛) ∈ 휃(푛).

푔(푛) resultará despreciable frente a ℎ(푛), cuando n sea suficientemente grande, lo que significa que hemos ganado aproximadamente un 25% de velocidad en comparación con el algoritmo clásico (en el que hacíamos 4 multiplicaciones). Aun así, el nuevo algoritmo tiene coste cuadrático de nuevo.

Usando el algoritmo de forma recursiva, la ecuación de recurrencia (o el tiempo) será:

푡(푛) = 3 ∗ 푡(푛/2) + 푔(푛) Tendremos los siguientes datos:

a: Número de llamadas recursivas = 3 b: Reducción del problema en cada llamada = 2

풄 ∗ 풏풌: Coste de las operaciones extras a las llamadas recursivas. Como hemos visto en el método clásico anterior, tendremos que 푔(푛) ∈ 휃(푛) ⇒ 푘 = 1

Al igual que antes y de nuevo insistiremos (hay que dar el latazo con estas formulas, así sí que se aprenden bien), la ecuación es de reducción por división y la resolución de dicha recursividad es:

휃(푛 ) si 푎 < 푏 푇(푛) = 휃 푛 ∗ 푙표푔(푛) si 푎 = 푏

휃 푛 si 푎 > 푏

Como vimos en el caso de 4 multiplicaciones, tendremos que resolver la ecuación y luego ver que inecuación es la adecuada 푎 = 푏

⇒ 3 > 2

Por ello, de nuevo será el tercer caso y el coste es 푇(푛) ∈ 휃 푛 =휽 풏퐥퐨퐠ퟐ ퟑ .

NOTA DEL AUTOR: No sé muy bien como separar dichos algoritmos clásicos, ya que nos guiamos por el libro de Brassard y no lo separan con el orden “lógico” que se quisiera. Por tanto, tendremos dos algoritmos clásicos, uno, el básico (el más burdo), de 4 multiplicaciones y otro, una mejora, de 3 multiplicaciones.

Page 127: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 7 de 30

Esta mejora merecerá la pena, ya que este algoritmo puede multiplicar dos enteros muy grandes más deprisa que el algoritmo clásico de multiplicación (insistimos, el primer algoritmo clásico, el de las 4 multiplicaciones). El algoritmo de divide y vencerás (el último que hemos visto) puede resultar más lento que el clásico cuando los enteros son demasiados pequeños. Por tanto, un algoritmo de divide y vencerás debe de evitar seguir avanzando recursivamente cuando el tamaño de los casos ya no lo justifique.

Cuando los números tienen longitud impar se pueden multiplicar fácilmente partiéndolos del modo más equitativo posible: un número de n cifras se parte en ⌊푛/2⌋ cifras (redondeo por abajo) y otro de ⌈푛/2⌉ cifras.

Si fuera el tamaño de las sumas (푤 + 푥 y 푦 + 푧) impar, éstas no podrán sobrepasar del tamaño 1 + ⌈푛/2⌉. Por ejemplo, si fuera la multiplicación de 5678 y 6789, el valor de 푟 = (푤 + 푥) ∗ (푦 + 푧) = 134 ∗ 156. Para multiplicar números de distintos tamaños podremos emplear estos algoritmos posibles:

- Si m y n no difieren en más de un factor 2 (es decir, como en nuestro caso de 981 y 1234), lo mejor es rellenar el operando más pequeño con ceros (sería el operando 981) para hacerlo de longitud igual a la del otro operando. El algoritmo de divide y vencerás utilizado con relleno está en 휃 푛 y el algoritmo clásico requiere un tiempo 휃(푚푛) para calcular la multiplicación de 2 enteros. La constante oculta del primero tiene más posibilidades de ser más grande que la del segundo. Vemos, por tanto, que el algoritmo de divide y vencerás con relleno es más lento que el clásico cuando 푚 < 푛 ( / ).

- Para obtener un algoritmo realmente mejor, la idea es segmentar el operando más largo, v, en bloques de tamaño m y utilizar entonces el algoritmo de divide y vencerás para multiplicar u por cada bloque v, de tal manera que se multiplicarán parejas de operandos de igual tamaño. El tiempo total de ejecución necesario para multiplicar un número de n cifras por un número de m cifras está en 휃 푛푚 ( / ) .

Page 128: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 8 de 30

7.2. El caso general

Tendremos el siguiente esquema, que está sacado del libro de problemas, ya que creo que es más claro como lo ponen allí que en el libro de teoría (Brassard):

fun divide-y-vencerás (problema) si suficientemente-simple (problema) entonces dev solucion-simple (problema) si no { No es solución suficientemente simple } {푝 . .푝 } ← decomposicion (problema) para cada 푝 hacer 푠 ← divide-y-vencerás (푝 ) fpara dev combinacion (푠 … 푠 ) fsi ffun

Las funciones que han de particularizarse son: - suficientemente-simple: Decide si un problema está por debajo del

tamaño umbral o no. - solucion-simple: Algoritmo para resolver los casos más sencillos, por

debajo del tamaño umbral. - descomposicion: Descompone el problema en subproblemas en tamaño

menor. - combinacion: Algoritmo que combina las soluciones a los subproblemas

en solución al problema del que provienen. Algunos algoritmos de divide y vencerás no siguen exactamente este esquema, puesto que hay casos en los que no tiene sentido reducir la solución de un caso muy grande a la de uno más pequeño. Entonces, divide y vencerás recibe el nombre de reducción (simplificación). Para que el enfoque de divide y vencerás merezca la pena es necesario que se cumplan estas tres condiciones:

1. La decisión de utilizar el subalgoritmo básico (suficientemente-simple) en lugar de hacer llamadas recursivas debe tomarse cuidadosamente.

2. Tiene que ser posible descomponer el ejemplar y en subejemplares y recomponer las soluciones parciales de forma bastante eficiente.

3. Los subejemplares deben ser en la medida de lo posible aproximadamente del mismo tamaño.

Tendremos que decidir la forma en la que dividir el caso y hacer llamadas recursivas o si el caso es tan sencillo que resulte mejor invocar directamente el subconjunto básico. Esta decisión se basa en un sencillo umbral, llamado 푛 . El subalgoritmo básico se emplea para resolver todos aquellos casos cuyo tamaño no supere 푛 .

La selección del mejor umbral se ve complicada por el hecho de que el valor óptimo depende en general no sólo del algoritmo en cuestión, sino también de la implementación particular.

Page 129: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 9 de 30

7.3. Búsqueda binaria

Probablemente se trate de la aplicación más sencilla de divide y vencerás, tan sencilla que hablando con propiedad se trata de una aplicación de reducción (simplificación) más que de divide y vencerás.

Se nos da un vector 푇[1. .푛] ordenado por orden creciente, esto es, 푇[푖] ≤ 푇[푗] siempre que 1 ≤ 푖 ≤ 푗 ≤ 푛. Sea x un elemento. El problema consiste en buscar x en la matriz T, si es que está. Formalmente, deseamos encontrar el índice i tal que 1 ≤ 푖 ≤ 푛 + 1 y 푇[푖 − 1] < 푥 < 푇[푖], con la convención lógica consistente en que 푇[0] = −∞ y 푇[푛 + 1] = +∞. La primera aproximación consiste en examinar secuencialmente los elementos de T, hasta que o bien lleguemos al final de la matriz o bien encontramos un elemento que no sea menor que x:

fun secuencial (푇[1. .푛], x) { Búsqueda secuencial de x en una matriz } para 푖 ← 1 hasta n hacer si 푇[푖] ≥ 푥 entonces devolver i devolver 푛 + 1;

Este algoritmo requiere un tiempo que está en 휃(푟), donde r es el índice que se devuelve. En el caso peor y en el promedio, la búsqueda secuencial requiere un tiempo que está en 휽(풏), porque el número medio de pasadas por el bucle es

.

Para acelerar la búsqueda deberíamos buscar x en la primera mitad de la matriz o en la segunda. Para averiguar cuál de estas búsquedas es la correcta, comparamos x con un elemento de la matriz. Sea 푘 = ⌈푛/2⌉. Si 푥 ≤ 푇[푘], entonces se puede restringir la búsqueda de x a 푇[1. .푘] (mitad izquierda); en caso contrario, basta con buscar en 푇[푘 + 1. . 푛] (mitad derecha). Para evitar comparaciones repetitivas en cada llamada recursiva, es mejor verificar desde un comienzo si la respuesta es 푛 + 1, esto es, si x está a la derecha de T.

NOTA DEL AUTOR (IMPORTANTE): En exámenes se verá que pidan que se detecte si el elemento x está fuera de la matriz, esto quiere decir la anterior expresión “estar a la derecha de T”. Por eso, el siguiente algoritmo es muy importante el controlarlo (entró en los últimos exámenes de 2007-2008).

Dicho esto, tenemos el siguiente algoritmo:

funcion busquedabin (푇[1. . 푛], x) si 푛 = 0 ó 푥 > 푇[푛] entonces devolver 푛 + 1 si no { Elemento dentro del vector } devolver binrec (푇[1. . 푛], x) Observamos e insistimos por su importancia que la llamada inicial (busquedabin) verifica que el elemento esté dentro del vector.

Page 130: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 10 de 30

Por tanto, la función propiamente de búsqueda binaria será:

funcion binrec (푇[푖. . 푗], x) { Búsqueda binaria de x en la submatriz 푇[푖. . 푗] con la seguridad de que 푇[푖 − 1] < 푥 ≤ 푇[푗] } si 푖 = 푗 entonces devolver i 푘 ← (푖 + 푗) ÷ 2 si 푥 ≤ 푇[푘] entonces devolver binrec (푇[푖. . 푘], x) { Mitad izquierda} si no devolver binrec (푇[푘 + 1. . 푗], x) { Mitad derecha }

Como añadido del autor, tendremos otra manera para averiguar si el elemento está en el vector T y es sustituyendo en el primer “si” del algoritmo anterior.

si 푖 = 푗 entonces si 푇[푖] = 푥 entonces { Elemento encontrado } dev i si no { Elemento fuera del vector T } dev -1 Ambos algoritmos son iguales, es decir, el del busquedabin y esta sustitución del bucle “si”. Bajo mi punto de vista lo que mejor encuentro es hacerlo tal y como viene en el libro, es decir, con ambos procedimientos distintos, así es seguro que esté correcto, aunque ver varias maneras nunca viene de más.

Sea 푡(푚) el tiempo requerido por una llamada a binrec (푇[푖. . 푗], x), en donde 푚 = 푗 − 푖 + 1 (siendo m el tamaño del problema) es el número de elementos que restan a efectos de búsqueda. El tiempo requerido por una llamada a busquedabin (푇[푖. . 푗], x) es claramente 풕(풏) salvo por una pequeña constante aditiva. Analizaremos el coste de este algoritmo, teniendo en cuenta que es una reducción por división. Por tanto, tendremos la siguiente ecuación de recurrencia:

푡(푛) = 1 ∗ 푡(푛/2) + 푂(1) Las distintas variables son:

a: Número de llamadas recursivas = 1, siendo este valor por haber una llamada a un subproblema por cada bucle “si”.

b: Reducción del problema en cada llamada = 2 풄 ∗ 풏풌: Coste de las operaciones extras a las llamadas recursivas. Tendremos que el valor de 푘 = 0, al ser constante 푂(1) .

De nuevo, la resolución de la recurrencia por división es:

휃(푛 ) si 푎 < 푏 푇(푛) = 휃 푛 ∗ 푙표푔(푛) si 푎 = 푏

휃 푛 si 푎 > 푏

Page 131: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 11 de 30

Resolvemos la ecuación 푎 = 푏 , en este caso, tenemos que 1 = 2 , por lo que el caso es el segundo. Sustituyendo, el coste del algoritmo es 휃 푛 ∗ 푙표푔(푛) =휽 풍풐품(풏) .

NOTA DEL AUTOR: Es muy IMPORTANTE tener MUY claro este coste, porque suele ser un problema bastante fácil y el coste muchas veces se confunde como puede ser, por ejemplo, con la ordenación por montículo,.., que es 휃 푛 ∗ 푙표푔(푛) .

Esta sería la versión recursiva del algoritmo de búsqueda binaria:

funcion binrec (푇[푖. . 푗], x) { Búsqueda binaria iterativa en x en la matriz T} si 푥 > 푇[푛] entonces devolver 푛 + 1 푖 ← 1; 푗 ← 푛 mientras 푖 < 푗 hacer { 푇[푖 − 1] < 푥 ≤ 푇[푗] } 푘 ← (푖 + 푗) ÷ 2 si 푥 ≤ 푇[푘] entonces 푗 ← 푘 si no 푖 ← 푘 + 1 devolver i Este algoritmo gráficamente haría algo así:

i j

En este caso, no es tan directa la búsqueda del elemento en ambas mitades. Según parece y es algo que he deducido con el código el puntero i se desplazaría a la derecha (sumando posiciones), mientras que el j al revés, de tal manera que hasta que i sea mayor que j seguirá haciendo estos movimientos de puntero (acabaría el algoritmo cuando i supere a j). Toda la explicación es una estimación del autor. Por último, decir que el coste del algoritmo es exactamente igual al que vimos con la versión recursiva, es decir, 휽 풍풐품(풏) .

De nuevo, es igualmente básico, fundamental el quedarnos con este coste. Se insistirá numerosas veces.

Page 132: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 12 de 30

7.4. Ordenación

Sea 푇[1. . 푛] una matriz de n elementos. Nuestro problema es ordenar estos elementos por orden ascendente. Previamente, hemos visto que se puede resolver mediante ordenación por selección y ordenación por inserción o mediante ordenación por montículo. Este último algoritmo de ordenación tiene coste en el caso peor y promedio 휽 풏 ∗ 풍풐품(풏) , mientras que los dos anteriores tienen coste cuadrático 휽(풏ퟐ). Hay varios algoritmos de ordenación que siguen el esquema de divide y vencerás. Veremos con más detenimiento dos de ellos:

- Ordenación por fusión (mergesort). - Ordenación rápida (quicksort).

7.4.1. Ordenación por fusión (mergesort)

El problema consiste en: - Descomponer la matriz T en dos partes cuyos tamaños sean tan

parecidos como sea posible. - Ordenar estas partes mediante llamadas recursivas. - Fusionar las soluciones en cada parte, teniendo buen cuidado de

mantener el orden.

Un ejemplo de este algoritmo podría ser el siguiente:

Tomamos 2 partes de igual tamaño.

Ordenamos las partes

Fusionamos esas dos mitades

Observamos, aunque sólo es un ejemplo a grosso modo, que el vector tras fusionar ya está ordenado.

Page 133: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 13 de 30

El problema es como fusionar esas mitades y hacerlo eficiente. Emplearemos como centinela (una especie de posición auxiliar, para evitar realizar cálculos extras) la última posición en las matrices U y V.

procedimiento fusionar (푈[1. .푚 + 1], 푉[1. .푛 + 1], 푇[1. .푚 + 푛]) { Fusiona las matrices ordenadas 푈[1. .푚] y 푉[1. . 푛] almacenándolas en 푇[1. .푚 + 푛], 푈[푚 + 1] y 푉[푛 + 1] se utilizan como centinelas } 푖, 푗 ← 1; 푈[푚 + 1], 푉[푛 + 1] ← ∞ para 푘 ← 1 hasta 푚 + 푛 hacer si 푈[푖] < 푉[푗] entonces 푇[푘] ← 푈[푖]; 푖 ← 푖 + 1 si no 푇[푘] ← 푉[푗]; 푗 ← 푗 + 1

El algoritmo de ordenación por fusión es como sigue, en donde utilizamos la ordenación por inserción (insertar) como subalgoritmo básico, que también añadiremos a continuación (tomándolo del tema 2).

procedimiento ordenarporfusion (푇[1. .푛]) si n es suficientemente pequeño entonces insertar (T) si no matriz 푈[1. .1 + ⌊푛/2⌋], 푉[1. .1 + ⌊푛/2⌋] 푈[1. . ⌊푛/2⌋] ← 푇[1. . ⌊푛/2⌋] 푉[1. . ⌊푛/2⌋] ← 푇[1 + ⌊푛/2⌋. . 푛] ordenarporfusion (푈[1. . ⌊푛/2⌋]) ordenarporfusion (푉[1. . ⌊푛/2⌋]) fusionar (U, V, T)

Usaremos la función de fusionar anterior. Vamos a ver la función de insertar tal y como hemos comentado previamente:

procedimiento insertar 푇([1. . 푛]) para 푖 ← 2 hasta n hacer 푥 ← 푇[푖]; 푗 ← 푖 − 1; mientras 푗 > 0 푦 푥 < 푇[푗] hacer 푇[푗 + 1] ← 푇[푗]; 푗 ← 푗 − 1; 푇[푗 + 1] ← 푥

Page 134: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 14 de 30

Gráficamente, empleando centinelas, como hemos explicado antes, tendremos el funcionamiento de la ordenación por fusión como sigue:

1 m m+1 1 n n+1

U V i j T k

Tenemos dos punteros i y j. Comparamos los elementos a los que apuntan estos dos punteros y vemos cuál es el menor, para luego copiarlo a T. Al copiarlo, incrementaríamos i y k, para desplazar una posición en ambos vectores (U y T), en este caso. Dependerá de la comparación, ver cuál es el puntero menor (si i o j). Para verificar que hemos llegado al final, al ir incrementándose ambos punteros (i o j), tendremos un momento en que uno de los dos o ambos lleguen al valor del centinela. Se nos daría un caso en que el puntero i llegara a dicho centinela y que el j no lo hiciera, eso querrá decir que los elementos menores de j ya están en T, por lo que los elementos hasta llegar a n (sin contar 푛 + 1) de V se copiarían directamente a T (creo recordar que era copia de cabos en estructura de datos). Sería algo así:

j Al vector T

Veremos un ejemplo práctico de una matriz a ordenar:

Matriz que hay que ordenar 3 1 4 1 5 9 2 6 5 3 5 8 9

La matriz se parte en dos mitades

Una llamada recursiva a ordenar por fusión para cada mitad

Una llamada a fusionar 1 1 2 3 3 4 5 5 5 6 8 9 9

∞ ∞

3 1 4 1 5 9 2 6 5 3 5 8 9

1 1 3 4 5 9 2 3 5 3 6 8 9

Page 135: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 15 de 30

El procedimiento de la ordenación por fusión es:

- Cuando el número de elementos que hay que ordenar es pequeño, se utiliza un algoritmo relativamente sencillo.

- Por otra parte, cuando esté justificado por el número de elementos, ordenarporfusion separa el ejemplar en dos subejemplares de tamaño mitad, resuelve las dos recursivamente y entonces combina las dos medias matrices ya ordenadas para obtener la solución del ejemplar original.

Analizaremos el coste de la ordenación por fusión separándolo por partes distintas, es decir, siguiendo el funcionamiento anteriormente dicho:

- La separación de T en U y V requiere un tiempo lineal. - fusionar (U, V, T) también requiere un tiempo lineal. - Tendremos la ecuación de recurrencia siguiente 푡(푛) = 푡(⌊푛/2⌋) +

푡(⌈푛/2⌉) + 푔(푛), donde 푔(푛) ∈ 휃(푛). Por tanto, esta recurrencia pasa a ser 푡(푛) = 2 ∗ 푡(푛/2) + 푔(푛) cuando n es par.

Resolvemos la recurrencia por reducción por división con los siguientes valores:

a: Número de llamadas recursivas = 2 b: Reducción del problema en cada llamada = 2

풄 ∗ 풏풌: Coste de las operaciones extras a las llamadas recursivas. Tendremos que el valor de 푘 = 1, al ser el tiempo extra lineal.

De nuevo, el valor de 푎 = 푏 ⇒ 2 = 2 , siendo el caso el segundo y

resolviéndose así: 푡(푛) ∈ 휃 푛 ∗ 푙표푔(푛) = 휽 풏 ∗ 풍풐품(풏) .

Por tanto, la eficiencia de ordenar por fusión (mergesort) es similar a la de ordenación por montículo (heapsort). La ordenación por fusión puede ser ligeramente más rápida en la práctica, pero requiere una cantidad significativamente mayor de espacio para las matrices intermedias U y V (recordemos que la ordenación por montículo puede hacerse in situ).

Cuando se crean subejemplares de distinto tamaño (los ejemplares están mal distribuidos) nos queda la siguiente variante (muy mala, por cierto) del algoritmo de ordenación por fusión, en la que tendremos una parte en la que se tienen todos los elementos menos uno y en la otra parte tendremos un único elemento. Por ello, será en este caso una reducción por sustracción.

procedimiento ordenarporfusionmala (푇[1. . 푛]) si n es suficientemente pequeño entonces insertar (T) si no matriz 푈[1. .1 + ⌊푛/2⌋], 푉[1. .1 + ⌊푛/2⌋] 푈[1. . 푛 − 1] ← 푇[1. . 푛 − 1] 푉[1] ← 푇[푛] ordenarporfusion (푈[1. .푛 − 1]) ordenarporfusion (푉[1. .1]) fusionar (U, V, T)

Por lo anteriormente escrito, el coste es de 휽(풏ퟐ), considerándose evidentemente coste ineficiente con respecto al otro algoritmo.

Page 136: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 16 de 30

7.4.2. Ordenación rápida (quicksort)

El funcionamiento del algoritmo es el siguiente: - El algoritmo selecciona como pivote uno de los elementos de la

matriz que haya que ordenar. - La matriz se parte a ambos lados del pivote: se desplazan los

elementos de tal manera que los que sean mayores que el pivote (p) queden a la derecha, mientras que los demás queden a su izquierda. Quedaría algo así, gráficamente: p

p

< p > p

- El pivote quedará ahora en su posición definitiva. Tendremos un pequeño problema al colocar los elementos iguales al pivote (p), aunque en este caso nos dará igual donde los coloquemos, si a la derecha o a la izquierda.

- Si ahora las partes de la matriz que quedan a ambos lados del pivote se ordenan independientemente mediante llamadas recursivas al algoritmo, el resultado final es una matriz completamente ordenada.

Para equilibrar los tamaños de los dos subcasos que hay que ordenar, lo idóneo es utilizar el elemento mediana como pivote. Expondremos el concepto de mediana brevemente, para así comprenderlo mejor, aunque más adelante lo veremos con más detenimiento.

Se define a la mediana de T como su ⌈푛/2⌉-ésimo elemento. Por tanto, la mediana es aquel elemento de T, tal que en T hay tantos elementos más pequeños que él como elementos mayores que él.

Por ejemplo, en este vector:

la mediana es el número 4, porque ordenado es:

Aunque veremos esta definición en este apartado, lo veremos con más detenimiento en el 7.5, empleando para ello una definición más formal. Aun así, el ejemplo que pondremos será similar.

3 1 4 1 5 9 2 6 5

1 1 2 3 4 5 5 6 9

Page 137: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 17 de 30

Desafortunadamente, encontrar la mediana requiere un tiempo excesivo. Por esta razón, nos limitaremos a utilizar como pivote un elemento arbitrario de la matriz y esperemos tener suerte.

Es necesario que la constante multiplicativa sea pequeña, para que quicksort sea competitivo con otras técnicas de ordenación como la ordenación por montículo. Podremos tener estos pasos:

- Supongamos que es preciso descomponer la submatriz 푇[푖. . 푗] empleando como pivote 푝 = 푇[푖]. Una buena forma de hacer la descomposición consiste en explorar la submatriz una sola vez, pero empezando en los dos extremos. Los punteros k y l se inicializan i y 푗 + 1, respectivamente.

- A continuación, se incrementa el puntero k hasta que 푇[푘] > 푝 y se decrementa el puntero l hasta que 푇[푙] ≤ 푝. Ahora se intercambian 푇[푘] y 푇[푙]. Este proceso continúa mientras sea 푘 < 푙.

- Finalmente, se intercambian 푇[푖] y 푇[푙] para poner el pivote en la posición correcta.

Los procedimientos serán estos, empezando por el del pivote:

procedmiento pivote (푇[푖. . 푗], var l) { Permuta los elementos de la matriz 푇[푖. . 푗] y proporciona un valor l, tal que, al final, 1 ≤ 푙 ≤ 푗; 푇[푘] ≤ 푝 para todo 푖 ≤ 푘 < 푙, 푇[푙] = 푝, y 푇[푘] > 푝 para todo 1 < 푘 ≤ 푗, en donde p es el valor inicial de 푇[푖] } 푝 ← 푇[푖]; 푘 ← 푖; 푙 ← 푗 + 1 repetir 푘 ← 푘 + 1 hasta que 푇[푘] > 푝 o 푘 ≥ 푗 repetir 푙 ← 푙 − 1 hasta que 푇[푙] ≤ 푝 mientras 푘 < 푙 hacer intercambiar 푇[푘] y 푇[푙] repetir 푘 ← 푘 + 1 hasta que 푇[푘] > 푝

repetir 푙 ← 푙 − 1 hasta que 푇[푙] ≤ 푝 intercambiar 푇[푖] y 푇[푙]

El algoritmo siguiente es el propio de la ordenación rápida (quicksort):

procedmiento quicksort (푇[푖. . 푗]) { Ordena la submatriz 푇[푖. . 푗] por orden no decreciente } si 푗 − 푖 es suficientemente pequeño entonces insertar (푇[푖. . 푗]) si no pivote (푇[푖. . 푗], l) quicksort (푇[푖. . 푙 − 1]) quicksort (푇[푙 + 1. . 푗])

Nos fijamos que usa la función insertar, visto ya en el apartado anterior.

.

Page 138: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 18 de 30

Un ejemplo de este mismo algoritmo será el siguiente y así fijar los conceptos vistos previamente. Tendremos que ordenar este vector, idéntico al de la ordenación por fusión:

Matriz que hay que ordenar

La matriz se particiona tomando como pivote su primer elemento, 푝 = 3

Se busca el primer elemento mayor que el pivote (subrayado, puntero k) y

el último elemento no mayor que el pivote (superrayado, puntero l)

Se intercambian esos elementos

Se vuelve a explorar en ambas direcciones

Se intercambian

Se explora

Los punteros se han cruzado (el elemento superrayado está a la izquierda del subrayado, 푘 > 푙): se intercambia el pivote con el elemento superrayado

La partición ya está ordenada Se ordenan recursivamente las submatrices a cada lado del pivote

3 1 4 1 5 9 2 6 5 3 5 8 9

3 1 4 1 5 9 2 6 5 3 5 8 9

3 1 4 1 5 9 2 6 5 3 5 8 9

3 1 3 1 5 9 2 6 5 4 5 8 9

3 1 3 1 5 9 2 6 5 4 5 8 9

3 1 3 1 2 9 5 6 5 4 5 8 9

3 1 3 1 2 9 5 6 5 4 5 8 9

2 1 3 1 3 9 5 6 5 4 5 8 9

1 1 2 3 3 4 5 5 5 6 8 9 9

Page 139: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 19 de 30

Vemos estos casos de colocación del pivote:

- Cuando el pivote p queda en un extremo (al inicio o al final, da igual): Tendremos una versión no equilibrada de ordenación rápida, en la que el tamaño del problema se reduce en una mitad. La situación tras colocar los elementos menores y mayores es:

푛 − 1

p

La ecuación de recurrencia, por tanto, sería:

푡(푛) = 푡(푛 − 1) + 푂(푛) Recolocación del pivote

Las distintas variables al igual que hemos visto previamente es: a: Número de llamadas recursivas = 1

b: Reducción del problema en cada llamada = 1 풄 ∗ 풏풌: Coste de las operaciones extras a las llamadas recursivas. Tendremos que el valor de 푘 = 1, al ser el tiempo extra lineal.

Recordemos que la resolución para la reducción de la recurrencia por sustracción es la siguiente:

휃(푛 ) si 푎 < 1 푇(푛) = 휃(푛 ) si 푎 = 1

휃 푎 si 푎 > 1

Por tanto, vemos que 푎 = 1, por lo que estaremos en el segundo caso. Pasamos a resolverlo, siendo el tiempo 푡(푛) ∈ 휃(푛 ) = 휽(풏ퟐ).

El algoritmo se comporta muy mal en este caso, siendo éste el caso peor. Veremos un ejemplo de este caso peor, si T ya está ordenado antes de la llamada a quicksort obtenemos 푙 = 푖 en todas las ocasiones, lo cual implica una llamada recursiva a un caso de tamaño 1 y otra a un caso cuyo tamaño se reduce en una unidad. Este caso podremos compararlo con el peor de la ordenación por fusión (donde estaban descompensadas las particiones), en la que recordemos tenía coste cuadrático.

Page 140: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 20 de 30

- Por otra parte, si los elementos de la matriz que hay que ordenar se encuentran inicialmente en orden aleatorio, tendremos que los subejemplares para ordenar estarán suficientemente bien equilibrados.

En el caso peor, tendremos: 1 n

~ n/2 ~ n/2

El pivote está ya ordenado.

Tendremos esta recurrencia por división:

푡(푛) = 2 ∗ 푡(푛/2) + 푂(푛) Recolocación

del pivote

De nuevo, los valores de las variables son: a: Número de llamadas recursivas = 2

b: Reducción del problema en cada llamada = 2 풄 ∗ 풏풌: Coste de las operaciones extras a las llamadas recursivas. Tendremos que el valor de 푘 = 1, al ser el tiempo extra lineal.

Resolvemos la ecuación 푎 = 푏 , siendo éste 2 = 2 , que es el segundo caso:

(푛) ∈ 휃 푛 ∗ 푙표푔(푛) = 휽 풏 ∗ 풍풐품(풏) .

Este caso correspondería con el mejor caso de esta ordenación.

- Para calcular el tiempo promedio haremos una suposición acerca de la distribución de probabilidad de los casos de los n elementos. La suposición más natural es que los elementos de T sean diferentes y que todas las 푛! permutaciones iniciales posibles de los elementos son igualmente equiprobables. El tiempo medio requerido es:

푡(푛) = ∑ 푔(푛) + 푡(푙 − 1) + 푡(푛 − 푙)

Realizando diversos cálculos llegamos a la conclusión que el coste requiere un tiempo en 푶 풏 ∗ 풍풐품(풏) para ordenar n elementos en el caso medio.

Comparación en cuanto a costes de las distintas ordenaciones: podremos decir que la constante oculta es más pequeña que las asociadas en la ordenación por montículo o en ordenar por fusión cuando funciona bien (el caso mejor). Aun cuando seleccionemos la mediana de 푇[푖. . 푗] como pivote, que se puede hacer en tiempo lineal, la ordenación rápida (quicksort) sigue requiriendo un tiempo cuadrático en el caso peor, lo cual sucede si son iguales todos los elementos que hay que ordenar (ojito con esta parte, puede entrar perfectamente en exámenes).

p

Page 141: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 21 de 30

Para intentar mejorar el tiempo en el caso peor tendremos que descomponer T en 3 secciones, empleando p como pivote: después de la partición, los elementos de 푇[푖. .푘] son más pequeños que p, los de 푇[푘 + 1. . 푙 − 1] son iguales a p y la de 푇[푙. . 푗] son más grandes que p. Tendremos lo siguiente: 1 k k+1 l-1 l j

< p = p > p Tendremos este nuevo procedimiento:

procedimiento pivotebis (T[i. . j], p; var k, l).

Después de hacer la partición con una llamada a pivotebis (T[i. . j], T[i], k, l) hay que llamar a quicksort recursivamente con 푇[푖. . 푘] y 푇[푙. . 푗].

Quicksort requiere ahora un tiempo en el caso peor 푶 풏 ∗ 풍풐품(풏) .

7.5. Búsqueda de la mediana Recordemos otra vez la definición de la mediana dada anteriormente, aunque de modo breve (en la ordenación rápida o quicksort):

Sea 푇[1. . 푛] una matriz de enteros y sea s un entero entre 1 y n. Se define el s-ésimo elemento de T como aquél elemento que se encontraría en la s-ésima posición si ordenara T en orden no decreciente. Dados T y s, el problema de encontrar el s-ésimo elemento de T se conoce como el problema de selección. En particular, se define la mediana de T como su ⌈푛/2⌉-ésimo elemento. Cuando n es impar y los elementos de T son diferentes, la mediana es simplemente aquel elemento de T, tal que en T hay tantos elementos más pequeños que él como elementos mayores que él.

Un ejemplo podrá ser este, en el que nos piden que calculemos la mediana de:

es 4, puesto que 3, 1, 1 y 2 son más pequeños que 4, mientras que 5, 9, 6 y 5 son mayores. Llegaremos a esta conclusión tras ordenar el vector, cosa que habremos hecho previamente.

Un algoritmo sencillo para determinar la mediana de 푇[1. . 푛] consiste en ordenar la matriz y extraer entonces el ⌈푛/2⌉-ésimo elemento. Si utilizamos ordenación por montículo u ordenación por fusión, esto requiere un tiempo que está en 푂 푛 ∗ 푙표푔(푛) (de nuevo hay que saberse de memoria este coste, es básico).

Es evidente que todo algoritmo para el problema de selección se puede utilizar para hallar la mediana: basta con seleccionar el ⌈푛/2⌉-ésimo elemento más pequeño. NOTA DEL AUTOR: Este último párrafo supongo que querrá decir que con cualquier algoritmo de ordenación ordenaremos el vector y basta con seleccionar dicho elemento más pequeño. Está copiado literalmente del libro.

3 1 4 1 5 9 2 6 5

Page 142: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 22 de 30

Para calcular el s-ésimo elemento más pequeño tendremos este algoritmo, resolviéndolo de forma parecida a la última mejora de quicksort, para ello usamos el procedimiento pivotebis. Recordemos que la llamada a pivotebis (푇[푖. . 푗], p; var k, l) particiona a 푇[푖. . 푗] en 3 secciones. T se organiza de tal manera que los elementos de 푇[푖. . 푘] sean más pequeños que p, los de 푇[푘 +1. . 푙 − 1] sean iguales a p y los de 푇[푙. . 푗] sean mayores que p. Se nos darán estos casos posibles:

- Tras una llamada a pivotebis (T, P, k, l) hemos terminado si 푘 < 푠 < 푙, puesto que entonces el s-ésimo elemento más pequeño de T es igual a p.

- Si 푠 ≤ 푘, entonces el s-ésimo elemento más pequeño de T es ahora el s-ésimo elemento más pequeño de 푇[1. . 푘].

- Por último, si 푠 ≥ 푙, entonces el s-ésimo elemento más pequeño de T es ahora el (푠 − 푙 + 1)-ésimo elemento más pequeño de 푇[1. . 푛].

1 k k+1 l-1 l j

< p = p > p

Tendremos este algoritmo para el cálculo del s-ésimo elemento más pequeño:

funcion selección (T[1. . n], s) { Busca el s-ésimo elemento más pequeño de T, 1 ≤ 푠 ≤ 푛 } 푖 ← 1; 푗 ← 푛 repetir { La respuesta se encuentra en T[i. . j] } 푝 ← mediana (T[i. . j]) pivotebis (T[i. . j], p, k, l) si 푠 ≤ 푘 entonces 푗 ← 푘 si no si 푠 ≥ 푙 entonces 푖 ← 푙 si no devolver p

Para hacer más eficiente el algoritmo tendremos que seleccionar el pivote como 푝 ← 푇[푖]. Esto da lugar a que el algoritmo invierta un tiempo cuadrático en el caso peor (como pasaba con quicksort). Sin embargo, en el caso promedio el algoritmo modificado funciona en un tiempo lineal. Para mejorar el caso peor de orden cuadrático tendremos que hacer que el número de pasadas por el bucle siga siendo logarítmico, siempre y cuando seleccionemos un pivote cercano a la mediana, lo que se denomina pseudomediana. El tiempo en el caso peor para hallar el s-ésimo elemento más pequeño usando el método de la pseudomediana es lineal.

La última parte, la de la pseudomediana, debido a su complejidad para resumirla, prácticamente no la hemos tocado, por tanto, se dejaría como lectura obligada y comprensiva.

Page 143: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 23 de 30

7.6. Multiplicación de matrices

El algoritmo clásico de multiplicación de matrices sigue su definición:

퐶 = ∑ 퐴 ∗ 퐵

El tiempo está en 휽(풏ퟐ) para calcularse 푛 entradas. Una posible mejora es multiplicar una matriz 2 x 2 en vez de en 8 multiplicaciones en 7 (parecido a la multiplicación de enteros muy grandes). La reducción de una multiplicación con respecto a más sumas es insignificante cuando el tamaño del problema es pequeño, pero es importante el ahorro cuando las matrices son grandes.

La ecuación de recurrencia es:

푡(푛) = 7 ∗ 푡(푛/2) + 푔(푛) Deduciendo de esta ecuación, tendremos las siguientes variables para la reducción por división:

a: Número de llamadas recursivas = 7 b: Reducción del problema en cada llamada = 2

풄 ∗ 풏풌: Coste de las operaciones extras a las llamadas recursivas. El significado de 푔(푛) es el coste de sumar y restar matrices, que seria 휃(푛 ). Por tanto, el valor de k es 푘 = 2.

Resolviendo la recurrencia tendremos lo siguiente:

휃(푛 ) si 푎 < 푏 푇(푛) = 휃 푛 ∗ 푙표푔(푛) si 푎 = 푏

휃 푛 si 푎 > 푏

Como en ocasiones anteriores sustituyendo en la ecuación 푎 = 푏 , tendremos la inecuación 7 > 2 , por lo que estaremos en el caso tercero, con coste 푡(푛) ∈휃 푛 = 휽 풏퐥퐨퐠ퟐ ퟕ . Nos fijamos que mejoramos el coste visto en el algoritmo que emplea 8 multiplicaciones.

Page 144: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 24 de 30

7.7. Exponenciación

Como recordatorio veremos esta comparación de costes entre los métodos hasta el momento vistos, en la que las multiplicaciones son operaciones elementales, es decir, que no tiene coste extra hacerlas:

- Método clásico: Tenemos que 푇(푚,푛) ∈ 휽(풎ퟐ풏ퟐ), demostrando que ambas cotas (inferior y superior) tienen el mismo coste.

- Divide y vencerás: Empleando una multiplicación de dos números de n cifras tiene coste 푶 풏퐥퐨퐠ퟐ ퟑ . Recordemos, que lo vimos previamente y que era el coste hacer 3 multiplicaciones en vez de 4, por lo que sería el algoritmo ya mejorado. Tendremos distintos casos usando el algoritmo de divide y vencerás:

1. Si multiplicamos dos números grandes, como hemos hecho en este apartado, empleando para ello un algoritmo de multiplicación mediante el esquema de divide y vencerás, tendremos que 푇(푚, 푛) ∈ 휽 풏ퟐ풎퐥퐨퐠ퟐ ퟑ .

2. Si multiplicamos dos números de n y m cifras respectivamente, siendo m mucho mayor que n (푛 ≪ 푚), tendríamos lo siguiente:

n n n n m

n

Haremos multiplicaciones de dos números de n cifras, por lo que el coste será:

푂 푛 = 푂 푚푛 = 푂 푚푛 = = 푂 푚푛 ( / )

Sean a y n dos enteros. Deseamos calcular la exponenciación 푥 = 푎 . Por sencillez, supondremos en toda esta sección que 푛 > 0. Si n es pequeño, el algoritmo evidente resulta adecuado:

funcion exposec (a, n) 푟 ← 푎 para 푖 ← 1 hasta 푛 − 1 hacer 푟 ← 푎 ∗ 푟 devolver r que equivale a hacer:

푥 = 푎 = 푎 ∗ 푎 ∗ … ∗ 푎

= 푎 ∗ 푎

Este algoritmo requiere un tiempo que está en 휽(풏), puesto que la instrucción 푟 ← 푎 ∗ 푟 se ejecuta exactamente 푛 − 1 veces, siempre y cuando las multiplicaciones cuenten como operaciones elementales. El inconveniente para analizar el coste es que el número que se multiplica puede ser grande.

Page 145: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 25 de 30

A continuación y en lo que acaba el apartado, veremos la multiplicación cuando los operandos son muy grandes, para ello es preciso tener en cuenta el tiempo necesario para cada multiplicación. Sea 푀(푞, 푠) el tiempo necesario para multiplicar dos enteros de tamaños q y s. Supongamos que 푞 ≤ 푞 y 푠 ≤ 푠 implican que 푀(푞 , 푠 ) ≤ 푀(푞 , 푠 ). Estimemos cuánto tiempo invierte nuestro algoritmo multiplicando enteros cuando se invoca exposec (a, n):

Como añadido del autor veremos cuánto cambia el tamaño de r en cada pasada (recordemos que teníamos que calcular 푎 ). Aplicaremos esta idea al problema nuestro. Haremos un producto de i y j cifras respectivamente, teniendo dos casos:

푖 + 푗 en el peor caso. 푖 + 푗 − 1 en el mejor caso.

Veremos el exponente, el valor y el tamaño de r: Exponente 1 2 3 ……… i Valor a 푎 푎 ……… 푎

Tamaño m 2푚2푚 − 1 3푚

3푚− 2 ……… 푖푚푖푚 − 푖 + 1

El tamaño indica el número de cifras de la exponenciación. En el nivel i de la exponenciación los casos que se nos dan son los siguientes:

풊풎: Es el caso peor y el coste de las multiplicaciones es 푀(푚, 푖푚). 풊풎 − 풊 + ퟏ: Es el caso mejor y el coste es 푀(푚, 푖푚 − 1 + 1).

Sea m el tamaño del problema a. En primer lugar, observemos que el producto de dos enteros de tamaños i y j tiene un tamaño que es al menos 푖 + 푗 − 1 y como máximo 푖 + 푗. Sean 푟 y 푚 el valor y el tamaño de r al principio de la i-ésima pasada por el bucle. Claramente, 푟 = 푎 y, por tanto, 푚 = 푚. Dado que 푟 = 푎푟 , el tamaño de 푟 es al menos 푚 + 푚 − 1 y como máximo 푚 + 푚 . La demostración por inducción matemática de que 푖푚 − 푖 + 1 ≤ 푚 ≤ 푖푚 para todo se sigue inmediatamente. Por tanto, la multiplicación efectuada en la i-ésima pasada por el bucle afecta a un entero de tamaño m y a un entero cuyo tamaño se encuentre entre 푖푚 − 푖 + 1 e 푖푚, lo cual requiere un tiempo que está entre 푀(푚, 푖푚 − 푖 + 1) y 푀(푚, 푖푚). El tiempo total 푇(푚, 푛) que se invierte en multiplicaciones cuando se calcula 푎 con exposec es:

∑ 푀(푚, 푖푚 − 푖 + 1) ≤ ( )

푇(푚,푛)≤ ∑ 푀(푚, 푖푚) ( )

donde m es el tamaño de a.

NOTA DEL AUTOR: Hay una pequeña errata en la ecuación anterior en el libro, en el que la cota superior es ∑ 푀(푖, 푖푚) cuando anteriormente estaba puesto 푀(푚, 푖푚). Al escribirlo en nuestro resumen la hemos subsanado y escrito correctamente.

Page 146: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 26 de 30

Distinguiremos dos casos de los algoritmos clásicos, en la que las multiplicaciones ya no son operaciones elementales, como dijimos previamente:

- Algoritmo sin mejorar (realiza 4 multiplicaciones): 푇(푚, 푛) es una buena estimación del tiempo total requerido por exposec, puesto que la mayor parte del trabajo se invierte en realizar estas multiplicaciones. Si utilizamos el algoritmo clásico de multiplicación, entonces 푀(푞, 푠) ∈휃(푞푠). Sea c tal que 푀(푞, 푠) ≤ 푐푞푠:

푇(푚,푛) ≤ ∑ 푀(푖, 푖푚) ≤ ∑ 푐 ∗ 푚 ∗ 푖푚 = 푐푚 ∑ 푖 < < 푐푚 푛

Hemos calculado la cota superior. El cálculo para la cota inferior será igual, concluyendo que para el algoritmo sin mejorar es:

푇(푚,푛) ∈ 휽(풏ퟐ풎ퟐ) Esta demostración está sacada del libro. Observamos que se ha hecho con el método clásico sin mejorar.

- Algoritmo mejorado (realiza 3 multiplicaciones): Haremos el mismo

cálculo para calcular la cota superior mediante este método mejorado:

푇(푚,푛) ≤ ∑ 푀(푖, 푖푚) =∑ 푚 = 푚 ∑ 푖 ≤ ≤ 푛 푚

Tendremos mediante esta demostración que:

푇(푚,푛) ∈ 푂 푛 푚

Calcularíamos la cota inferior, pero es igual, por tanto, concluimos que:

푇(푚,푛) ∈ 휽 풏ퟐ풎퐥퐨퐠ퟐ ퟑ

Para mejorar exposec haremos que 푎 = 푎( / ) , cuando n es par. Se puede calcular 푎( / ) aproximadamente cuatro veces más deprisa que 푎 con exposec y basta una única elevación al cuadrado para obtener el resultado deseado a partir de 푎( / ). Tendremos lo siguiente:

푎 = 푎( / ) ∗ 푎( / ) cuando n es par.

푎 = 푎 ∗ 푎 cuando n es impar. En la siguiente llamada recursiva la resolveríamos con el algoritmo de arriba, ya que n sería ya par.

La recurrencia que produce es:

a si 푛 = 1 푎 = 푎( / ) si n es par 푎 ∗ 푎 en caso contrario

Page 147: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 27 de 30

Un ejemplo que tendremos es:

푎 = 푎 ∗ 푎 = 푎 ∗ (푎 ) = 푎 ∗ ((푎 ) ) = ⋯ = 푎 ∗ ((푎 ∗ (푎 ∗ 푎 ) ) ) Sólo utilizaría 3 multiplicaciones y 4 elevaciones al cuadrado en lugar de las 28 multiplicaciones que se necesitan con exposec.

La recursión anterior da lugar a este algoritmo:

funcion expoDV (a, n) si 푛 = 1 entonces devolver a si n es par entonces devolver [푒푥푝표퐷푉(푎,푛/2)] devolver 푎 ∗ 푒푥푝표퐷푉(푎, 푛 − 1)

El número de multiplicaciones sólo es una función del exponente n, por lo que denotamos 푁(푛). Veremos los casos distintos:

- Cuando 풏 = ퟏ, no se efectúa operación, así que 푁(1) = 0. - Cuando n es par, se efectúa una multiplicación (la elevación del

cuadrado de (푎, 푛/2), además de las 푁(푛/2) multiplicaciones implicadas en la llamada recursiva a expoDV (a, 푛/2).

- Cuando n es impar, se efectúa una multiplicación (a por 푎 ) además de las 푁(푛 − 1) multiplicaciones requeridas por la llamada recursiva a expoDV (a, 푛 − 1).

Por tanto, la recurrencia de nuevo en función de 푁(푛) es:

0 si 푛 = 1 푁(푛) = 푁(푛/2) + 1 si n es par 푁(푛 − 1) + 1 en caso contrario

Acotamos inferior y superiormente la función mediante funciones no decrecientes. De nuevo, tendremos estos casos:

- Cuando 푛 > 1 es impar:

푁(푛) = 푁(푛 − 1) + 1 = 푁 ( ) + 2 = 푁(⌊푛/2⌋) + 2.

- Cuando 푛 > 1 es par:

푁(푛) = 푁(⌊푛/2⌋) + 1.

Page 148: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 28 de 30

Por tanto, tendremos la siguiente recurrencia a partir de los casos anteriores:

푁(⌊푛/2⌋) + 1

≤ 푁(푛) ≤ 푁(⌊푛/2⌋) + 2

Tendremos las siguientes variables empleando para ello la ecuación anterior: a: Número de llamadas recursivas = 1, que sería 1 si es impar o par.

b: Reducción del problema en cada llamada = 2 풄 ∗ 풏풌: Coste de las operaciones extras a las llamadas recursivas. Al ser una constante la operación extra, tendremos que 푘 = 0.

Ahora estaremos en el caso segundo siendo 푡(푛) ∈ 휃(푛 ∗ log(푛)) = 휃(log(푛)).

Las funciones anteriores (empleando operaciones elementales) están en orden de 휃(log(푛)), por lo que igualmente lo está 푁(푛). En este mismo caso, exposec usaba un número de multiplicaciones de tiempo 휃(푛) para efectuar la misma exponenciación, por tanto, podemos concluir que expoDV es más eficiente que exposec usando este criterio. Añadiremos más adelante una tabla comparativa incluyendo este coste también, a modo de recordatorio.

Sea 푀(푞, 푠) el tiempo necesario para multiplicar 2 enteros de tamaños q y s y 푇(푚,푛) el tiempo que invierta en multiplicar una llamada a expoDV (a, n), en donde m es el tamaño de a. Tendremos, por tanto, esta recurrencia:

0 si 푛 = 1 푇(푚,푛) ≤ 푇(푚, 푛/2) + 푀(푚푛/2,푚푛/2) si n es par 푇(푚, 푛 − 1) + 푀 푚,푚(푛 − 1) si n es impar

Se ha hecho otra pequeña modificación en el caso en el que n es impar, por lo que se varía un parámetro de la multiplicación M, poniendo del modo correcto bajo mi punto de vista.

Las siguientes multiplicaciones M son:

푀(푚푛/2,푚푛/2) = (푚 ∗ 푛/2) ∗ (푚 ∗ 푛/2).

푀 푚,푚(푛 − 1) = 푚 ∗푚

Calculamos 푇(푚,푛) en el caso peor, teniendo esta ecuación para calcular el tiempo:

푇(푚,푛) ≤ 푇(푚, ⌊푛/2⌋) + 푀(푚⌊푛/2⌋,푚⌊푛/2⌋)

+ 푀 푚,푚(푛 − 1)

Recordemos que la expresión ⌊푛/2⌋ significa tomar la parte inferior de la mitad de n.

Page 149: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 29 de 30

Dicho esto, veremos la resolución de esta ecuación puesta arriba empleando los distintos métodos ya conocidos: 1. Utilizando el método clásico, que recordemos que realizaba 4

multiplicaciones. Tendremos lo siguiente:

푀(푚⌊푛/2⌋,푚⌊푛/2⌋) ~ 푚 푛 푀 푚,푚(푛 − 1) ~ 푚 푛

Sustituyendo en la ecuación anterior nos quedará:

푇(푚,푛) ≤ 푇(푚, ⌊푛/2⌋) + 풎ퟐ풏ퟐ Podremos decir que la parte de la multiplicación de las dos mitades es la que impera en el tiempo total del algoritmo, la cual hemos resaltado en negrita. Pasamos a resolver la recurrencia de reducción por división empleando las siguientes variables:

a: Número de llamadas recursivas = 1 b: Reducción del problema en cada llamada = 2

풄 ∗ 풏풌: Coste de las operaciones extras a las llamadas recursivas. 푘 = 2, recordemos que había que hacer 4 multiplicaciones, siendo 푘 =log 4 = 2. Previamente, hemos visto cual era ese coste.

Como es habitual la resolución de la recurrencia es:

휃(푛 ) si 푎 < 푏 푇(푛) = 휃 푛 ∗ 푙표푔(푛) si 푎 = 푏

휃 푛 si 푎 > 푏

Sustituyendo en 푎 = 푏 , tendremos que 푎 < 푏 , por lo que el coste es 푡(푛) ∈ 휃(푛 ). Por tanto, 푡(푚, 푛) ∈ 휃(푚 푛 ), y además vimos que imperaba este coste en el algoritmo. Posteriormente veremos una tabla comparativa de costes, aunque podemos decir que como conclusión que el coste del método clásico empleando exposec y expoDV es el mismo, siendo éste el deducido previamente.

2. Utilizando el método mejorado, que recordemos que consistía en hacer 3

multiplicaciones. Igualmente, tendremos:

푀(푚⌊푛/2⌋,푚⌊푛/2⌋) ~ 푚 푛

푀 푚,푚(푛 − 1) ~ 푚 푛

La ecuación sería:

푇(푚,푛) ≤ 푇(푚, ⌊푛/2⌋) + 풎퐥퐨퐠ퟐ ퟑ풏퐥퐨퐠ퟐ ퟑ

Recordemos que log 3 > 1, por lo que imperará 푚 푛 .

Page 150: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 7 Curso 2007-08 Página 30 de 30

Resolveremos, de nuevo, la recurrencia de reducción por división empleando las siguientes variables:

a: Número de llamadas recursivas = 1 b: Reducción del problema en cada llamada = 2

풄 ∗ 풏풌: Coste de las operaciones extras a las llamadas recursivas, siendo 푘 = log 3, como hemos visto previamente.

Resolviendo 푎 = 푏 , tendremos de nuevo que 푎 < 푏 , por lo que el coste es 푡(푛) ∈ 휃 푛 . Por tanto, 푡(푚, 푛) ∈ 휃 푚 푛 .

Recordemos que el coste de realizar 3 multiplicaciones empleando exposec era 휃 푛 푚 , mientras que empleando el algoritmo expoDV es 휃 푚 푛 , el cual acabamos de hallar. Veremos una tabla comparativa a modo de último recordatorio.

Compararemos los distintos métodos vistos hasta el momento:

Por último, tendremos una versión iterativa del algoritmo de exponenciación:

funcion expoiter (a, n) 푖 ← 푛; 푟 ← 1; 푥 ← 푎 mientras 푖 > 0 hacer si i es impar entonces 푟 ← 푟푥 푥 ← 푥 푖 ← 푖 ÷ 2 devolver r

Multiplicación Operaciones

elementales Clásica D y V

exposec 휃(푛) 휃(푚 푛 ) 휃 푚 푛 expoDV 휃(log(푛)) 휃(푚 푛 ) 휃 푚 푛

Page 151: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Alumna: Alicia Sánchez Centro: UNED-Las Rozas (Madrid)

Resumen de programación 3

Tema 9. Exploración de grafos.

Page 152: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 2 de 38

Índice:

9.1. Grafos y juegos: introducción .…………………………………… 3 9.2. Recorrido de árboles ……………………………………………... 4

9.2.1. Precondicionamiento ……………………………………… 6 9.3.Recorrido en profundidad: grafos no dirigidos …………………… 7

9.3.1. Puntos de articulación …………………………………….. 8 9.4. Recorrido en profundidad: grafos dirigidos ………………………9 9.5. Recorrido en anchura …………………………………………… 10 9.6. Vuelta atrás ……………………………………………………... 21

9.6.1. El problema de la mochila (3) …………………………… 23 9.6.2. El problema de las 8 reinas ……………………………… 25 9.6.3. El caso general …………………………………………... 29

9.7. Ramificación y poda ……………………………………………. 29 9.7.1. El problema de la asignación ……………………………. 33 9.7.2. El problema de la mochila (4) …………………………... 37 9.7.3. Consideraciones generales ………………………………. 38

Bibliografía: Se han tomado apuntes de los libros:

Fundamentos de algoritmia. G. Brassard y P. Bratley Estructuras de Datos y Algoritmos. R. Hernández

Page 153: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 3 de 38

En este capítulo presentamos algunas técnicas generales que se pueden utilizar cuando no se requiere ningún orden concreto en nuestro recorrido. 9.1. Grafos y juegos: introducción

Veremos brevemente un juego llamado Nim, que consiste en que dos jugadores escogen una serie de casillas hasta que gane el jugador que se quede con la última cerilla. No se permiten empates. Ambos jugadores cumplen las mismas normas. Por tanto, para ganar la partida tendremos que imaginarnos mentalmente las jugadas tanto las nuestras como la del contrincante presentes y futuras. Para formalizar este proceso de pensamiento anticipatorio, representamos el juego mediante un grafo dirigido. Cada nodo del grafo corresponde a una situación del juego y cada arista corresponde a una jugada que nos lleva de una situación a otra. Los nodos del grafo que corresponden a este juego son, por tanto, parejas de la forma ⟨푖, 푗⟩. En general, ⟨푖, 푗⟩ con 1 ≤ 푗 ≤ 푖 indica que en la mesa quedan i cerillas, y que en la jugada siguiente se puede tomar cualquier número de cerillas entre 1 y j. Las aristas que salen de esta situación, esto es, las jugadas que se pueden hacer, van a los j nodos ⟨푖 − 푘, min(2 ∗ 푘, 푖 − 푘)⟩, 1 ≤ 푘 ≤ 푗. Para decidir cuáles son las situaciones de victoria y derrota, partimos de la situación de derrota ⟨0,0⟩ y retrocedemos. Este nodo no tiene sucesor y el jugador que se encuentre en esta situación perderá la partida. Podemos, por tanto, resumir las reglas vistas antes: una situación será de victoria si al menos uno de los sucesores es una situación de derrota. Tendremos el siguiente algoritmo que determina si una situación es de victoria o de derrota: funcion ganarec (i, j)

{ Devuelve verdadero si y sólo si la situación ⟨푖, 푗⟩ es de victoria, suponemos que 0 ≤ 푗 ≤ 푖 } para 푘 ← 1 hasta n hacer si no ganarec 푖 − 푘,푚푖푛(2 ∗ 푘, 푖 − 푘) entonces devolver verdadero devolver falso

Este algoritmo tiene un inconveniente muy grande, que es que calcula el mismo valor una y otra vez. Para solucionarlo tendremos dos enfoques:

1. Aplicando programación dinámica, cuyo algoritmo no veremos, por no estar en nuestro temario (seria el tema 8 que nos falta por estudiar).

2. Utilizando una función con memoria, donde usaremos un vector de booleanos indicando qué nodos hemos visitado durante el cálculo recursivo.

Asociamos a cada nodo una etiqueta para indicar si es victoria, derrota o tablas.

Page 154: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 4 de 38

A lo largo del capítulo usaremos la palabra grafo de dos maneras distintas:

1. Un grafo puede ser una estructura de datos en la memoria de una computadora.

2. El grafo solamente existe de forma implícita. Este grafo nunca llega a existir en la memoria de la máquina. Lo veremos más adelante y será básico para la vuelta atrás.

9.2. Recorrido de árboles Tendremos tres técnicas para recorrer árboles, que recordemos es un grafo acíclico, conexo y no dirigido:

Preorden: Visitamos primero el nodo, segundo el subárbol izquierdo y, por último, el subárbol derecho.

Orden infijo: Visitamos primero el subárbol izquierdo, segundo el nodo y, por último, el subárbol derecho.

Postorden: Visitamos primero el subárbol izquierdo, después el subárbol derecho y, por último, el nodo.

Generalmente, usaremos la primera y tercera técnica (preorden y postorden), por lo que veremos varios ejemplos de recorrido de árboles. Pondremos el árbol siguiente:

A

E

C B

H G D F

J M L K I

Page 155: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 5 de 38

Empezamos a ver el recorrido en preorden, que será el que usemos normalmente. El orden de los nodos visitados lo pondremos en el siguiente grafo: 1

2 7

3 4 6 8 9

5 10 11 12 13

El recorrido en postorden será:

13

5 12

1 3 4 6 11

2 7 8 9 10

Exploramos el árbol de izquierda a derecha en estas técnicas aunque se puede recorrer de derecha a izquierda igualmente.

Veremos este lema correspondiente con estas técnicas, aunque no lo demostraremos formalmente:

퐿푒푚푎 9.2.1 Para cada una de las seis técnicas mencionadas, el tiempo total

푇(푛) que se necesita para explorar el árbol binario que contiene n nodos se

encuentra en 휃(푛).

A

E

C B

H G D F

J M L K I

A

E

C B

H G D F

J M L K I

Page 156: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 6 de 38

9.2.1. Precondicionamiento

Emplearemos el precondicionamiento en dos casos distintos (aunque no están distinguidos en el libro directamente por lo que lo haremos en el resumen):

1er caso: Realización de cálculos anticipados de información auxiliar. Puede merecer la pena invertir una cierta cantidad de tiempo en calcular resultados auxiliares que pueden ser utilizados en el futuro para acelerar la resolución de cada paso. Esto es el precondicionamiento.

Sea a el tiempo que necesita para resolver un caso típico cuando no se dispone de información auxiliar, sea b el tiempo si se dispone de información auxiliar y p el tiempo para calcular esta información auxiliar. Sin precondicionamiento, para resolver n casos típicos necesitaremos un tiempo 푛 ∗ 푎. Con precondicionamiento, empleamos un tiempo 푝 + 푛 ∗ 푏. Siempre que 푏 < 푎, resulta ventajoso el precondicionamiento cuando 푛 >

( ).

El empleo de este caso es para aplicaciones en tiempo real, donde se necesita asegurar una respuesta suficientemente rápida.

2º caso: Usaremos el problema de determinar los antecesores dentro de un árbol con raíz. Sea T un árbol con raíz, no necesariamente binario, decimos que un nodo v de T es un antecesor del nodo w si v está en el camino que va desde w hasta la raíz de T. El problema es dado un par de nodos (푣,푤) de T determinar si v es o no antecesor de w.

Para precondicionar el árbol, recorremos en preorden y luego en postorden, numerando secuencialmente los nodos a medida que los visitamos. Tendremos, por tanto, que:

푝푟푒푛푢푚[푣] es el número asignado a v cuando se recorre el árbol en preorden. 푝표푠푡푛푢푚[푣] es el número asignado a v cuando se recorre el árbol en postorden.

En preorden, recordemos que primero numeramos el nodo y después sus subárboles de izquierda a derecha tendremos:

푝푟푒푛푢푚[푣] ≤ 푝푟푒푚푢푚[푤] ⇔ 푣 푒푠 푢푛 푎푛푡푒푐푒푠표푟 푑푒 푤, 표 푏푖푒푛

푤 푒푠푡á 푎 푙푎 푖푧푞. 푑푒 푤 푒푛 푒푙 á푟푏표푙

En postorden, recordemos que primero numeramos sus subárboles de izquierda a derecha y después numeramos el nodo tendremos:

푝표푠푡푛푢푚[푣] ≥ 푝표푠푡푚푢푚[푤] ⇔ 푣 푒푠 푢푛 푎푛푡푒푐푒푠표푟 푑푒 푤, 표 푏푖푒푛

푤 푒푠푡á 푎 푙푎 푑푒푟.푑푒 푤 푒푛 푒푙 á푟푏표푙

Se sigue que:

푝푟푒푛푢푚[푣] ≤ 푝푟푒푚푢푚[푣] 푦 푝표푠푡푛푢푚[푣] ≥ 푝표푠푡푚푢푚[푤] ⇔

푣 푒푠 푎푛푡푒푐푒푠표푟 푑푒 푤

Page 157: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 7 de 38

Los valores de premun y postnum se han calculado en un tiempo que está en 휃(푛), mientras que la condición requerida se puede comprobar en un tiempo que está en 휃(1). Esta es la importancia realmente de este caso, que luego tarda poco en comprobar y ahorra mucho tiempo.

9.3. Recorrido en profundidad: grafos no dirigidos Para el recorrido en profundidad se siguen estos pasos:

Se selecciona cualquier nodo 푣 ∈ 푁 como punto de partida. Se marca este nodo para mostrar que ya ha sido visitado. Si hay un nodo adyacente a v que no haya sido visitado todavía, se toma

este nodo como punto de partida y se invoca recursivamente al procedimiento en profundidad. Al volver de la llamada recursiva, si hay otro nodo adyacente a v que no haya sido visitado se toma este nodo como punto de partida siguiente, se llama recursivamente al procedimiento y, así sucesivamente.

Cuando están marcados todos los nodos adyacentes a v el recorrido que comenzó en v ha finalizado. Si queda algún nodo de G que no haya sido visitado tomamos cualquiera de ellos como nuevo punto de partida y (como en los grafos no conexos), volvemos a invocar al procedimiento. Se sigue así hasta que estén marcados todos los nodos de G.

El procedimiento de inicialización y arranque será:

procedimiento recorridop (G) para cada 푣 ∈ 푁 hacer 푚푎푟푐푎[푣] ← no visitado para cada 푣 ∈ 푁 hacer si 푚푎푟푐푎[푣] ≠ visitado entonces rp (v)

El algoritmo de recorrido en profundidad siguiendo los pasos anteriores es: procedimiento rp (v) { El nodo v no ha sido visitado anteriormente } 푚푎푟푐푎[푣] ← visitado

para cada nodo w adyacente a v hacer si 푚푎푟푐푎[푣] ≠ visitado entonces rp (v)

Ejemplo: Se nos da este grafo no dirigido:

1

2 3 4

5 6 7 8

Page 158: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 8 de 38

Suponemos que el nodo de partida es el 1. La exploración del grafo en profundidad progresa en la forma siguiente: 1. rp(1) Llamada inicial 2. rp(2) Llamada recursiva 3. rp(3) Llamada recursiva 4. rp(6) Llamada recursiva 5. rp(5) Llamada recursiva; no se puede continuar 6. rp(4) No se ha visitado un vecino del nodo 1 7. rp(7) Llamada recursiva 8. rp(8) Llamada recursiva; no se puede continuar 9. No quedan nodos por visitar

Análisis del coste: Si se representa el grafo de tal manera que la lista de nodos adyacentes tenga un acceso directo, empleando para ello lista de adyacencias (recordemos grafolista del tema 5), entonces este trabajo es proporcional a a en total. El algoritmo requiere un tiempo que está en 휃(푛) para las llamadas al procedimiento y un tiempo en 휃(푎) para inspeccionar las marcas. Por tanto, el tiempo de ejecución está en 휃 푚푎푥(푎, 푛) .

El recorrido en profundidad de un grafo conexo asocia al grafo un árbol de recubrimiento. Sea T este árbol. Las aristas de T corresponden a las aristas utilizadas para recorrer el grafo; están dirigidas del primer nodo visitado al segundo. Las aristas que no se utilizan en el recorrido del grafo no tienen una arista correspondiente en T. El punto inicial de partida de la exploración pasa a ser la raíz del árbol. Resulta fácil mostrar que una arista de G que no tenga una arista correspondiente en T une necesariamente un nodo v con alguno de sus antecesores en T. Si el grafo que se está explorando no es conexo, entonces un recorrido en profundidad le asocia no sólo a un único árbol, sino a un todo un bosque de árboles, uno por cada componente conexa del árbol. Un recorrido en profundidad también ofrece una manera de numerar los nodos del grafo que se está visitando. Los nodos del árbol asociado se numeran en preorden.

9.3.1. Puntos de articulación

Un nodo v de un grafo conexo es un punto de articulación si el subgrafo que se obtiene borrando v y todas las aristas en v ya no es conexo. Nos evitaremos más detalles, puesto que no nos interesará saber más de este apartado.

Page 159: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 9 de 38

9.4. Recorrido en profundidad: grafos dirigidos

El algoritmo es esencialmente el mismo que para los grafos no dirigidos; la diferencia reside en la interpretación de la palabra “adyacente”. En un grafo dirigido, el nodo w es adyacente al v si existe la arista dirigida (푣,푤).

Ejemplo: En el siguiente grafo dirigido:

Suponemos que el nodo de partida es el 1. La exploración del grafo en profundidad progresa en la forma siguiente:

1. rp(1) Llamada inicial 2. rp(2) Llamada recursiva 3. rp(3) Llamada recursiva; no se puede continuar 4. rp(4) No se ha visitado un vecino del nodo 1 5. rp(8) Llamada recursiva 6. rp(7) Llamada recursiva¸ no se puede continuar 7. rp(5) Nuevo punto de comienzo 8. rp(6) Llamada recursiva¸ no se puede continuar 9. No quedan nodos por visitar

Análisis del coste: El tiempo que requiere este algoritmo también está en 휃 푚푎푥(푎, 푛) . En este caso, las aristas utilizadas para visitar todos los nodos de un grafo dirigido 퐺 = ⟨푁,퐴⟩ pueden formar un bosque de varios árboles aunque G sea conexo.

2 3 4

5 6 7 8

1

Page 160: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 10 de 38

9.5. Recorrido en anchura

Cuando un recorrido en profundidad llega a un nodo v, intenta a continuación, visitar algún vecino de v, después algún vecino del vecino y, así sucesivamente. Daremos una formulación no recursiva del algoritmo de recorrido en profundidad:

Sea pila un tipo de datos que admite dos valores apilar y desapilar. Se pretende que este tipo represente una lista de elementos que hay que manejar por el orden “primero en llegar, primero en salir”. La función cima denota el elemento que se encuentra en la parte superior de la pila.

El algoritmo de recorrido en profundidad ya modificado es: procedimiento rp2 (v) 푃 ← pila-vacía

marca[푤] ← visitado apilar w en P

mientras P no esté vacía hacer mientras exista un nodo w adyacente a cima (P)

tal que marca[푤] ≠ visitado hacer marca[푤] ← visitado apilar w en P { w es la nueva cima (P) }

desapilar P

NOTA DEL AUTOR: Tras ver el código de nuevo se ha encontrado lo que a mi parecer es una errata, y es que nunca entraría en el bucle “mientras” a no ser que apiles algún nodo en P, por ello se añade la línea ‘apilar w en P’. Está en la fe de erratas del libro de problemas, por lo que se escribe correctamente (el código está en la página 338).

Ejemplo: Tendremos el siguiente ejemplo de profundidad con pila, donde señalaremos con flechas el sentido de apilar y desapilar, para verlo de modo más grafico:

A B

C

D F

E

Page 161: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 11 de 38

Lo veremos paso a paso empezando a visitar el nodo A:

Inicialmente: La pila está vacía:

1er paso: Apilamos el nodo A y lo marcamos como visitado. En la pila: A

A

2o paso: Apilamos el nodo B y lo marcamos como visitado. En la pila: A, B

B A

NOTA: Escogemos ese nodo sin ningún criterio en especial, ya que estimo que sería más ‘lógico’ escoger el nodo C, pero ahí queda. Más abajo se aclarará como se hace la búsqueda en profundidad.

3er paso: Apilamos el nodo C y lo marcamos como visitado. En la pila: A, B, C

C B A

4o paso: Apilamos el nodo D y lo marcamos como visitado. En la pila: A, B, C, D

D

C B A

Page 162: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 12 de 38

5o paso: Apilamos el nodo E y lo marcamos como visitado. En la pila: A, B, C, D, E E D

C B A

6o paso: Desapilamos el nodo E, porque no tiene más nodos adyacentes. En la pila: A, B, C, D

D

C B A

7o paso: Desapilamos el nodo D, porque no tiene más nodos adyacentes. En la pila: A, B, C

C B A

8o paso: Apilamos el nodo F, por ser un hijo del nodo C y lo marcamos como visitado. En la pila: A, B, C, F F

C B A

9o paso: Desapilamos el nodo F, porque no tiene más nodos adyacentes. En la pila: A, B, C

C B A

Page 163: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 13 de 38

10o paso: Desapilamos el nodo C, porque no tiene más nodos adyacentes. En la pila: A, B

B A

11o paso: Desapilamos el nodo B, porque no tiene más nodos adyacentes. En la pila: A

A

12o paso y último: Desapilamos el nodo A, porque no hay más nodos que explorar. La pila está vacía, llegamos al final.

El orden de exploración será: A, B, C, D, E, F

Es una coincidencia el recorrerlos por orden, pero no tiene porque ser así.

Para verlo más gráficamente, tendremos este árbol que iremos marcando con flechas por donde iríamos recorriendo:

Como se observa el recorrido iría del nodo de arriba hasta el de abajo para luego cuando no se pueda continuar seguir con los demás nodos, tal y como hemos visto en el ejemplo anterior.

Page 164: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 14 de 38

En cuanto al recorrido en anchura seguiremos estos pasos:

Se toma cualquier nodo 푣 ∈ 푁 como punto de partida. Se marca este nodo como visitado. Después se visita a todos los adyacentes antes de seguir con nodos más

profundos.

El procedimiento de inicialización y arranque será:

procedimiento recorrido (G) para cada 푣 ∈ 푁 hacer 푚푎푟푐푎 [푣] ← no visitado para cada 푣 ∈ 푁 hacer si 푚푎푟푐푎 [푣] ≠ visitado entonces {푟푝2 표 푟푎} (v) Para el algoritmo de recorrido en anchura necesitamos un tipo cola que admite las dos operaciones poner o quitar. Este tipo representa una lista de elementos que hay que manejar por el orden “primero en llegar, primero en salir”. La función primero denota el elemento que ocupa la primera posición en la cola. El recorrido en anchura no es naturalmente recursivo, por lo que el algoritmo será:

procedimiento ra (v) 푄 ← cola-vacía poner v en Q mientras Q no esté vacía hacer 푣 ← primero (Q) quitar u de Q para cada nodo w adyacente a u hacer

si marca [푤] ≠ visitado entonces marca [푤] ← visitado poner w en Q

NOTA DEL AUTOR: Al igual que pasaba con el algoritmo rp2, añadiremos una nueva línea ‘poner v en Q’, de tal manera que al encolar el primer nodo ya la cola Q no está vacía y entraría en el bucle “mientras”, exactamente como pasaba antes.

Page 165: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 15 de 38

Ejemplos: Veremos un par de ejemplos que hace este tipo de búsqueda en anchura, aunque con uno de los dos sería suficiente para saber hacerlo. De nuevo pondremos flechas que indican el sentido de encolar y desencolar.

El primero de ellos es el siguiente, sacado del libro completamente y respetando el orden de visitas, en el que empezaremos a visitarlo por el nodo 1:

Inicialmente: La cola está vacía (se añade este paso a la teoría del libro):

1er paso: Encolamos el nodo de partida, 1 (se añade este paso a la teoría del libro). En la cola: 1

1

2o paso: Visitamos (desencolamos) el nodo 1 y encolamos los hijos de él, que son el 2, 3 y 4. En la cola: 2, 3, 4

4 3 2

3er paso: Visitamos (desencolamos) el nodo 2 y encolamos los hijos, añadiéndolos a la cola. En la cola: 3, 4, 5, 6

6 5 4 3

1

2 3 4

5 6 7 8

Page 166: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 16 de 38

4o paso: Visitamos (desencolamos) el nodo 3 y al no tener hijos no encolamos ningún nodo más. En la cola: 4, 5, 6

6 5 4

5o paso: Visitamos (desencolamos) el nodo 4 y encolamos los hijos del mismo, de nuevo. En la cola: 5, 6, 7, 8

8 7 6 5

6o paso: Visitamos (desencolamos) el nodo 5 y al tener el resto ya recorridos no apilamos ninguno más. En la cola: 6, 7, 8

8 7 6

7o paso: Visitamos (desencolamos) el nodo 6, que por los mismos motivos de antes no apilamos ningún nodo más. En la cola: 7, 8

8 7

8o paso: Visitamos (desencolamos) el nodo 7 y no añadimos ningún nodo más. En la cola: 8

8

9o paso y último: Visitamos el último nodo y ya queda la cola vacía.

El orden de exploración será: 1, 2, 3, 4, 5, 6, 7, 8

Page 167: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 17 de 38

El segundo ejemplo es idéntico al que vimos anteriormente en el recorrido en profundidad, en el que veremos todavía más desgranados los pasos, por lo que puede quedar algo distinto al anterior, pero usando la misma técnica. Es un ejemplo complementario para comprender más este tipo de estructuras de datos. Empezamos por el nodo A:

Inicialmente: La cola está vacía:

1er paso: Encolamos el nodo de partida, A. En la cola: A

A

2o paso: Encolamos uno de los hijos de A, que es el B, marcándolo como recorrido. En la cola: A, B

B A

3er paso: Encolamos el otro hijo de A, que es el C. En la cola: A, B, C

C B A

A B

C

D F

E

Page 168: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 18 de 38

4o paso: Desencolamos el nodo A, ya que no hay hijos sin visitar que explorar. En la cola: B, C

C B

NOTA: Vemos que en el ejemplo anterior estos tres primeros pasos lo hemos hecho en sólo uno, en los que primero encolamos los hijos, hasta recorrerlos todos y luego desencolamos el padre.

5o paso: Desencolamos el nodo B, por no tener ningún descendiente no explorado previamente. En la cola: C

C

6o paso: Encolamos uno de los hijos de C, que es el D, marcándolo como recorrido. En la cola: C, D

D C

7o paso: Encolamos el otro hijo de C, que es el F. En la cola: C, D, F

F D C

8o paso: Desencolamos el nodo C, ya que no hay hijos sin visitar que explorar. Nos fijamos que de nuevo, pasa algo similar al paso 2. En la cola: D, F

F D

9o paso: Encolamos el otro hijo de D, que es el E. En la cola: D, F, E

E F D

Page 169: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 19 de 38

10o paso: Desencolamos el nodo D, ya que no hay hijos sin visitar que explorar. En la cola: F, E

E F

11o paso: Desencolamos el nodo F, por no tener ningún descendiente no explorado previamente. En la cola: E

E

12o paso: Desencolamos el nodo E, por no tener ningún descendiente no explorado previamente, por lo que la cola ya está vacía.

El orden de exploración será: A, B, C, D, F, E

Como hemos hecho en la exploración en profundidad veremos cómo lo haremos en anchura de modo más grafico:

………

……………………

Al igual que el recorrido en profundidad, podemos asociar un árbol al recorrido en anchura. Si el grafo G que se está recorriendo es no conexo, el recorrido en anchura genera un bosque de árboles, uno por cada componente de G.

Análisis del coste: Igual que el recorrido en profundidad tendremos que el coste es 휽 풎풂풙(풂,풏) . Se puede aplicar el recorrido en anchura tanto en grafos dirigidos como en no dirigidos.

Page 170: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 20 de 38

La comparación entre ambos recorridos será la siguiente:

El recorrido en anchura se realizará en una de estas situaciones: 1. Cuando haya que efectuar una exploración parcial de un grafo

infinito. En ocasiones puede no terminar si hay niveles con un número infinito de vecinos (no se suele dar en la práctica), como pudiera ser el siguiente grafo:

……

2. Para hallar el camino más corto desde un punto de un grafo a otro, es decir, la solución será el nodo más cercano a la raíz y tiene la certeza de hallar una solución si existe. Un ejemplo pudiera ser este grafo:

Solución

El recorrido en profundidad puede no terminar si las ramas son infinitas. Una rama infinita puede ser la siguiente:

Page 171: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 21 de 38

9.6. Vuelta atrás

Hay problemas que son inabordables mediante grafos abstractos (almacenados en memoria). Si el grafo contiene un número elevado de nodos y es infinito, puede resultar inútil construirlo implícitamente en memoria. En tales situaciones emplearemos un grafo implícito, que será aquél para el cual se dispone de una descripción de sus nodos y aristas, de tal manera que se pueden construir partes relevantes del grafo a medida que progresa el recorrido.

En su forma básica, la vuelta atrás se asemeja a un recorrido en profundidad dentro de un grafo dirigido. Esto se consigue construyendo soluciones parciales a medida que progresa el recorrido; estas soluciones parciales limitan las regiones en las que se puede encontrar una solución completa. Se nos darán estos casos:

El recorrido tendrá éxito si se puede definir por completo una solución. En este caso, el algoritmo puede o detenerse (si sólo necesita una solución al problema) o bien seguir buscando soluciones alternativas (si deseamos examinarlas todas).

Por otra parte, el recorrido no tiene éxito si en alguna etapa de la solución parcial construida hasta el momento no se puede completar, lo cual denominaremos condición de poda (no se construye esa parte del árbol). Cuando vuelve a un nodo que tiene uno o más vecinos sin explorar, prosigue el recorrido de una solución.

Hemos visto antes que podemos explorar buscando soluciones alternativas, en ese caso, la vuelta atrás se puede usar para problemas de optimización, lo que implica que:

Exige encontrar todas las soluciones y quedarnos con la óptima. No es el más adecuado, ya que lo veríamos más adelante en este tema

(usaríamos para ello ramificación y poda).

Este primer esquema de vuelta atrás es el general, en nuestro caso, será el básico que tengamos que saber para la asignatura:

fun vuelta-atrás (ensayo) si valido (ensayo) entonces devolver ensayo si no para cada hijo en compleciones (ensayo) hacer si condiciones-de-poda (hijo) entonces vuelta-atrás (hijo) fsi fpara fsi

ffun

Page 172: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 22 de 38

Necesitaremos especificar lo siguiente:

1. Ensayo: Es el nodo del árbol. 2. Función valido: Determina si un nodo es solución al problema o no. 3. Función compleciones: Genera los hijos de un nodo dado. 4. Función condiciones-de-poda: Verifica si se puede descartar de

antemano una rama del árbol, aplicando los criterios de poda sobre el nodo origen de esa rama. Adoptaremos el convenio de que la función condiciones-de-poda devuelve cierto si ha de explorarse el nodo, y falso si puede abandonarse.

El segundo esquema que veremos y será una variación del visto anteriormente será aquél en el que finaliza al encontrar la primera solución.

fun vuelta-atrás (ensayo) dev (solución) si valido (ensayo) entonces devolver ensayo si no lista ← compleciones (ensayo) solución ← solución_vacia mientras no vacía (lista) y solución = solución_vacia hacer hijo ← primero (lista) lista ← resto (lista) si condiciones-de-poda (hijo) entonces solución ← vuelta-atrás (hijo) fsi fmientras devolver solución fsi

ffun

Nos fijamos que almacena en una estructura de datos lista los nodos que se pueden seguir explorando, es decir, que no cumple las condiciones de poda, como hemos explicado previamente.

Page 173: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 23 de 38

El tercer esquema y último en el que igualmente finaliza al encontrar la primera solución será el siguiente:

fun vuelta-atrás (ensayo) dev (es_solucion, solución) si valido (ensayo) entonces devolver (verdadero, ensayo) si no es_solucion ← false lista ← compleciones (ensayo) solución ← solución_vacia mientras no vacía (lista) y no es_solución hacer hijo ← primero (lista) lista ← resto (lista) si condiciones-de-poda (hijo) entonces (es_solucion, solución) ← vuelta-atrás (hijo) fsi fmientras devolver (es_solucion, solución) fsi

ffun Este último esquema tendrá una leve modificación con respecto al anterior, por lo que añadiremos una variable booleana, que nos indicará si hay solución o no. Esta variable nos servirá para, por ejemplo, verificar mediante una llamada externa a la función si ha encontrado una solución (cuando la encuentre se para el algoritmo).

NOTA DEL AUTOR: El primer esquema se ha sacado directamente del libro de problemas, aunque adaptado para cerrar los bucles (como puede ser “si”, “para”,…) y así quede más didáctico. Los dos siguientes son adaptaciones de otros de problemas. Se ponen por si entrara en examen.

9.6.1. El problema de la mochila (3)

Se nos da un cierto número de objetos y una mochila. En esta ocasión y a diferencia de los algoritmos voraces, en lugar de suponer que estén disponibles n objetos, supondremos que los que tenemos son n tipos de objetos y que está disponible un número adecuado de objetos de cada tipo.

Cada objeto de un tipo i 푖 = 1, 2, … , 푛 tiene un peso positivo 푤 (푤 > 0) y un valor positivo 푣 (푣 > 0). La mochila puede llevar un peso que no exceda de W (al igual que en los voraces, insistimos de nuevo).

Nuestro objetivo es llenar la mochila de tal manera que se maximice el valor de los objetos incluidos, respetando la restricción de capacidad. No encuentra función de selección que garantice que es solución óptima, por eso se descarta el esquema voraz (aunque el esquema de vuelta atrás no es el más adecuado por ser un problema de optimización). No podemos aplicar tampoco búsqueda en profundidad o en anchura porque el árbol puede ser infinito.

Page 174: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 24 de 38

Ejemplo: Se nos dan cuatro tipos de objetos distintos con peso máximo 푊 = 8

푛 = 4, 푊 = 8 w (pesos) 1 2 3 4 v (valores) 3 5 6 10

Desarrollaremos el árbol implícito del problema de la mochila, que será el siguiente:

w; v

Hemos marcado las posibles soluciones con un doble cuadrado. Podemos acordar que cargaremos los objetos en la mochila por orden creciente de peso, como hemos marcado en nuestro dibujo. Reducimos el tamaño del árbol a explorar aunque podemos usar otro orden, que no sea el que hemos hecho antes.

El procedimiento es el siguiente para el ejemplo anterior: - Inicialmente, la solución parcial está vacía. - El algoritmo de vuelta atrás explora el árbol como un recorrido en

profundidad, construyendo nodos y soluciones parciales a medida que avanza. En el ejemplo, el primer nodo que se visita es el (2; 3), el siguiente el (2,2; 6), el tercero el (2,2,2; 9) y el cuarto el (2,2,2,2; 12).

- A medida que se visita cada nodo, se extiende la solución parcial. Después visitar estos cuatro nodos, se bloquea el recorrido en profundidad: el nodo (2,2,2,2; 12) no posee sucesores por no cumplir las restricciones del problema. Dado que esta solución parcial, puede resultar ser la solución óptima, de nuestro caso, la memorizamos.

0; 0

2; 3 5; 10 4; 6 3; 5

2,2; 6 2,3; 8 2,4;9 2,5;13 3,5;15 3,4;11 3,3;10 4,4;12

2,2,2;9 2,2,3;11 2,2,4;12 2,3,3;13

2,2,2,2; 12

Page 175: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 25 de 38

- El algoritmo de recorrido en profundidad, vuelve atrás ahora en busca de otras soluciones. En el ejemplo, el recorrido vuelve primero a (2,2,2; 9), que carece de sucesores no visitados, sin embargo, al retroceder un paso más por el árbol hasta el nodo (2,2; 6) hay 2 sucesores que quedan por visitar.

- Explorando de esta manera encontramos que (2,3,3; 13) es una solución mejor que la que ya tenemos y que (3,5; 15) es aun mejor, siendo ésta la solución óptima, por ser la que tiene mayor valor de todas.

Programar el algoritmo es sencillo e ilustra la íntima relación existente entre recursión y recorrido en profundidad. Supongamos que los valores de n y de W y los valores de las matrices 푤[1. . 푛] y de 푣[1. .푛] correspondientes al caso que hay que resolver están disponibles como variables globales. La ordenación de los tipos de elementos es irrelevante. El algoritmo será:

funcion mochilava (i, r) { Calcula el valor de la mejor carga que se puede construir empleando elementos de los tipos 1 a n cuyo peso total no sobrepase r } 푏 ← 0;

{ Se prueban por turno las clases de objetos admisibles } para 푘 ← 1 hasta n hacer si 푤[푘] ≤ 푟 entonces 푏 ← 푚푎푥 푏, 푣[푘] + 푚표푐ℎ푖푙푎푣푎 (푘, 푟 − 푤[푘]) devolver b

La llamada inicial es mochilava (1, W). Cada llamada recursiva a mochilava se corresponde con extender el recorrido en profundidad hasta el nivel inmediatamente inferior del árbol, mientras que el bucle “para” se encarga de examinar todas las posibilidades en un nivel dado.

9.6.2. El problema de las 8 reinas Este es el segundo problema que veremos de vuelta atrás. Consiste en situar ocho reinas en un tablero de ajedrez de tal manera que ninguna de ellas amenace a ninguna de las demás. Recordemos que una reina amenaza a los cuadrados de la misma fila, columna o diagonal. Veremos las distintas maneras de resolver el problema:

La forma más evidente consiste en generar todas las posibilidades de colocar 8 reinas en un tablero. Esto será un enfoque exhaustivo, es decir, recorriendo todas las posibilidades (por “fuerza bruta”). Nos queda el número de situaciones siguiente:

= 4.426.165.368 situaciones hasta llegar a solución.

Una primera mejora consiste en no poner nunca más de una reina en una fila. Reduce la representación del tablero a un vector de ocho elementos, cada uno de los cuales da la posición de la reina dentro de la fila correspondiente.

Page 176: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 26 de 38

Quedaría: 1 8

1

8 donde: 푣[푖]: Indica la fila en la que está la reina en la i-ésima columna. En esta mejora el número de situaciones que hay que considerar será:

8 = 16.277.216

Una segunda mejora consiste en hacer lo mismo que antes sólo que en las columnas. Ahora representaremos el tablero mediante un vector formado por ocho números diferentes entre 1 y 8, es decir, mediante una permutación de los ocho primeros números enteros. Por lo que el número de situaciones posibles es:

8! = 40.320 Usaremos el siguiente algoritmo: proc perm (i) si 푖 = 푛 entonces usar (T) { T es una nueva permutación } si no para 푗 = 푖 hasta n hacer intercambiar 푇[푖] y 푇[푗] 푝푒푟푚 (푖 + 1) intercambiar 푇[푖] y 푇[푗] Si se utiliza el algoritmo anterior para generar las permutaciones sólo se consideran 2.830 situaciones antes de que encuentre una solución.

Una tercera mejora será aquélla en la que ninguna reina está en la misma diagonal. Evidentemente, el número de situaciones posibles es aún menor.

Todos estos algoritmos comparten un defecto común: nunca comprueban si una situación es una solución mientras no se hayan colocado todas las reinas en el tablero (son exhaustivos).

Page 177: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 27 de 38

La vuelta atrás permite mejorar esto, utilizando otro enfoque distinto. Como primer paso reformulamos el problema de las ocho reinas como un problema de búsqueda en un árbol. Decimos que un vector 푉[1. . 푘] de enteros entre 1 y 8 es k-prometedor para 0 ≤ 푘 ≤ 8, si ninguna de las k reinas colocadas en las posiciones (1,푉[1]), (2,푉[2]), … , (푘,푉[푘]) amenaza a ninguna de las otras.

Matemáticamente, 푉 es k-prometedor si, para todo par de enteros i y j entre 1 y k, con 푖 ≠ 푗, tenemos que 푉[푖]− 푉[푗] ∉ {푖 − 푗, 0, 푗 − 푖}. Para 푘 ≤ 1, todo vector V es k-prometedor. Las soluciones del problema de las ocho reinas se corresponden con aquellos vectores que son 8-prometedores.

Ejemplos: 1 8

1

8

푉[1]− 푉[2] = 1 − 2 = −1. 푉[1,2] No vale, no es k-prometedor

1 8

1

8

푉[1]− 푉[3] = 1 − 3 = −3. 푉[1,3] Vale, ya que es k-prometedor

Sea N el conjunto de vectores k-prometedores, 0 ≤ 푘 ≤ 8. Sea 퐺 = ⟨푁,퐴⟩ el grafo dirigido tal que (푈,푉) ∈ 퐴 si y sólo si existe un entero k, con 0 ≤ 푘 ≤8, tal que:

o 푈 es k-prometedor o 푉 es (k+1)-prometedor o 푈[푖] = 푉[푖] para todo 푖 ∈ [1. .푘]

Este grafo es un árbol. Su raíz es el árbol vacio correspondiente a 푘 = 0. Sus hojas son o bien soluciones (푘 = 8) o posiciones sin salida (푘 < 8) tales como [1,4,2,5,8] (o como hemos visto previamente en los ejemplos anteriores): en tal situación, resulta imposible colocar una reina en la fila siguiente sin amenazar por lo menos a una de las reinas que ya están en el

X X

X X

Page 178: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 28 de 38

tablero. Las soluciones del problema de las ocho reinas se pueden obtener explorando este árbol. Sin embargo, no generamos explícitamente el árbol para explorarlo después. Más bien, se generan y abandonan los nodos en el transcurso de la exploración. El recorrido en profundidad es el método evidente, sobre todo si sólo necesitamos una solución.

Gráficamente, puede ser algo así:

0-prometedor 1-prometedor

2-prometedor

n-prometedor

Esta técnica tiene dos ventajas con respecto a las anteriores:

1. El número de nodos del árbol es menor que 8! = 40.320. Bastaría con explorar 114 nodos para obtener la primera solución.

2. Para decidir si un vector es k-prometedor, sabiendo que es una extensión de un vector (k-1)-prometedor, sólo necesitamos comprobar la última reina que haya que añadir.

En el procedimiento siguiente, 푠표푙[1. .8] es una matriz global. Tendremos:

procedimiento reinas (k, col, diag45, diag135) { 푠표푙[1. .푘] es k-prometedor, 푐표푙 = {푠표푙[푖]|1 ≤ 푖 ≤ 푘}, 푑푖푎푔45 = {푠표푙[푖]− 푖 + 1|1 ≤ 푖 ≤ 푘} y 푑푖푎푔135 = {푠표푙[푖] + 푖 − 1|1 ≤ 푖 ≤ 푘} si 푘 = 8 entonces { un vector 8-prometedor es una solución } escribir sol si no para 푗 ← 1 hasta 8 hacer si 푗 ∉ 푐표푙 y 푗 − 푘 ∉ 푑푖푎푔45 y 푗 + 푘 ∉ 푑푖푎푔135 entonces 푠표푙[푘 + 1] ← 푗 { 푠표푙[1. . 푘 + 1] es (k+1)-prometedor } reinas(푘 + 1, 푐표푙 ∪ {푗}, 푑푖푎푔45 ∪ {푗 − 푘},

푑푖푎푔135 ∪ {푗 + 푘}) La llamada inicial es 푟푒푖푛푎푠 (0,∅,∅,∅).

La ventaja obtenida al utilizar la vuelta atrás en lugar de un enfoque exhaustivo (la forma evidente y sus mejoras) se vuelve más pronunciada a medida que crece n. Por ejemplo, para 푛 = 12 son 479.001.600 las posibles permutaciones que hay que considerar.

Page 179: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 29 de 38

9.6.3. El caso general.

Los algoritmos de vuelta atrás se pueden utilizar aun cuando las soluciones buscadas no tengan todas necesariamente la misma longitud. Siguiendo con el planteamiento anterior de los k-prometedores tendremos este cuarto esquema, que será:

fun vueltaatrás (푣[1. .푘]) { v es un vector k-prometedor } si v es una solución entonces escribir v si no para cada vector (k+1)-prometedor w

tal que 푤[1. . 푘] = 푣[1. .푘] hacer vueltaatrás (푤[1. .푘 + 1])

Tanto el problema de la mochila como el de las n reinas se resolvían empleando una búsqueda en profundidad en el árbol correspondiente. Algunos problemas pueden llegar a ser un grafo infinito, que como vimos antes se resolverían empleando un recorrido en anchura.

NOTA DEL AUTOR: Veremos varios ejercicios resueltos que compararán ambas técnicas, usando vuelta atrás como la conocemos (haciendo más exhaustiva la búsqueda y el grafo implícito mayor) y vectores k-prometedores (justo al contrario, el grafo implícito será menor).

IMPORTANTE: Hemos puesto cuatro posibles esquemas, los cuales son básicos el primero y el cuarto. Los otros dos intermedios son variaciones sobre el primero de ellos. Se recalca esto, al igual que en una nota anterior.

9.7. Ramificación y poda Es otra técnica para explorar un grafo dirigido implícito y la última que veremos en este capítulo, además de ser la más complicada de entender. Esta vez vamos a buscar la solución óptima de algún problema. En cada nodo calculamos una cota del posible valor de aquellas soluciones que pudieran encontrarse más adelante en el grafo. Si la cota muestra que cualquiera de estas soluciones tiene que ser necesariamente peor que la mejor solución hallada hasta el momento, entonces no necesitaremos seguir explorando esta parte del grafo.

Page 180: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 30 de 38

Tendremos dos posibles implementaciones:

El primer esquema posible que tendremos será aquél en el que el cálculo de las cotas se combina con un recorrido en anchura, que situará la solución más cerca de la raíz (nota del autor) y las cotas solamente sirven para podar ciertas ramas de un árbol.

fun ramificación-y-poda (ensayo) p ← cola-vacía cota-superior ← inicializar-cota-superior solución ← solución-vacía encolar (ensayo, p) mientras no vacía (p) hacer nodo ← desencolar (p) si valido (nodo) entonces si coste (nodo) < cota-superior entonces solución ← nodo cota-superior ← coste (nodo) fsi si no { Nodo no es válido (solución) } para cada hijo en compleciones (nodo) hacer si condiciones-de-poda (hijo) y

cota-inferior (hijo) < cota-superior entonces encolar (hijo, p)

fsi fpara fsi fmientras ffun

Nos fijamos que usamos estructura de cola, ya que es un recorrido en anchura, por encontrar la solución más cerca de la raíz (apreciación del autor). Veremos con algo más de detalle las distintas funciones y variables que son nuevas correspondientes a dicho esquema, el resto de funciones (compleciones, condiciones-de-poda) ya las hemos estudiado previamente en el esquema general de vuelta atrás. Por tanto, tenemos estas nuevas funciones y variables:

- Cota-superior: Será, en cada momento, el coste de la mejor solución encontrada hasta el momento. En el esquema anterior es una pequeña modificación respecto al mismo, en la que llaman c a esta variable, pero conceptualmente es similar.

- Cota-superior-inicial: Será aquella cota que en un primer momento estimemos. Podrá ser tanto un valor muy alto, que luego haga podar menos ramas tanto un valor cercano a la solución, en todo caso, dependerá del tipo del problema en cuestión.

- Función cota-inferior (nodo): Será aquél valor de cota en el nodo que se estime para alcanzar la solución.

- Función coste (nodo): Será aquel valor del nodo una vez alcanzada la solución. Podrá mejorar al valor de la cota-superior, ante lo cual se actualiza esta última.

Page 181: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 31 de 38

El segundo esquema será aquél en el que el cálculo de las cotas se utilizan para seleccionar el camino que, entre los abiertos, parezca más prometedor para explorarlo primero y además como el esquema anterior para podar ramas. Será el que más empleemos en los distintos ejercicios que se nos den. Tenemos el algoritmo:

fun ramificación-y-poda (ensayo) m ← montículo-vacío cota-superior ← inicializar-cota-superior solución ← solución-vacía añadir-nodo (ensayo, m) mientras no vacío (m) hacer nodo ← extraer-raíz (m) si valido (nodo) entonces si coste (nodo) < cota-superior entonces solución ← nodo cota-superior ← coste (nodo) fsi si no { Nodo no es válido (solución) } si cota-inferior (nodo) ≥ cota-superior entonces devolver solución si no { cota-inferior (nodo) < cota-superior } para cada hijo en compleciones (nodo) hacer si condiciones-de-poda (hijo) y

cota-inferior (hijo) < cota-superior entonces añadir-nodo (hijo, m) fsi

fsi fpara fsi fmientras ffun

En este caso usaremos una estructura de datos montículo que nos hará escoger siempre el nodo más prometedor (lo usaremos como una lista con prioridad). Es el mismo esquema que previamente, sólo que añadimos la selección del camino que nos lleve antes a solución. Se añaden un par de líneas en este esquema que son:

si cota-inferior (nodo) ≥ cota-superior entonces devolver solución

Esto significará que cuando en un nodo tengamos una cota-inferior (recordemos que es la estimación hasta encontrar la solución) igual o mayor a la cota-superior (que es el coste de la mejor solución encontrada hasta el momento) entonces no podremos llegar por ningún otro nodo del montículo a una solución mejor, por lo que dejamos de explorar el resto del grafo implícito (devolvemos la solución mejor y salimos del bucle).

Page 182: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 32 de 38

Los dos esquemas anteriores para problemas de minimización, ya que iremos rebajando la cota superior una vez encontrada la solución si la mejora (podaremos más ramas).

NOTA DEL AUTOR: Estos dos esquemas se han sacado completamente del libro de problemas, y el siguiente es totalmente personal, a partir del anterior.

Igualmente (y como añadido del autor) podremos emplearlo para problemas de maximización como sigue (aunque lo veremos en los problemas resueltos). La única diferencia apreciable será que se actualizará la cota inferior (se incrementará) al encontrar una mejor solución y las comparaciones serán con respecto a la cota-superior del nodo, no obstante, la filosofía será similar.

fun ramificación-y-poda (ensayo) m ← montículo-vacío cota-inferior ← inicializar-cota-inferior solución ← solución-vacía añadir-nodo (ensayo, m) mientras no vacío (m) hacer nodo ← extraer-raíz (m) si valido (nodo) entonces si coste (nodo) > cota-inferior entonces solución ← nodo cota-inferior ← coste (nodo) fsi si no { Nodo no es válido (solución) } si cota-superior (nodo) ≤ cota-inferior entonces devolver solución si no { cota-superior (nodo) > cota-inferior } para cada hijo en compleciones (nodo) hacer si condiciones-de-poda (hijo) y

cota-superior (hijo) > cota-inferior entonces añadir-nodo (hijo, m) fsi

fpara fsi fsi fmientras ffun

Page 183: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 33 de 38

9.7.1. El problema de la asignación

Hay que asignar n tareas a n agentes, de forma que cada agente realice exactamente una tarea. Si el agente i, con 1 ≤ 푖 ≤ 푛, se le asigna la tarea j, con 1 ≤ 푗 ≤ 푛, entonces el coste de realizar esta tarea será 퐶 . Dada la matriz de costes completa, el problema consiste en asignar tareas a los agentes de tal manera que minimice el coste de ejecución de las n tareas.

Veremos un ejemplo de este problema, en la que se nos da esta matriz de costes:

Tareas

Agentes

1 2 3 a 4 7 3 b 2 6 1 c 3 9 4

Si asignamos la tarea 1 al agente a, la tarea 2 al agente b y la tarea 3 al agente c, nuestro coste total será: 4 + 6 + 4 = 14. La asignación óptima es 푎 →2,푏 → 3 푦 푐 → 1, cuyo coste es 7 + 3 + 1 = 11.

Este problema tiene numerosas aplicaciones. En general, con n agentes y n tareas, hay 푛! posibles asignaciones que considerar, que son demasiadas incluso para valores moderados de n. Por tanto, recurriremos a la ramificación y poda.

Veremos otro ejemplo más e iremos paso a paso resolviéndolo. De nuevo, la matriz de costes será:

Tareas

Agentes

1 2 3 4 a 11 12 18 40 b 14 15 13 22 c 11 17 19 23 d 17 14 20 28

El primer paso es obtener la cota superior inicial del problema:

1ª solución posible: Diagonal principal: 11 + 15 + 19 + 28 = 73. 2ª solución posible: Diagonal secundaria: 40 + 13 + 17 + 17 = 83.

La segunda solución posible no supone mejora respecto a la primera. Por tanto, tomamos la primera de ellas como cota superior.

Page 184: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 34 de 38

El segundo paso es obtener la cota inferior del problema. Para ello sumamos los elementos más pequeños. En este ejemplo calcularemos dos posibles cotas inferiores (son estimaciones, no son soluciones reales):

1ª cota inferior: Será aquélla que sale de asignar a cada agente la tarea que mejor sabe hacer. En este caso, inicialmente tendremos 11 + 12 + 13 +22 = 58 2ª cota inferior: Será aquélla que sale de asignar a cada tarea el agente que mejor la realiza. Por lo que tendremos 11 + 13 + 11 + 14 = 49.

En este caso, al ser un problema de minimización, tendremos que la segunda cota inferior será peor que la primera, ya que la solución óptima no puede ser mejor que 49, por ser imposible este resultado. Por ello, tendremos la mayor de las cotas inferiores.

La cota donde, por tanto, estará ubicada la solución será la del intervalo [58. .73]. NOTA DEL AUTOR: Al ser problema de minimización la cota inferior será la mayor de las cotas inferiores, a diferencia de los problemas de maximización que será la menor de las cotas superiores y calcularíamos las dos posibles cotas superiores.

Realizaremos el cálculo de las cotas inferiores de manera recursiva hasta encontrar una solución válida, que nos resuelva el problema. Exploraremos un árbol cuyos nodos conexos corresponden a asignaciones parciales. En la raíz del árbol no se han hecho asignaciones. En lo sucesivo, en cada nivel se determina la asignación de un agente más. Para cada nodo, calculamos una cota de las soluciones que se pueden obtener completando la asignación parcial correspondiente y utilizar esta cota para cerrar caminos y guiar la búsqueda. Asignamos la tarea al agente a, por lo que nos queda:

Las cotas inferiores para la asignación 푎 → 1 serán:

1ª cota inferior: 11 + 13 + 17 + 14 = 55 (asignar a cada agente la tarea). 2ª cota inferior: 11 + 14 + 13 + 12 = 60 (asignar a cada tarea el agente).

a -> 4

a -> 2

a -> 3

a -> 1

Page 185: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 35 de 38

Cualquier solución que se pueda alcanzar por esta rama va a tener coste máximo de 60. Por tanto, calculando igual para el resto de asignaciones tendremos el siguiente árbol:

60 58 65 78*

El asterisco (*) indica que la rama se poda por superar el valor máximo del intervalo antes puesto, por ello, no se seguirá explorando. Lo denominaremos nodo muerto.

Seguimos por la rama de cota inferior más baja: 58, que es la rama más prometedora. Las ramas que se pueden seguir explorando las denominaremos nodo vivo.

Seguimos por la rama 푎 → 2, que como hemos puesto es la rama de cota inferior más baja (la que nos optimice la solución) como sigue:

60 65

78*

Continuamos haciendo el mismo procedimiento de antes y llegamos a calcularlo para los nodos que salen de 푎 → 2. Por ejemplo, 푎 → 2, 푏 → 1:

1ª cota inferior: 푎 → 2,푏 → 1, 푐 → 3, 푑 → 3: 12 + 14 + 19 + 20 = 65 (asignar a cada agente la tarea). 2ª cota inferior: 푎 → 2,푏 → 1, 푐 → 3, 푐 → 4: 12 + 14 + 19 + 23 = 68 (asignar a cada tarea el agente).

a -> 4

a -> 2

a -> 3

a -> 1

a -> 4

a -> 2

a -> 3

a -> 1

a -> 2, b -> 4

a -> 2, b -> 3

a -> 2, b -> 1

Page 186: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 36 de 38

La cota inferior está limitada por el valor 68. Calcularemos de igual manera las demás cotas. Por lo que tendremos:

60 68 59 65 64 78*

NOTA DEL AUTOR: Hemos visto que el valor 59 es la menor estimación de cota inferior mejorando a la del intervalo, ya que el coste de la solución nunca será inferior a 59. Por ello se actualiza el intervalo a [59. .73]. No está hecho así en el ejercicio, simplemente es para comprenderlo.

De nuevo, seguimos por 푎 → 2,푏 → 3, que nos quedará:

60 68 64 65 65 64 78*

Ya encontramos dos posibles soluciones. Como es similar al paso anterior, nos evitamos hacer otro grafo de nuevo y ponemos las cotas inferiores. Puesto que la primera solución mejora a la del intervalo será nuestra nueva cota inferior, quedando [59. .64]. Inmediatamente, podamos las ramas tras la actualización que sean superiores a 64. Sólo queda una rama menor de 64, que es la de 푎 → 1. Por tanto, desarrollando ese árbol como hemos hecho antes, tendremos lo siguiente:

a -> 4

a -> 2

a -> 3

a -> 1

a -> 2, b -> 4

a -> 2, b -> 3

a -> 2, b -> 1

a -> 4

a -> 2

a -> 3

a -> 1

a -> 2, b -> 4

a -> 2, b -> 3

a -> 2, b -> 1

a -> 2, b -> 3, c -> 4, d -> 1

a -> 2, b -> 3, c -> 1, d -> 4

Page 187: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 37 de 38

68* 69* 61 66* 68* 64*

65* 65* 64* 78*

Como no quedan más ramas que explorar la solución 푎 → 1,푏 → 3, 푐 →4,푑 → 2 es la óptima. En este algoritmo lo hemos resuelto siguiendo nuestro segundo esquema (aquél que guiaba la búsqueda).

9.7.2. El problema de la mochila (4)

Se nos pide maximizar ∑ 푥 ∗ 푣 sometido a la restricción ∑ 푥 ∗ 푤 ≤푊, donde 푣 y 푤 son estrictamente positivas y los 푥 son enteros no negativos. Resolveremos este problema por ramificación y poda.

Las variables están numeradas de ≥ . Si los valores de 푥 ,푥 , … , 푥 , 0 ≤ 푘 ≤ 푛 quedan fijados, con ∑ 푥 ∗ 푤 ≤ 푊, es fácil ver que el valor que se puede obtener sumando más elementos de los tipos 푘 + 1, … ,푛 a la mochila no puede sobrepasar el valor:

∑ 푥 ∗ 푣

+ 푤 −∑ 푥 ∗ 푤 ∗

ñ

Para resolver el problema de ramificación y poda, exploramos un árbol en cuya raíz no está fijado el valor de ninguno de los 푥 y en cada nivel sucesivo se va determinando el valor de una variable más, por orden numérico de variables. En cada nodo que exploremos, sólo generamos aquellos sucesores que satisfagan la restricción de peso, de tal manera que cada nodo tiene un número finito de sucesores. Siempre que se genera un nodo, calculamos una cota superior del valor de la solución que se puede obtener la carga

a -> 4

a -> 2

a -> 3

a -> 1

a -> 1, b -> 4

a -> 1, b -> 3

a -> 1, b -> 2

a -> 1, b -> 3, c -> 4, d -> 2

a -> 1, b -> 3, c -> 2, d -> 4

a -> 2, b -> 4

a -> 2, b -> 3

a -> 2, b -> 1

a -> 2, b -> 3, c -> 4, d -> 1

a -> 2, b -> 3, c -> 1, d -> 4

Page 188: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Resumen tema 9 Curso 2007-08 Página 38 de 38

parcialmente especificada y utilizando estas cotas para cortar ramas inútiles y guiar la exploración del árbol. NOTA DEL AUTOR: Hay una errata del libro en la que el símbolo de la restricción es mayor o igual, cuando debería ser menor o igual, por lo que la errata se ha subsanado anteriormente.

9.7.3. Consideraciones generales

Usaremos montículos para almacenar una lista de nodos generados pero no explorados. No se dispone de una formulación recursiva elegante de ramificación y poda. Resulta imposible dar una idea precisa de lo bien que se va a comportar esta técnica en un problema, empleando una cota dada. Hay que llegar a un compromiso en respuesta a la cota calculada. Si es cota mejor examinaremos menos nodos y llegaremos a la solución óptima más rápidamente si hay suerte. Pero podemos pasar mucho tiempo calculando la cota correspondiente. En el caso peor, tendremos una cota excelente, pero no podamos ninguna rama, por lo que desperdiciamos tiempo. En la práctica, casi siempre es rentable invertir el tiempo necesario para calcular la mejor cota posible.

Page 189: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Tema 10. Resumen de las ecuaciones.

Page 190: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

La regla del límite: Nos permite comparar dos funciones en cuanto a la notación asintótica se refiere. Tendremos que calcular el siguiente límite:

lim →( )( )

.

Al resolver el limite se nos darán 3 posibles resultados:

1. lim →( )( )

= 푐 ∈ 푅 ⇒

푓(푛) ∈ 푂 푔(푛) 푓(푛) ∈ 훺 푔(푛) 푓(푛) ∈ 휃 푔(푛) 푔(푛) ∈ 푂 푓(푛) 푔(푛) ∈ 훺 푓(푛) 푔(푛) ∈ 휃 푓(푛)

.

Estas funciones se comportan igual, diferenciándose en una constante multiplicativa.

2. lim →( )( )

= ∞ ⇒

푓(푛) ∉ 푂 푔(푛) 푓(푛) ∈ 훺 푔(푛) 푓(푛) ∉ 휃 푔(푛) 푔(푛) ∈ 푂 푓(푛) 푔(푛) ∉ 훺 푓(푛) 푔(푛) ∉ 휃 푓(푛)

.

Por muy alta que sea la constante multiplicativa de 푔(푛) nunca superará a 푓(푛).

3. lim →( )( )

= 0 ⇒

푓(푛) ∈ 푂 푔(푛) 푓(푛) ∉ 훺 푔(푛) 푓(푛) ∉ 휃 푔(푛) 푔(푛) ∉ 푂 푓(푛) 푔(푛) ∈ 훺 푓(푛) 푔(푛) ∉ 휃 푓(푛)

.

푔(푛) crece más exponencialmente que 푓(푛), por lo que sería su cota superior.

Page 191: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

Tendremos dos tipos:

- Reducción por sustracción: La ecuación de la recurrencia es la siguiente:

푐 ∗ 푛 si 0 ≤ 푛 < 푏 푇(푛) =

푎 ∗ 푇(푛 − 푏) + 푐 ∗ 푛 si 푛 ≥ 푏 La resolución de la ecuación de recurrencia es: 휃(푛 ) si 푎 < 1 푇(푛) = 휃(푛 ) si 푎 = 1

휃 푎 si 푎 > 1

- Reducción por división: La ecuación de la recurrencia es la siguiente:

푐 ∗ 푛 si 1 ≤ 푛 < 푏 푇(푛) =

푎 ∗ 푇(푛/푏) + 푐 ∗ 푛 si 푛 ≥ 푏 La resolución de la ecuación de recurrencia es: 휃(푛 ) si 푎 < 푏 푇(푛) = 휃 푛 ∗ 푙표푔(푛) si 푎 = 푏

휃 푛 si 푎 > 푏

siendo: a: Número de llamadas recursivas. b: Reducción del problema en cada llamada. 풄 ∗ 풏풌: Todas aquellas operaciones que hacen falta además de las de recursividad.

Page 192: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

El esquema voraz es el siguiente:

funcion voraz (C: Conjunto): conjunto { C es el conjunto de candidatos } 푆 ← ∅ { Construimos la solución en el conjunto S } mientras 퐶 ≠ 0 y ¬solución (푆) hacer 푥 ← 푠푒푙푒푐푐푖표푛푎푟 (퐶) 퐶 ← 퐶\{푥};

si factible (푆 ∪ {푥}) entonces 푆 ← 푆 ∪ {푥} si solución (푆) entonces devolver S

si no devolver “no hay solución”

Los esquemas de vuelta atrás:

fun vuelta-atrás (ensayo) si valido (ensayo) entonces devolver ensayo si no para cada hijo en compleciones (ensayo) hacer si condiciones-de-poda (hijo) entonces vuelta-atrás (hijo) fsi fpara fsi

ffun

fun vueltaatrás (푣[1. .푘]) { v es un vector k-prometedor } si v es una solución entonces escribir v si no para cada vector (k+1)-prometedor w

tal que 푤[1. . 푘] = 푣[1. .푘] hacer vueltaatrás (푤[1. .푘 + 1])

Page 193: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8

El de ramificación y poda (problemas de maximización):

fun ramificación-y-poda (ensayo) m ← montículo-vacío cota-superior ← inicializar-cota-superior solución ← solución-vacía añadir-nodo (ensayo, m) mientras no vacío (m) hacer nodo ← extraer-raíz (m) si valido (nodo) entonces si coste (nodo) < cota-superior entonces solución ← nodo cota-superior ← coste (nodo) fsi si no { Nodo no es válido (solución) } si cota-inferior (nodo) ≥ cota-superior entonces devolver solución si no { cota-inferior (nodo) < cota-superior } para cada hijo en compleciones (nodo) hacer si condiciones-de-poda (hijo) y

cota-inferior (hijo) < cota-superior entonces añadir-nodo (hijo, m) fsi

fsi fpara fsi fmientras ffun

El esquema de divide y vencerás:

fun divide-y-vencerás (problema) si suficientemente-simple (problema) entonces dev solucion-simple (problema) si no { No es solución suficientemente simple } {푝 . .푝 } ← decomposicion (problema) para cada 푝 hacer 푠 ← divide-y-vencerás (푝 ) fpara dev combinacion (푠 … 푠 ) fsi ffun

Page 194: RESUMENES PROGRAMACIÓN III - extensionuned.es · RESUMENES PROGRAMACIÓN III ... Fundamentos de algoritmia. G. Brassard y P. Bratley . Resumen tema 2 Curso 2007/08 Página 3 de 8