Resumenesprogramacioniii
Resumenesprogramacioniii
Resumenesprogramacioniii
RESUMENES
PROGRAMACIÓN III
Curso 2008 ‐ 2009
Resumen de programación 3
Tema 1. Preliminares.
Bibliografía:
Se ha tomado apuntes del libro:
Bibliografía:
Se ha tomado apuntes del libro:
(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.
(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.
≤ ∗ + ∗ + ∗ ≤ max ( , , )∗( + + )
siendo:
, , : Variables según la implementación.
, , : Constantes que cambiarán de una implementación a otra.
1. Vector ya ordenado:
i
(5) Coste n
Bibliografía:
Se ha tomado apuntes del libro:
( )
( )
(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í:
∈ ( )
Transitiva: ( ) ∈ ( ) ( )∈ ℎ( ) ⇒ ( ) ∈ ℎ( ) .
La demostración será:
∃ ∈ℝ , ∈ℕ ∀ > , ( )= ∗ ( ) Por definición.
,
∃ ∈ℝ , ∈ℕ ∀ > , ( )= ∗ ℎ( ) Por definición.
( )≤ ∗ ( )≤ ∗ ∗ ℎ( ) ( )∈ ℎ( )
( )≈ ( ) ∈ ( ) : Cota inferior.
( )∈ ( ) : Cota superior.
( )
(umbral)
( )∈ ( ) ( )∈ ( ) ( )∈ ( )
⇒ .
( )∈ ( ) ( )∈ ( ) ( )∈ ( )
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.
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
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.
Resumen tema 4 Curso 2007/08 Página 3 de 12
El coste del algoritmo, siguiendo la regla del máximo será:
( )∈ á ( ), ( )
Si =0ó
= 1 ( < 2)
( )= ( − 1) + ( − 2) + ℎ ( ) En caso contrario
j i
= −1 − +1= ≈ .
= − −1 +1 = ≈ .
∗ si 0 ≤ <
( )=
∗ ( − )+ ∗ si ≥
( ) si <1
( )= ( ) si =1
si >1
∗ si 1 ≤ <
( )=
∗ ( / )+ ∗ si ≥
( ) 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.
Bibliografía:
Se han tomado apuntes de los libros:
Vectores Punteros
pila-vacía cte cte
apila cte cte
desapila cte cte
vacía cte cte
llena cte cte
altura cte cte
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].
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.
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.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.
NIL
Vectores Punteros
lista-vacía cte cte
añadir cte cte
resto cte cte
vacía cte cte
miembro ( ) ( )
elemento cte ( )
primero cte cte
alpha beta
gamma delta
b c
a b
d e
a b
d e
a b
alpha beta
gamma delta
={ ℎ , , , }.
( ℎ , ), ( ℎ , ), ( , ), ( , ℎ ),
= .
( , ), ( , )
3 1 5
7 4
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
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:
alpha
beta gamma
alpha
beta gamma
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.
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:
∑ = .
2
3
4 5 6 7
8 9 10 11 12 13 14
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.
[2 ∗ ]
Por tanto, el nodo en la posición [ ] es el padre de , que son
[ 2 ∗ + 1]
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 ∗ ]
Hijos con respecto a padres.
[ ] ≥ [ 2 ∗ + 1]
[ ]≤ [ 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.
10
7
9
4 7 5 2
2 1
Vemos que cada padre es mayor o igual que sus hijos. Por tanto, es
montículo.
7
9
4 7 5 2
2 1
7
3
4 7 5 2
2 1
7
5
4 7 3 2
2 1
7
5
4 7 3 2
2 8
7
5
8 7 3 2
2 4
A continuación, hay que hacer otro cambio para que cumpla de nuevo
la propiedad del montículo:
8
5
7 7 3 2
2 4
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.
8
5
7 7 3 2
2 4
8
5
7 7 3 2
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:
7
5
4 7 3 2
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)
6
9
2 7 5 2
7 4 10
2 7 5 2
7 4 10
7 10
2 4 7
10
7 7
2 4 6
10
9
7 7 5 2
2 4 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.
( ) si <1
( )= ( ) si =1
si >1
desordenado 1ª raíz
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:
rótulo3
rótulo1
rótulo2
Ejemplo:
1 2 3 4 5 6 7 8 9 10
1 2 3 2 1 3 4 3 3 4
1 2 3
5 4 6 8 10
7 10
Hijo de 1
1 2
5 4
7 10
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.
Bibliografía:
Se han tomado apuntes de los libros:
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.
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
a b
2 3
c
Resumen tema 6 Curso 2007/08 Página 6 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
a b
1 1
c
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.
V de longitud mínima
N/B B
1 2
1 2 3
4 6 4 5 6
4 3 5 8 6
4 7 3
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( )).
1 2 3
4 6 4 5 6
4 3 5 8 6
4 7 3
1 2 3
4 6 4 5 6
4 3 5 8 6
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 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).
Resumen tema 6 Curso 2007/08 Página 16 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
Prim Kruskal
General ( ) ( ∗ log( ))
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.
Origen
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.
1
10 50
100 30
5 2
10 20 5
4 50 3
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]
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.
Origen
S
v
El camino especial más corto
←
devolver x
Seleccionar: xi Valor
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.
P’
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.
r s t p u v q w Conjunto óptimo
′ x y p r q
′ u s t p r v q w
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 .
Primer intento: 2 1
Tercero intento: 2 1 4
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.
= , ( ) .
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. . ]
0 1 2 3
0 1 2
0 2
1 3
1 2
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
a b c d
e f g h
ℎ∗ ℎ∗ ℎ∗ ℎ∗
∗
+
+
Nuestra resolución de la reducción por división tiene 3 casos distintos, que son:
( ) si <
( )= ∗ ( ) si =
si >
Despejamos ( ∗ + ∗ ) y queda:
( ∗ + ∗ )= − ∗ − ∗
= ∗ ℎ( ) + ( ).
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.
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.
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 >
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í:
Al vector T
<p >p
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:
3 1 4 1 5 9 2 6 5
la mediana es el número 4, porque ordenado es:
1 1 2 3 4 5 5 6 9
( ) si <1
( )= ( ) si =1
si >1
<p =p >p
3 1 4 1 5 9 2 6 5
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.
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.
= = =
( / )
=
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.
0 si = 1
( , )≤ ( , /2) + ( /2, /2) si n es par
( , − 1) + , ( − 1) si n es impar
Multiplicación
Operaciones Clásica DyV
elementales
exposec ( ) ( )
expoDV (log( )) ( )
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
B C
D E F G H
I J K L M
1
A
2 7
B C
3 4 6 8 9
D E F G H
5 10 11 12 13
I J K L M
13
A
5 12
B C
1 3 4 6 11
D E F G H
2 7 8 9 10
I J K L M
[ ]≥ [ ]⇔ ,
á . á
Se sigue que:
[ ]≤ [ ] [ ]≥ [ ]⇔
2 3 4
5 6 7 8
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 ( , ) .
2 3 4
5 6 7 8
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.
A B
D F
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
D
C
B
A
E
D
C
B
A
D
C
B
A
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
C
B
A
B
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.
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.
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.
2 3 4
5 6 7 8
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
4 3 2
6 5 4 3
6 5 4
8 7 6 5
8 7 6
8 7
A B
D F
BA
C B A
CB
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.
DC
F DC
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
FD
E F D
EF
………
……………………
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.
……
Solución
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.
2; 3 3; 5 4; 6 5; 10
2,2,2,2; 12
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.
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).
1 8
1 X
X
0-prometedor
1-prometedor
2-prometedor
n-prometedor
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).
Tareas
1 2 3
Agentes
a 4 7 3
b 2 6 1
c 3 9 4
Tareas
1 2 3 4
Agentes
a 11 12 18 40
b 14 15 13 22
c 11 17 19 23
d 17 14 20 28
a -> 1
a -> 2
a -> 3
a -> 4
a -> 1
60
a -> 2 58
a -> 3 65
a -> 4 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:
a -> 4 78*
a -> 4 78*
a -> 4 78*
a -> 4 78*
( )∈ ( ) ( )∈ ( ) ( )∈ ( )
⇒ .
( )∈ ( ) ( )∈ ( ) ( )∈ ( )
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.
Tendremos dos tipos:
- Reducción por sustracción:
La ecuación de la recurrencia es la siguiente:
∗ si 0 ≤ <
( )=
∗ ( − )+ ∗ si ≥
( ) si <1
( )= ( ) si =1
si >1
∗ si 1 ≤ <
( )=
∗ ( / )+ ∗ si ≥
( ) 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.
El esquema voraz es el siguiente: