Professional Documents
Culture Documents
Nota 1: La presente colección de finales resueltos que usted está consultando fueron preparados por un
alumno mientras preparaba el examen. Existe la posibilidad de que hayan cosas mal resueltas, así que no se
confíe!
Nota 2: Recomiendo visitar la siguiente página para “ver” los algoritmos: https://www.cs.usfca.edu/
~galles/visualization/Algorithms.html.
Índice
1. Preguntas que nos dio la última clase 3
Preguntas sobre TDA conjunto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Pregunta a . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Pregunta b . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Pregunta c . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Pregunta d . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
Verdadero o falso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1
ÍNDICE ÍNDICE
4. Final 1 20
Problema 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Pregunta 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
Pregunta 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Pregunta 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Pregunta 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
Pregunta 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Problema 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Problema 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Pregunta 1 (incompleta) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
Pregunta 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
5. Final 2 24
Problema 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Pregunta 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
Pregunta 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Problema 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Problema 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Problema 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Preguntas 1 y 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
Pregunta 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
6. Final 3 30
Problema 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Problema 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Pregunta 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Pregunta 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Pregunta 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Pregunta 4 (no tengo idea) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Pregunta 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Problema 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Preguntas 1 y 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Pregunta 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
Problema 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Pregunta 1 (falta completar) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
Pregunta 2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
a) Indique implementaciones que provean un alta O (1) para el peor caso y describa los algoritmos
correspondientes.
b) Indique implementaciones que provean baja O (n) para el peor caso y describa los algoritmos.
c) Indique implementaciones que provean la operación “pertenece”, para el peor caso:
1) O (1).
2) O (log n).
3) O (n).
d) Si se requiere que se provea la operación intersección entre conjuntos, describa el costo para distintas
implementaciones, justificando con la descripción del algoritmo correspondiente.
Pregunta b
Se me ocurre una implementación con una lista o con un arreglo no ordenado. En el peor caso el elemento
que se quiere dar de baja está al final de todo y entonces el tiempo hasta encontrarlo (y borrarlo) es O (n).
Pregunta c
La operación “pertenece” básicamente tiene que buscar el elemento en el conjunto para luego tomar la
decisión.
Una implementación que sea O (1) implica que el elemento es encontrado en tiempo constante. Esto es
posible únicamente en el caso en que ya se conozca de antemano a dónde ir a buscar el elemento. Es
decir que el conjunto almacena los elementos en un arreglo de acceso constante y tamaño definido. Una
implementación que permita lograr esto solo es aplicable a conjuntos de tamaño máximo finito, es decir
un conjunto que de antemano tiene definida la máxima cantidad de elementos que puede almacenar. Por
ejemplo un conjunto de colores primarios (rojo, azul y amarillo) se puede implementar con un arreglo de
tamaño 3 y asignarle a cada color una posición fija. La operación “pertenece” será O (1). En cambio, un
conjunto de “colores” donde los colores pueden ser cualesquiera definidos por una terna RGB no se podría
implementar así ya que son infinitos.
Una implementación que permita determinar si un elemento pertenece o no al conjunto con tiempo
O (log n) podría ser con un árbol de búsqueda. Este tipo de implementación solo es aplicable a conjuntos
cuyos elementos sean “comparables”, es decir para los cuales se pueda definir las operaciones “mayor” y
“menor” para así ir buscándolos en el árbol. Un ejemplo en el que esto no se puede aplicar es un conjunto
de colores.
Implementaciones que provean la operación “pertenece” en tiempo O (n) son las más sencillas: una lista
o un arreglo no ordenado. En este caso para saber si un elemento está o no en el conjunto hay que ir uno
por uno buscándolo.
Pregunta d
Implementación con bytes Si se realiza la implementación de bajo nivel hecha con bytes (ver respuesta a
la pregunta a) entonces la operación intersección entre dos conjuntos es de tiempo constante ya que se realiza
con una operación AND entre los dos bytes1 . Si se utiliza más de un byte para implementar el conjunto entonces
la complejidad comenzará a crecer ya que la operación de AND deberá realizarse para varios bytes.
l Simse utilizan
NB bytes y cada byte tiene Nb bits entonces un conjunto de n elementos requerirá de NB = Nnb bytes. Se
observa que para n → ∞ se obtiene NB ∝ n por lo tanto habrá que hacer una cantidad de operaciones AND
proporcional a n por lo que es O (n).
Implementación con arreglo no ordenado A la hora de construir el conjunto intersección habrá que
tomar uno de los conjuntos y recorrerlo elemento a elemento preguntándose, en cada uno, si está o no en el
otro conjunto. Este recorrido ya impone una cota Θ (n1 ) donde n1 es la cantidad de elementos del conjunto 1.
En el peor de los casos ambos conjuntos son iguales y la operación “pertenece al conjunto 2” es O (n2 ) lo cual
termina resultando en que el tiempo total de la intersección es O (n1 n2 ).
1 En clase lo vimos así, pero esto en realidad no es el análisis asintótico porque si se utilizó un único byte entonces n es constante...
Implementación con árboles de búsqueda (balanceado) En este caso también habrá que recorrer al
primer conjunto elemento a elemento verificando si está o no en el segundo, por lo tanto nuevamente se tiene una
cota Θ (n1 ). Al tratarse de un árbol balanceado entonces la búsqueda de un elemento en el segundo conjunto
será de complejidad O (log n2 ) por lo tanto la operación de intersección termina siendo O (n1 log n2 ). En base a
esto se observa que siempre conviene poner en primer lugar (o sea en n1 ) al conjunto con la menor cantidad de
elementos.
Implementación con trie Cuando los elementos que contendrá el conjunto lo permiten, una implementación
con trie es conveniente ya que la búsqueda es muy rápida. Si se considera un conjunto de palabras del idioma
castellano entonces la búsqueda de una palabra de k caracteres es O (k) y no depende de n. Esto implica que
la intersección se podrá computar en tiempo Θ (n1 ) sin importar el tamaño de n2 (nota2 ).
Verdadero o falso
a) Verdadero. La programación dinámica reduce el problema a uno sencillo y luego comienza a avanzar de a
un paso por vez hasta resolver el problema original.
d) En base a lo que dice Wikipedia3 sobre top-down y bottom-up yo creo que la programación dinámica usa
el esquema bottom-up ya que resuelve problemas pequeños que cada vez son más grandes hasta resolver el
problema original.
e) No tengo idea.
g) Verdadero. En el peor de los casos existe la posibilidad de que la rama que resuelve el problema sea
explorada por el método backtracking al final de todo, haciendo que la resolución del problema sea tan costosa
como la de fuerza bruta.
h) Falso. La estrategia de fuerza bruta se aplica a todos los problemas. Simplemente consiste en probar todas
las soluciones una por una hasta encontrar la mejor.
i) No sé.
2 El corrector ortográfico del Word hace justamente esto. Toma el conjunto de palabras n que están en el texto y las busca en
1
el conjunto de palabras n2 del idioma configurado, que según algunos textos que leí suele estar implementado con una estructura
trie. Si tuviera alguna clase de dependencia con n2 entonces sería lentísimo, pero no.
3 https://en.wikipedia.org/wiki/Top-down_and_bottom-up_design .
Problema 2
1. Defina árbol B.
2. Indique usos de la estructura.
3. Considere un árbol B de 5 vías (órden 5).
a) Muestre gráficamente la evolución del mismo, inicialmente vacío. Las claves que ingresan son: 200,
250, 150, 180, 200, 300, 40, 50, 60, 35, 10, 40.
b) Luego se borran estas claves: 200, 300, 150, 10, 35. Mostrar gráficamente el borrado.
4. ¿Cuántas claves tiene como máximo un árbol B de altura h?
Problema 3
1. Defina el árbol heap, indique coste temporal del alta y de la baja.
2. Explique qué relación hay entre la estructura heap y la estructura greedy de resolución de algoritmos (es
decir, para qué suele usarse un heap en esa estrategia y en qué beneficia a la resolución del problema).
Problema 4
1. Describa un algoritmo que permita determinar si un grafo tiene ciclos o no. ¿Cuál es el coste del algoritmo
que ha descripto? Justifique.
2. Explique qué es un recorrido topológico.
3. En el siguiente grafo, si es posible, realice un recorrido topológico e indique la salida.
4. Describa con detalle el algoritmo.
Resolución
Problema 1
Ítem 1
Un trie es una clase de árbol en la que la búsqueda se hace siguiendo una secuencia de caracteres de la clave
que se desea encontrar. Cada nodo de un trie tiene R punteros (donde R es el tamaño del alfabeto) y cada uno
de estos punteros se corresponde con un elemento del alfabeto. Cada nodo puede tener además un determinado
valor que es el asociado a la clave. Si dicho valor es NULL entonces la clave no está en el árbol. Esto está bien
detallado en [1, pag. 732].
A juzgar por lo que dice el ítem 2 (que tiene el terminador λ) me parece que en clase vimos la estructura
que presenta [2, sec. 7.2] (en donde usa como terminador el símbolo #).
Ítem 2
Subítem a. Si se lo implementa con arreglos de punteros entonces quedaría algo así:
a b c λ
a b c λ a b c λ
b
a b c λ a b c λ a b c λ
aa
a b c λ a b c λ a b c λ a b c λ a b c λ
aab
a b c λ abc bca a b c λ bcca
abbc
Ésta es la implementación que usa [2, sec. 7.2]. Los elementos indicados como a, b, c y λ son punteros y
cuando no apuntan a nada (en el dibujo) significa que son NULL.
Subítem b. Implementándolo con listas no sé bien cómo sería. Me parece que es la implementación que está
en [2, fig. 7.35], pero no estoy seguro. Asumiendo que sí, entonces sería:
a b
a b c λ
λ b c λ a c
c λ λ a
Todos los punteros que están sin indicar nada son NULL.
Ítem 3
El principal problema de los tries (implementados con arreglos de punteros) es la gran cantidad de espacio
que utilizan (y desperdician) [2, pag. 348]. Cuando el árbol no está completo quedan muchos punteros nulos, lo
cual se traduce en espacio desperdiciado.
La segunda implementación es mejor en este sentido: cuando el árbol está incompleto desperdicia menos
espacio. El problema que tiene es que cuando está completo ocupa más espacio (se guardan dos punteros por
cada elemento mientras que antes uno solo) y es más lento el acceso (esto afecta cuando se tiene un alfabeto
con muchos elementos).
Item 4
Caso de arreglo de punteros. Para determinar si una clave está o no en el trie se procede verificando el
puntero correspondiente a cada caracter en cada nivel. Por ejemplo supóngase que se quiere verificar si la clave
“abc” está en el trie. Entonces se entra en la raíz del árbol y se busca el puntero correspondiente al primer
elemento de la cadena, es decir el puntero “a”:
abc a b c λ
a b c λ a b c λ
Como dicho puntero no es nulo entonces seguimos avanzando. En el segundo nivel buscamos el puntero
correspondiente al segundo caracter de la cadena, es decir “b”:
abc a b c λ
abc a b c λ a b c λ
b
a b c λ a b c λ a b c λ
aa
A continuación buscamos el tercer elemento de la cadena, “c”, en el tercer nivel del árbol:
abc a b c λ
abc a b c λ a b c λ
abc
b c λ a b c λ a b c λ
aa
b c λ a b c λ a b c λ a b
Finalmente, como ya se alcanzó el final de la cadena, se busca si el terminador λ está referenciado a NULL o
no:
abc a b c λ
abc a b c λ a b c λ
abc
a b c λ a b c λ
aa
a b c λ a b c λ a
aab
a b c λ abc Está en el árbol
donde la notación cadena[2:end] es la notación de Octave/Matlab que significa: todos los elementos de la
cadena desde el 2 hasta el último.
Caso de lista de punteros. La idea básicamente es la misma pero cambia la forma en que se accede a la
información. El pseudocódigo sería más o menos así:
1 // P s e u d o c ó d i g o.
2 E s t á _ e n _ t r i e( cadena , t ) { // I m p l e m e n t a c i ó n con lista de p u n t e r o s.
3 i f ( length ( cadena ) == 0) { // Base case .
4 i f ( t . clave == lambda )
5 return S Í _ E S T Á;
6 else
7 return N O _ E S T Á;
8 } else {
9 e l q u e b u s c o = cadena [1]; // Este es el e l e m e n t o que
b u s c a m o s.
10 i f ( t . clave == e l q u e b u s c o) // Avanzo al s i g u i e n t e nivel
del árbol .
11 return E s t á _ e n _ t r i e ( cadena [2: end ] , t . izq ) ;
12 e l s e i f ( t . clave < e l q u e b u s c o) { // Tengo que a v a n z a r en
la lista de p u n t e r o s del nivel actual .
13 i f ( t . der != NULL )
14 return E s t á _ e n _ t r i e( cadena , t . der ) ;
15 else
16 return N O _ E S T Á;
17 } e l s e // if ( t . clave > e l q u e b u s c o)
18 return N O _ E S T Á;
19 }
20 };
Quizá me olvidé de algo, pero la idea es la misma salvo que con más precauciones por el tema de no meternos
en un puntero nulo y cosas así.
Ítem 5
El tiempo de este algoritmo es4
(
O (k) Implementación con arreglos de punteros
T (k) ∈
No sé Implementación con listas de punteros
4 Acá http://stackoverflow.com/questions/17891442/what-is-the-best-worst-average-case-big-o-runtime-of-a-trie-data-structure
donde k es la cantidad de caracteres de la clave que se desea encontrar (o sea la cantidad de elementos de la
cadena). Es independiente del tamaño del árbol, sólo depende de la clave.
Ítem 6
Los tries se usan para armar diccionarios en los que se quiere asegurar que la búsqueda sea rápida. Parece
que son bastante usados en los sistemas de autocompletado predictivo de texto de los celulares por ejemplo
debido a su elevada velocidad.
Según [2, sec. 7.4] se usan para los correctores ortográficos en los procesadores de texto.
Problema 2
Ítem 1
Nosotros en la clase vimos que un árbol B es un árbol multivía que verifica:
Todos los nodos, excepto la raíz, deben estar siempre completos (con claves) al menos hasta la mitad.
Si un nodo que no sea hoja tiene h claves entonces debe tener h + 1 nodos hijo no nulos.
La raíz: o es hoja o tiene al menos dos hijos.
Todas las hojas están al mismo nivel siempre.
Ítem 2
Según Wikipedia5 son muy usados por los sistemas de archivos modernos para permitir un acceso rápido a
un bloque arbitrario de memoria. Ejemplos de sistemas de archivos que los usan son: HFS+ de Apple, NTFS
de Microsoft y Ext4 de Linux.
En clase lo único que tengo anotado es que se usan para armar los TDA índice, que son un tipo de TDA
diccionario. Los índices se usan en los archivos para lo mismo que en un libro: evitan recorrer el archivo de
punta a punta hasta encontrar la información que se busca.
Aparentemente, según lo que leí, tienen las ventajas de los ABB pero minimizan el acceso a disco (que es
lento).
Ítem 3
Este ejercicio se resuelve en el siguiente link: https://www.cs.usfca.edu/~galles/visualization/BTree.
html. Yo igual voy a anotar los pasos acá:
1. El árbol comienza vacío:
200 - - -
200 250 - -
1 200 250 -
6. Cuando llega el 200 se busca la posición adecuada (que será justo en el medio o en la siguiente, da igual):
200
pero como el nodo ya está lleno entonces hay que laburar un poco más. Como mínimo habría que agregar
una hoja, pero de acuerdo a la definición de árbol B es necesario que todos los nodos (salvo la raíz) tienen
que estar completos hasta la mitad (o más). La forma de resolver esto entonces es agregar dos hojas de la
siguiente forma:
200 - - -
200 - - -
8. El 40:
200 - - -
9. El 50:
200 - - -
200 - - -
6
180 - - -
5 150 2 2 25
ya que te quedan menos nodos y aún satisface la definición de árbol B. En la clase tampoco lo hicimos así y no me avivé de
preguntar por qué...
200 - -
Cuando pasa algo así parece que lo que se hace siempre es “el elemento del medio (60) se ubica en el nivel
superior y se parte el nodo en dos”.
11. Cuando llega el 35 se lo ubica al principio de todo:
60 200 - -
60 200 - -
13. Finalmente llega el 40 que nuevamente encuentra un nodo lleno y hace lo mismo que hizo antes el 60: “el
elemento del medio se ubica en el nivel superior y se parte el nodo en dos”:
40 60 200 -
14. Ahora se comienzan a eliminar elementos. Primero se elimina el 200. Para ello se navega por el árbol hasta
encontrarlo en su primera aparición:
40 60 200 -
A continuación se “sube” el elemento más grande del nodo inferior, es decir el 180:
40 60 180 -
El problema ahora es que nos queda un nodo que no cumple las reglas de los árboles B ya que tiene menos
de la mitad de elementos. Entonces hay que hacer una rotación y el árbol queda así:
40 60 200 -
15. Al eliminar el 300 nuevamente queda un nodo que no satisface las reglas de los árboles B:
por lo tanto habrá que hacer rotaciones hasta que todo quede bonito. La forma en que se resuelve es la
siguiente:
40 60 - -
16. La eliminación del 150 debería ser sencilla: simplemente se lo borra y chau. Sí, lo verifiqué con la página
que puse al principio ,. Queda así:
40 60 - -
17. Al eliminar el 10 nuevamente hay problemas. Creo que lo que se hace es similar a cuando se eliminó el
300: se elimina el nodo primero y se baja el 40 al nodo segundo:
40 60 - -
- 60 - -
60 - - -
60 - - -
Ítem 4
Sea un árbol B de orden o (o sea de o punteros por nodo). Si el mismo tiene altura h y tiene la máxima
cantidad posible de elementos entonces todos los punteros están “ocupados”. Esto implica que tiene oh nodos. A
su vez, cada nodo tiene (o − 1) claves porque el árbol está lleno, por lo tanto la cantidad de claves de un árbol
B satisface
# de claves de árbol B de orden o y altura h ≤ oh (o − 1)
Si nos hubieran preguntado
por la cantidad mínima la cosa sería así: por definición de árbol B los nodos no
pueden tener menos de o−1 elementos (salvo la raíz). La raíz sí puede tener 1 elemento. Entonces, definiendo
2
def o−1
q = 2 , el árbol estaría conformado así:
Finalmente uniendo las dos cosas (no lo pedía la consigna pero soy crack) la cantidad de claves k de un árbol
B de orden o (o sea con o punteros por nodo) y de altura h satisface
h
o−1
2 + 1 − 1 ≤ k ≤ oh (o − 1)
2
Problema 3
Ítem 1
De acuerdo con [2, sec. 6.9] y [3, sec. 6.1] un árbol heap máximo (mínimo) es un árbol binario que satisface
las siguientes propiedades:
El valor de cada nodo es mayor o igual (menor o igual) al valor de cada uno de sus hijos.
El árbol está perfectamente balanceado (salvo quizá en el último nivel) y las hojas del último nivel están
todas lo más a la izquierda posible.
El coste del alta y la baja en el heap es, según los apuntes del campus, O (log n) donde n es la cantidad de
elementos del heap. Esto se debe a la forma en que se realizan estas operaciones:
Problema 4
Ítem 1
De acuerdo con el apunte sobre recorridos de grafos del campus, la detección de ciclos en grafos se puede
realizar con el algoritmo de “DFS de Tarjan”. Según Wikipedia8 el algoritmo corre en tiempo O (|V | + |E|)
donde |E| es el número de aristas (puentes) y |V | el número de vértices (nodos). Un análisis más completo se
puede encontrar en [2, sec. 8.4] que también usa el algoritmo de Tarzan de la siguiente forma:
7 Esto es asumiendo que el heap se implementa en un array y que no hay que hacer un realloc porque nos quedamos sin lugar,
Ítem 2
Un recorrido topológico (topological order según [4, sec. 12.4.1]) es un recorrido que permite ordenar los
vértices de un grafo dirigido y acíclico respetando su precedencia. La mejor forma de explicarlo sin entrar en
detalles del algoritmo es con un ejemplo como el siguiente [4, fig. 12.7]:
El ejemplo clásico que presentan todos los libros es el sistema de correlatividades de materias de la facultad.
Se trata de un grafo dirigido acíclico donde los vértices (nodos) son las materias y las aristas (flechitas) indican
las correlatividades.
Existen dos tipos de recorridos topológicos:
Depth-first o “en profundidad primero” [4, sec. 12.4.2]. Link9
Breadth-first o “en anchura primero” [4, sec. 12.4.3]. Link10
Ítem 3
Dos posibles recorridos en profundidad (depth-first) que comienzan en el nodo A son el rojo y el azul
mostrados a continuación:
E
C
B
J
I
*
D
G
F
En cada uno de los círculos de cada recorrido es cuando se procesa la información de cada nodo. El recorrido
preorder, inorder o postorder de un árbol es el recorrido en profundidad, obsérvese que es completamente
análogo.
Un recorrido en anchura es equivalente a un recorrido por niveles de un árbol (de hecho un recorrido por
niveles de un árbol es un caso particular de recorrido por anchura).
Ítem 4
Supongo que se habla del algoritmo de recorrido en profundidad. En clase lo vimos así:
1 R e c o r r e r en p r o f u n d i d a d ( G ) { // «G» es el grafo .
2 Marcar todos los nodos de G como no v i s i t a d o s;
3 Para cada nodo v de G {
4 i f ( v == no v i s i t a d o)
9 https://www.cs.usfca.edu/~galles/visualization/DFS.html
10 https://www.cs.usfca.edu/~galles/visualization/BFS.html
5 V i s i t a r( v ) ; // Esta f u n c i ó n está d e f i n i d a a
c o n t i n u a c i ó n.
6 }
7 }
8
9 V i s i t a r( v ) {
10 Marcar v como v i s i t a d o;
11 Para cada nodo u a d y a c e n t e a v {
12 i f ( u == no v i s i t a d o) {
13 V i s i t a r( u ) ;
14 }
15 }
16 }
Consigna
Problema 1
1. Defina hashing. Explique para qué se usa.
2. Explique los modos de resolver una colisión.
3. Indique qué implementaría mediante hashing y por qué.
4. Caracterice el hashing abierto y cerrado.
5. ¿Qué puede decir acerca del tiempo medio esperado hasta recuperar un registro en el hashing con enca-
denamiento?
Problema 2
1. Defina árbol B. Indique usos de la estructura.
2. Considere un árbol B 2-3 y muestre gráficamente la evolución del mismo cuando
Problema 3
1. Diseñe un algoritmo que permita probar si un grafo dirigido es o no acíclico.
2. ¿Cuál es el coste del algoritmo? Explique por qué.
Problema 4
Se quiere mantener las ciudades A, B, C, D, E y F conectadas al menor costo posible. Describa un algoritmo
que permita determinar cómo debe ser el grafo que logre este objetivo, desarróllelo y determine su complejidad.
Resolución
Problema 1
Ver la respuesta del ejercicio 1 del “final 1” en la página 21.
Problema 2
Ver la respuesta del ejercicio 2 del final del 10/02/2015 en la página 10.
Problema 3
Pregunta 1
Ver la respuesta del ejercicio 4 ítem 1 del final del 10/02/2015 en la página 15.
Pregunta 2
El coste del algoritmo es O (|V | + |E|). Esto se debe a que el algoritmo visita todos los nodos una única vez
cada uno y en cada visita recorre las aristas aún no recorridas, lo cual hace que finalmente haya recorrido todas
las aristas.
Problema 4
El problema de las ciudades se puede representar mediante un grafo en el que cada ciudad es un nodo y
las aristas tienen los costos. Lo que queremos encontrar es el árbol de expansión de coste mínimo (o mínimum
spanning tree). Según el apunte del campus “GrafosApunte3_7504.pdf” los algoritmos que permiten encontrar
este bello árbol son, entre otros, el algoritmo de Prim y el de Kruskal.
El siguiente video de Youtube lo explica en 2 minutos, está bueno: https://www.youtube.com/watch?
v=cplfcGZmX7I. Es un algoritmo que usa la estrategia greedy.
En [3, pag. 634] habla sobre este algoritmo. Dice que es greedy y también lo detalla en procedimiento. El
tiempo de ejecución del mismo depende de cómo se lo implementa. Parece que (ver en el libro) necesita una
cola de prioridades Q para funcionar y en función de cómo se la implementa el tiempo de ejecución es
(
O (|E| log |V |) Usando binary min-heap
T (n)Prim ∼
O (|E| + |V | log |V |) Usando Fibonacci heap
4. Final 1
Este es uno de los finales que andan dado vueltas sin fecha.
Consigna
Problema 1
1. Defina hashing, explique para qué se usa.
2. Explique diferentes modos de resolver una colisión.
3. Indique qué implementaría mediante hashing y por qué.
4. Caracterice el hashing abierto y cerrado.
5. ¿Qué puede decir acerca del tiempo medio esperado para hasta recuperar un registro en el hashing con
encadenamiento?.
Problema 2
1. Defina árbol B. Indique usos de la estructura.
2. Considere un árbol B de orden 5 y muestre gráficamente la evolución del mismo, inicialmente vacío, cuando
a) ingresan las claves 20, 25, 15, 18, 27, 30, 49, 59, 67, 35, 13 y 48.
b) Luego se borran las claves 15, 30, 49, 13, 48.
Problema 3
1. Caracterice la estrategia greedy, indique cuándo la aplicaría (condiciones a verificarse en el problema).
2. Describa un problema de grafos que pueda resolverse aplicando estrategia greedy y el algoritmo que lo
resuelva aplicando dicha estrategia. Indique el coste del algoritmo especificando las estructuras que usaría
en la implementación del mismo.
Resolución
Problema 1
Pregunta 1
Hashing es una técnica utilizada para implementar tablas hash. Estas tablas son estructuras de datos muy
eficientes para implementar diccionarios que sólo soporten las operaciones de insertar, buscar y eliminar [3,
cap. 11]. La característica principal de las tablas hash es que el tiempo de búsqueda es
(
O (1) Average case
Tbúsqueda en tabla hash (n) ∼
Θ (n) Worst case
Las tablas hash son efectivas cuando el número de elementos que almacenan es pequeño en comparación al
número total de posibles claves. En [3, sec. 11.2] definen las cosas en base al siguiente esquema:
Hash table
k6 0 = h(k4)
k1 U Hash function 1 = h(k7)
k8
2 -
h(k)=memmory position for k 3 = h(ki)
k9 ki k2
K 4 -
k7 5 -
k4 ... ...
K=Actually stored keys
m-1 = h(k2)
De todo el universo de claves posibles U solo almacenamos un subconjunto K (cuanto más |K| ≪ |U | más
efectivo es el hashing) en un arreglo y la posición de memoria para la clave ki se calcula utilizando la función
hash haciendo
posición de ki = h (ki )
La función hash suele ser de la forma
h (k) = H (k) mód m
de modo tal que el valor de h (k) sea siempre un número contenido entre 0 y m − 1 (que es el tamaño de la
tabla). La función H (k) es, en principio, cualquier función.
Debido a que |U | > m con m el tamaño de la tabla hash entonces necesariamente habrán dos (o más) claves
ki y kj tales que h (ki ) = h (kj ), es decir que habrán dos (o más) claves que tendrán asignada la misma posición
de memoria. Esto se llama colisión y es algo que se quiere evitar, mas es imposible.
Pueden haber funciones hash malas o buenas. Lo que se busca es que la función hash distribuya las claves
de la manera más uniforme posible pero no siempre es fácil de lograr. El siguiente dibujito ilustra esto:
collision
La función hash ideal será aquella que produzca la cantidad mínima de colisiones pero esto no es fácil de
lograr. La elección de la función hash dependerá de las claves que se quiera manejar.
Pregunta 2
Esto lo vimos en 15 minutos la última clase. Vimos los siguientes métodos:
1. Direccionamiento abierto (o hashing cerrado).
a) Sondeo lineal. Si la función hash nos manda a una posición que ya está ocupada entonces avanzamos
una posición y nos fijamos si está libre, si sí ya está y si no entonces avanzamos otra posición y
así. Este método produce lo que se conoce como “clusterización primaria” o “amontonamiento de
sinónimos”, que es algo que la función hash trata de evitar. Es por ello que no es un buen método
mas es simple.
b) Sondeo cuadrático. Es igual al sondeo lineal pero aumenta en forma cuadrática. Es decir que si la
posición p de la tabla está ocupada entonces se prueba en la p + 1, en la p + 2, en la p + 9, . . . , p + i2
y así sucesivamente. La idea es que los sinónimos queden dispersos a lo largo de la tabla.
c) Usar una segunda función hash. Si la posición dada por h1 (k) está ocupada entonces se intenta con
h1 (k)+h2 (k), si está ocupada entonces h1 (k)+2h2 (k) y así sucesivamente en la forma h1 (k)+ih2 (k).
d) Generalización de direccionamiento abierto. Esto no lo vimos en clase, lo vi en algún video de Youtube.
La función hash es, como dije más arriba, de la forma h (k) = H (k) mód m . Lo que se puede hacer
es modificarla así:
h (k) = (H (k) + f (i)) mód m
donde H (k) es la misma función de antes, i es un índice que empieza en 0 y lo incrementamos
cada vez que hay una colisión y f es una función cualquiera. Entonces todos los casos anteriores de
direccionamiento abierto son casos particulares de este.
2. Encadenamiento (o hashing abierto). Esto está explicado en [3, sec. 11.2]. Lo que se hace es simplemente
armar una lista enlazada en cada posición de la tabla hash entonces si hay una colisión se añaden las
claves en el mismo lugar y listo, problema resuelto. A continuación dejo una imagen del libro donde se
detalla la estructura
Pregunta 3
Las tablas hash son buenas cuando se requiere un acceso rápido a los datos. La verdad que no encuentro
una respuesta certera a esta pregunta en las fuentes típicas (libros, apuntes, internet), así que recurriré a mi
sentido común. Yo implementaría el TDA Conjunto usando una tabla hash cuando sé que mi conjunto no va a
tener “muchas claves” contenidas, es decir cuando veo que |U | ≫ |K|. De esta forma las operaciones agregar,
sacar y está? tendrían tiempo O (1) en el caso promedio.
Si mi aplicación requiere operaciones como encontrar máximo (o mínimo) o cosas así entonces la tabla hash
no sirve ya que es lineal, es como tener todo tirado en un arreglo desordenado [5, chap. 20].
Pregunta 4
Como se dijo antes cada tipo de hashing es
Hashing cerrado: Las colisiones se resuelven por sondeo.
Hashing abierto: Las colisiones se resuelven armando listas enlazadas.
Hashing abierto. Esto lo basé en lo que dice [3, sec. 11.2]. Para el peor caso (que nunca se va a dar, pero
bueno... Es el peor) el tiempo de búsqueda de un elemento es O (n) ya que podría pasar que la función hash es
pésima y todos los elementos colisionan en el mismo lugar, dando origen a una lista de longitud n.
Para el caso promedio tenemos los teoremas [3, teoremas 11.1 y 11.2] que dicen que el tiempo de búsqueda
en una tabla hash en la que las colisiones se resuelven por encadenamiento (o sea hashing abierto) es Θ (1 + α)
donde α es el load factor definido como
def n
α= → Load factor
m
con n la cantidad de elementos almacenados en la tabla y m el tamaño de la misma.
Para el mejor caso, obviamente, el elemento es encontrado de una así que es Ω (n).
Pregunta 5
Creí que era la pregunta anterior. En el hashing con encadenamiento (o abierto) el tiempo medio hasta
encontrar un elemento es Θ (1 + α) donde α = mn
es el load factor.
Problema 2
Ver respuesta al problema 2 del final del 10/02/2015, en página 10.
Problema 3
Pregunta 1 (incompleta)
La estrategia greedy (del inglés codicioso, ávido, goloso) consiste en dividir a un problema en subproblemas
elementales y resolver cada uno de estos problemas por separado, tomando como solución para cada problema
la que parece ser mejor en forma aislada.
Faltan las condiciones del problema para que se pueda resolver por estrategia greedy.
Pregunta 2
Según el apunte “GrafosApunte3_7504.pdf” del campus el algoritmo de Dijkstra para el cálculo de los
caminos mínimos con un mismo origen en un grafo ponderado (con ponderaciones no negativas) utiliza la
estrategia greedy. La mejor forma de aprender el algoritmo es en Youtube, por ejemplo acá: https://www.
youtube.com/watch?v=zXfDYaahsNA. También se puede ver el algoritmo en funcionamiento acá: https://
www.cs.usfca.edu/~galles/visualization/Dijkstra.html.
11 Las fórmulas de sondeo lineal se pueden encontrar en [5, theorem 20.3] mientras que las de doble hashing en [3, theorems 11.6
and 11.8].
5. Final 2
Este es uno de los finales que andan dado vueltas sin fecha.
Consigna
Problema 1
1. Determine el valor de los caminos mínimos de este grafo tomando como vértice de partida 1. Describa el
algoritmo usado.
2. Indique qué estructuras y de qué modo se podrían usar para almacenar datos y lograr un algoritmo más
eficiente que O n2 . Explique cuál es la estrategia usada y qué elementos la caracterizan.
Acá había un cartel indicando que debería haber una imagen del
grafo, pero la misma no estaba.
Problema 2
1. Defina hashing, indique qué implementaría mediante hashing y por qué.
Problema 3
1. Defina árbol B. Indique usos de la estructura.
2. Considere un árbol B de 4 claves y 5 vías y muestre gráficamente la evolución del mismo, inicialmente
vacío, cuando
a) ingresan las claves 23, 20, 25, 10, 15, 27, 38, 49, 59, 67, 65, 45, 5, 3, 6.
b) Luego se borran las claves 25, 15, 49, 59.
Problema 4
1. Explique qué es un trie y para qué se utiliza.
2. Muestre gráficamente dos implementaciones distintas de trie en las que se hayan almacenado estos datos:
abbc, baac, baa, baab, abbb, a.
3. Explique los pasos de la baja en un trie en una de las implementaciones planteadas (no se pide codificar).
4. ¿Qué elementos determinan la complejidad temporal del alta y la baja de las claves en un trie?
Resolución
Problema 1
Pregunta 1
Ya que no está el dibujo del grafo lo que voy a hacer es asumir que es el siguiente:
INF,-1
Los valores en rojo al lado de cada nodo son primero el costo y segundo el “from” del cuadro que indica desde
qué nodo llegamos. Los pongo únicamente por claridad, con el cuadro solo ya alcanza. Primero se le coloca a
todos los nodos un costo infinito (o un número suficientemente grande que sabremos que jamás se alcanzará,
por ejemplo 1000 ya alcanzaría acá) y se los marca a todos como “no visitados”.
A continuación vamos al nodo que tiene el menor costo, en este caso el 0 ya que es por el cual vamos a
arrancar. Lo marcamos como visitado:
0,-1
INF,-1
Lo que sigue es fijarse quiénes son los vecinos y comparar su costo con ∞ (que es el costo actual). Como el
costo será menor, lo anotaremos. Esto es:
0,-1
INF,-1
Ahora el algoritmo ya empieza a repetirse: visitar aquel nodo que no haya sido visitado que tenga el menor
costo y hacer lo mismo. En este caso es el 1. Lo visitamos y lo marcamos como ya visitado:
0,-1
INF,-1
Ahora hay que registrar los costos que tienen todos sus vecinos realizando la siguiente operación:
donde cost(i) es el costo actual que tiene cada nodo (ver cuadro) y edge(1,i) es el costo de recorrer el
camino que une al nodo 1 con el i y from(i) es el valor de la columna from del cuadro. En dibujos sería así:
0,-1
INF,-1
0,-1
INF,-1
Ahora, como ya no quedan vecinos no visitados en el 5 entonces terminamos. Repetimos lo mismo que antes:
de los nodos no visitados vamos a aquel con menor costo, ahora es el 2:
0,-1
INF,-1
A continuación verificamos si hay que actualizar el peso hacia los vecinos no visitados. En este caso el único
es el 4 y resulta que no hay que actualizarlo:
0,-1
INF,-1
Lo que falta es repetir lo mismo hasta terminar. Primero vamos a aquel nodo (de los no visitados) que tiene
el menor costo, ahora será el 4:
0,-1
INF,-1
0,-1
INF,-1
Ahora cuando nos volvemos a preguntar ¿cuál de los no visitados tiene el menor costo? lo que ocurre es que
los que quedan son infinitos, entonces no los vamos a visitar (justamente porque están desconectados). Entonces
el algoritmo termina acá.
Ahora si queremos el menor camino al nodo i lo que hacemos es ir a dicho nodo (en la tabla) y ver desde
dónde vinimos. A continuación vamos a aquel nodo desde el cual vinimos y repetimos en forma recursiva hasta
llegar al 0 y habremos obtenido el camino mínimo al nodo i.
Pregunta 2
2
Según Wikipedia12 el algoritmo de Dijkstra es O |V | donde |V | es la cantidad de nodos (del inglés
vertices). Hay una modificación del algoritmo (Wikipedia) que utiliza una implementación basada en una cola de
prioridades (min-priority queue) implementada con un Fibonacci heap que corre en tiempo O (|E| + |V | log |V |)
donde |E| es la cantidad de aristas o caminitos (del inglés edges).
Problema 2
Ver la respuesta al problema 1 del “Final 1” en la página 21.
Problema 3
Ver la respuesta al problema 2 del final del 10/02/2015 en la página 10.
Problema 4
Preguntas 1 y 2
Ver respuesta al problema 1 del final del 10/02/2015 en la página 7.
Pregunta 3
Las dos implementaciones que conozco de trie son las que presenté en la página 7 en la respuesta de la
pregunta 1 ítem 2. Éstas son:
Implementación con arreglo de punteros.
Implementación con listas.
El procedimiento para dar de baja una clave se puede hacer así:
1. Buscar la clave en el árbol.
2. Si la encontramos entonces la sacamos, si no la encontramos no hacemos nada.
La parte “complicada” es buscar la clave en el árbol, el paso 2 es simplemente un if que borra un puntero. La
búsqueda en un trie la describí en la respuesta de la pregunta 1 ítem 4 del final del 10/02/2015, en la página 8.
12 https://en.wikipedia.org/wiki/Dijkstra’s_algorithm.
6. Final 3
Este es uno de los finales que andan dado vueltas sin fecha.
Consigna
Problema 1
Defina O, Ω y Θ. Ejemplifique.
Problema 2
1. Explique en qué consiste la estrategia “divide y vencerás”.
2. ¿Cómo se puede determinar el umbral para continuar o no la partición en subproblemas?
3. ¿Siempre es más eficiente un algoritmo que usa esta estrategia?
4. ¿Qué relación hay (si existe tal relación) entre esta estrategia y la modularización?
5. Mencione algoritmos que la apliquen.
Problema 3
1. Defina árbol multivías y árbol B.
2. Considere un árbol B 2-3 inicialmente vacío. Muestre gráficamente su evolución cuando
Problema 4
1. ¿Qué es un TDA Conjunto? ¿Qué es un TDA Diccionario? ¿Cuáles son las operaciones básicas de cada
uno? ¿Para qué puede utilizarse cada uno?.
2. Indique tres implementaciones distintas para un diccionario. De las que ha elegido, ¿cuál le parece la más
conveniente? ¿Por qué?.
Resolución
Problema 1
Las definiciones de cada uno de estos conjuntos de funciones son, de acuerdo a como lo vimos en la clase
práctica,
def
O (f (n)) = {g (n) : ∃n0 , c > 0 tq 0 ≤ g (n) ≤ cf (n) ∀n > n0 }
def
Ω (f (n)) = {g (n) : ∃n0 , c > 0 tq 0 ≤ cf (n) ≤ g (n) ∀n > n0 }
def
Θ (f (n)) = {g (n) : ∃n0 , c1 , c2 > 0 tq 0 ≤ c1 f (n) ≤ g (n) ≤ c2 f (n) ∀n > n0 }
Problema 2
Pregunta 1
La estrategia “divide and conquer” consiste en tomar un problema y partirlo en n subproblemas de naturaleza
idéntica en forma recursiva, hasta llegar a un caso base en el que la solución del problema es trivial.
Pregunta 2
No sé exactamente a qué se refiere. Supongo que habla de cómo se sabe si se llegó al caso base... Bueno, uno
define una condición de caso base y en cada llamada recursiva la testea con un if.
Pregunta 3
En el apunte13 del campus llamado “dvide and conquer.pdf” dice que no, que los algoritmos que se obtienen
utilizando la técnica de dividir y conquistar son recursivos y son muy buenos, pero se los puede mejorar llevándo-
los a su versión iterativa. Básicamente dice que lo bueno de método de dividir y conquistar es que generalmente
quedan algoritmos que se pueden pasar a la versión iterativa de forma muy fácil mejorando así su performance
(mejora el tiempo de ejecución y, si el algoritmo recursivo se apila en el stack, también la complejidad espacial),
pero atentando en contra de la legibilidad del código y el mantenimiento.
Problema 3
Preguntas 1 y 2
Ver respuesta al problema 2 del fina del 10/02/2015 en la página 10.
Pregunta 3
El algoritmo de alta se puede expresar de la siguiente manera:
1. Ubicar el nodo en el que debería estar el nuevo elemento.
2. Si el nodo tiene al menos un lugar libre hacer a), si no hacer b):
a) Colocar el nuevo elemento en la posición correspondiente dentro del nodo y acomodar todos los
demás, junto con los punteros. Listo, terminó el alta.
b) Si el nodo está lleno entonces hacer “el elemento del medio se coloca en el nivel superior y se parte
el nodo en dos”. Esto significa: armar un arreglo ordenado con todos los elementos el nodo más
el elemento nuevo. Buscar el elemento “del medio” y mandarlo al nivel superior. La mitad inferior
mandarla al nodo existente y la mitad superior a un nodo nuevo ubicado en el mismo nivel.
13 En este caso en realidad creo que es un capítulo robado de un libro, pero no dice cuál.
1) Si el “nodo superior” también está lleno entonces repetir el paso b) en forma recursiva hasta
hasta que todo quede ordenado. En el peor de los casos se llega a la raíz.
Para entender mejor todo esto puede ser conveniente mirar la respuesta que puse en el problema 2 ítem 3 del
final del 10/02/2015 en la página 10.
Problema 4
Pregunta 1 (falta completar)
Según vimos en clase:
TDA Conjunto Es un TDA contenedor14 que almacena un grupo de datos sin que esté permitido que haya
repeticiones. La idea es la misma que la del conjunto matemático.
Sus operaciones básicas son
Crear.
Destruir.
Alta de un elemento.
Baja de un elemento.
Está (o pertenece).
TDA Diccionario Es un TDA contenedor de pares ordenados (clave, valor) donde las claves no pueden estar
repetidas.
Pregunta 2
Implementaciones que se me ocurren para un diccionario son
1. Array. Esta es la implementación más básica de todas, un arreglo que contenga en cada posición una clave
y su valor asociado. No es una implementación eficiente desde ningún punto de vista.
2. Tabla hash. Las tablas hash son buenas cuando la cantidad de claves almacenadas en el diccionario es
mucho menor al conjunto total de claves posibles ya que permiten tiempos de búsqueda, en promedio,
O (1).
3. Trie. Los tries son ideales para implementar diccionarios, cuando las claves lo permiten (por ejemplo de
palabras donde las claves son cadenas).
Yo elegiría una tabla hash o un trie dependiendo de la aplicación. Si quisiera implementar el diccionario de la
Real Academia Española usaría un trie ya que es una estructura más flexible y que garantiza tiempos de acceso
constantes (al menos independientes de n, la cantidad de palabras). Además en esta aplicación la cantidad de
palabras almacenadas en el diccionario sería (idealmente) el 100 % de las claves posibles lo cual haría que una
tabla hash comience a perder eficiencia.
Referencias
[1] Algorithms, Sedgewick & Wayne.
[2] Data structures and algorithms in C++, Drozdek.
[3] Introduction to algorithms, Cormen.
[4] Data structures and program design in C++, Kruse.
[5] Data Structures and Problem Solving with C++, Weiss.