1. Árboles y grafos
Vladimir Támara Patiño.
2004. Dominio público. Sin garantías.
Agradecemos que como fuente se cite: http://structio.sourceforge.net/guias/arbgraf/
Este escrito se dedican a nuestro Padre Creador, a su santo Espíritu y a Jesús su Hijo y maestro nuestro.
Capítulo 1 Introducción y definiciones
l logro: Define grafo, bosque y árbol
l logro: Emplea árboles binarios
1.1 Definiciones
Indicadores de logro:
l indicador: Define diversos tipos de grafo
l indicador: Define árbol
l indicador: Implementa y emplea un TAD para árboles binarios
En este contexto árboles y grafos se refiere a estructuras de datos que permiten organizar y mantener información en un computador. Esta forma se inspira una forma de
organizar información con lápiz y papel usando nodos y flechas entre los nodos (a esas flechas también se les llama arcos, a los nodos también se les llama vértices). Los
grafos y árboles en papel son apropiados por ejemplo para capturar sólo una parte de la información de objetos, situaciones y otros tipos de información (i.e son
apropiados para abstraer).
En un computador además de permitir organizar información, resultan estructuras útiles para resolver ciertos tipos de problema (por ejemplo pueden emplearse árboles
AVL para mantener información ordenada de forma eficiente).
Para jugar, entender y emplear mejor grafos (y árboles) varias personas (e.g Euler) han propuesto definiciones; a partir de estas definiciones y con ayuda de
razonamientos lógicos han demostrado propiedades. Un mínimo de definiciones y de propiedades de grafos y árboles (con estilo inspirado en [6]) se presenta a
continuación.
Note que para ver mejor esta página puede requerir configurar su navegador para que presente símbolos especiales, que se esperan con el tipo de letra de symbol . En el
caso del navegador Mozilla, y suponiendo que en su sistema ya está instalado y configurado para Mozilla el tipo de letra para símbolos marque el botón de chequeo que
permite que el documento use otras fuentes, en el menú apariencia del diálogo de preferencias (elemento del menú editar).
1.1.1 Teoría
Grafos
Un grafo G es una pareja G=(V,A), donde V es un conjunto finito (i.e vértices) y A es un subconjunto del conjunto de parejas no ordenadas de V (i.e arcos). Por
ejemplo G=({a,b,c},{{a,c},{c,b}}), que se representa gráficamente en la figura 1.1.
Figure 1.1: Ejemplo de grafo
Dado un grafo G con V(G) denotamos el conjunto de vértices y con A(G) denotamos el conjunto de arcos. Decimos que un arco {x,y }Î A(G) une los vértices x e y ; así
que x,y denota el mismo arco que {y ,x}. Si {x,y } es un arco, decimos que x e y son extremos del arco, también decimos que x es adyacente o vecino de y .
Un grafo G'=(V',A') es subgrafo de G=(V,A) si V'Ì V y AÌ A'. En tal caso escribimos G'Ì G.
Si G' contiene todos los arcos de G que unen dos vértices de V' decimos que G' es un subgrafo inducido por V' y se denota por G[V'].
El orden de un grafo G es el número de vértices, también se denota por G 1. El tamaño de G es el número de arcos, y se denota por a(G).
Decimos que dos grafos son isomorfos si hay una correspondencia entre sus conjuntos de vértices que preserva la adyacencia. Así que G=(V,E) es isomorfo a G'=
(V',E') si hay una biyección j: V V' tal que x,y Î E si y solo si j(x)j(y )Î E'. Entonces dos grafos isomorfos tienen el mismo orden y el mismo tamaño.
El tamaño de un grafo de orden n es al menos 0 y a lo sumo ( 2n). Un grafo de orden n y tamaño ( 2n) se llama un ngrafo completo 2 y se denota por Kn; un ngrafo
2. vació En tiene orden n y ningún vértice. En todo Kn todo par de vértices son adyacentes, mientras que en En ningún par de vértices son adyacentes.
El conjunto de vértices adyacentes a un vértice xÎ G, se denota por G(x). El grado de x es g(x)= G(x). El grado mínimo de los vértices de un grafo se denota por d
(G), mientras que el grado máximo se denota por D(G). Un vértice de grado 0 se dice que es un vértice aislado. Si d(G)=D(G)=k, es decir todo vértice es de grado k
entonces se dice que G es kregular o regular de grado k. Un grafo es regular si es kregular para algún k.
Dado que V(G)={x 1,x 2∙∙∙ x n}, como todo arco tiene dos vértices extremos, la suma de los grados es exactamente dos veces el número de vértices:
n
å i=1
g(x i)=2e(G)
Esta observación se llama también el lema de los apretones de manos, pues expresa que en una fiesta el número total de manos apretadas es par. Esto también
establece que el número de vértices de grado impar es par.
Un camino W en un grafo G es una secuencia de los vértices de G, (x 0,x 1,∙∙∙,x l) tal que {x i,x i+1 }Î A(G) para 0£ i<l. A x 0 y a x l los llamamos vértices extremos del
camino. Un camino en el que todos los vértices son diferentes se denomina sendero. Un sendero en el que los vértices extremos son iguales se denomina un circuito o si
l>2 también se denomina un ciclo, y puede denotarse por x 1x 2∙∙∙ x l.
Con Cl denotamos un ciclo de longitud l. Llamamos a C3 un triángulo, a C4 un cuadrilátero, a C5 un pentágono, etc.
Dados vértices x, y su distancia d(x,y ) es la longitud mínima de un camino de x a y .
Un grafo es conexo si para todo par x, y de vértices diferentes hay un camino de x a y . Note que un grafo conexo de orden al menos 2 no puede tener vértices aislados.
Con la definición que empleamos un grafo no contiene lazos, es decir un arco que una vértice consigo mismo; ni contiene arcos múltiples es decir que varios arcos unan
los mismos dos vértices. En un multigrafo se permiten lazos, arcos múltiples y lazos múltiples.
Si los arcos son parejas ordenadas de vértices entonces tenemos la noción de grafo dirigido o multigrafo dirigido. Un pareja ordenada (a,b) se llama un arco dirigido
de a hacia b, o decimos que el arco comienza en a y termina en b. En un grafo dirigido dado un vértice x, llamamos grado de entrada de x ge(x) a la cantidad de arcos
dirigidos que comienzan en x, mientras que el grado de salida de x gs(x) es la cantidad de arcos dirigidos que terminan en x. En un grafo dirigido una secuencia de
vértices (x 0,x 1,∙∙∙,x l) es un camino si existen arcos dirigidos (x i,x i+1 ) para 0£ i<l.
Llamamos vista no dirigida de un grafo dirigido G=(V,A) al grafo no dirigido G'=(V,A') en el que tomamos los arcos dirigidos de G como arcos no dirigidos, i.e: A'=
{{x,y }:(x,y )Î A}.
Un grafo dirigido decorado G=(V,A,d) consta de un grafo (V,A) y una función d:A E que decora los arcos con elementos de cierto conjunto E de etiquetas (e.g E
podría ser conjunto de números, en el caso de grafos cuyos arcos se decoran con cantidades por ejemplo para representar distancia).
Árboles
Un grafo sin ciclos es un bosque o un grafo acíclico; mientras que un árbol es un bosque conexo.
TAD Árbol Binario
Llamamos árbol binario a un grafo dirigido decorado con etiquetas E={ Derecho, Izquierdo} que cumple:
l su vista no dirigida es un árbol
l todo vértice tiene grado de salida a lo sumo 2 y grado de entrada a lo sumo 1.
l todo vértice adyacente a un vértice inicial x, está marcado bien como hijo izquierdo o bien como hijo derecho.
Un árbol binario tiene un vértice con grado de entrada 0 al que llamamos raíz. A los vértices con grado de salida 0 los llamamos hojas.
Para representar árboles binarios en un programa puede emplearse un TAD3
l para un árbol vacío ABvacio y
l para un árbol con un posible hijo derecho y/o un posible hijo izquierdo ABcons
y con selectoras:
l izq que retorna el hijo izquierdo de un vértice,
l der que retorna el hijo derecho,
l raiz que retorna la identificación del vértice raíz del árbol.
l esVacio que decide si un árbol es vacío.
1.1.2 Lecturas recomendadas
Las definiciones de grafos son adaptadas de [6], mientras que el TAD Árbol Binario es adaptado de [11].
3. 1.1.3 Ejercicios para afianzar teoría
1. De un ejemplo de un grafo de orden 4 y tamaño 3 y dibuje una representación gráfica.
2. En el grafo presentado al comienzo de esta guía ¿cuáles son los vértices adyacentes a c y cuales a b ?
3. ¿Cual es el subgrafo inducido por V'={a} en el grafo de ejemplo?
4. Los grafos G=({a,b,c},{{a,b},{a,c}}) y G'=({1,2,3},{{2,3},{3,1}}) son isomorfos, ¿Cual es la biyección (o correspondencia) entre sus vértices que preserva
adyacencia?
5. Presente dos grafos isomorfos de orden 4 y tamaño 3, junto con la biyección explicita.
6. De un ejemplo de un grafo de orden 4, conexo, con al menos un ciclo de orden 3.
7. De un ejemplo de un grafo 3regular. Es su ejemplo un grafo regular?
1.1.4 Ejercicios de programación
1. Implemente un TAD Árbol Binario y pruebe cada función con casos de diversa complejidad.
2. Emplee el TAD que implementó en las siguientes funciones:
¡ orden que retorna el orden de un árbol
¡ tamaño que retorna el tamaño
¡ distancia que retorne la distancia entre dos vértices del árbol
¡ haySendero que decide si hay o no sendero entre dos vértices.
3. Proponga un TAD grafo e impleméntelo.
4. Usando su TAD escriba la función distancia que determine la distancia entre dos vértices de un grafo.
5. Proponga un TAD grafo dirigido.
1
Note que la cardinalidad de un conjunto X, se denota por X, así que G= V(G).
2
Note que para determinar el tamaño de un grafo completo puede pensarse como formar todas las posibles parejas no ordenadas de vértices (hay ( 2n) posibles) o
pueden tratarse de contar sin repetir: del primer vértice salen n1 arcos, del segundo sin repetir serían n2, del tercero sin repetir n3 y así sucesivamente, del
último vértice serían 0, de forma que el total de arcos sería 1+2+∙∙∙+n+(n1)=å n1i que a su vez es (n1)(n) /2 que es igual a ( n).
i=1 2
3
TAD es abreviatura de Tipo Abstracto de Datos. Un tipo abstracto de datos encapsula un tipo con funciones al menos para construir elementos del tipo (i.e
constructoras) y funciones para analizar o extraer información (i.e selectoras).
Capítulo 2 Árboles
l logro: Calcula complejidad de funciones
l logro: Emplea árboles binarios y narios para representar información y realizar operaciones eficientes
l logro: Emplea herramientas y TADs eficientes
2.1 Árboles de búsqueda
Indicadores de logro:
l indicador: Emplea un árbol binario para realizar búsquedas
l indicador: Emplea un árbol binario balanceado
l indicador: Emplea graphviz para visualizar grafos
2.1.1 Teoría
Un problema importante en informática es determinar si un elemento e pertenece a una secuencia (e1, e2,∙ en) o a un conjunto {e1,∙∙∙ en}. Aunque la solución más simple
(examinar uno a uno los elementos de la secuencia o del conjunto) puede ser suficiente para algunos propósitos, cuando se requiere eficiencia deben emplearse
estructuras de datos apropiadas.
En esta sección se estudian estructuras basadas en árboles que permiten efectuar búsquedas eficientemente.
Para medir eficiencia suelen emplearse dos métricas: espacio y tiempo. Ambas suelen medirse de forma asintótica para el peor de los casos. Por ejemplo con el algoritmo
de búsqueda recién mencionado en el peor de los casos (si el elemento no está), se requieren n comparaciones cuando el tamaño del problema es n, se dice entonces que
la complejidad temporal de ese algoritmo es O(n) (orden de n), como no se requiere espacio adicional para implementarlo su complejidad espacial es O(1).
4. Entonces al decir que la complejidad temporal de un algoritmo es O( (n)) (siendo f (n) una función en la variable n), decimos que dada una entrada de tamaño n, el tiempo
f
f
que requiere el algoritmo para dar una respuesta es proporcional a f (n). Al referirnos a complejidad espacial del orden O( (n)) expresamos que la cantidad de espacio (i.e
memoria) que un algoritmo requiere para completarse en el pero de los casos cuando procesa una entrada de tamaño n es proporcional a f (n).
Recorridos y otras operaciones con árboles binarios
Un árbol binario puede recorrerse de diversas formas, hay 3 de estas formas que se presentan a continuación junto como implementaciones en Ocaml que usan el
siguiente TAD:1
type 'a arbin = ABvacio | ABcons of 'a arbin * 'a * 'a arbin;;
let izq= function
| ABvacio > failwith ("vacio")
| ABcons (i,_,_) > i
;;
let der=function
| ABvacio > failwith ("vacio")
| ABcons (_,_,d) > d
;;
let raiz= function
| ABvacio > failwith ("vacio")
| ABcons (_,r,_) > r
;;
l Preorden. Primero información del vértice, luego subárbol izquierdo y luego subárbol derecho.
let rec preorden a=if a=ABvacio then
[]
else
(raiz a)::(preorden (izq a))@(preorden (der a))
;;
l Inorden. Primero subárbol izquierdo, luego vértice y finalmente subárbol derecho.
let rec inorden a=if a=ABvacio then
[]
else
(inorden (izq a))@[raiz a]@(inorden (der a))
;;
l Postorden.
let rec postorden a=if a=ABvacio then
[]
else
(postorden (izq a))@(postorden (der a))@[raiz a]
;;
Note que los nombres de las funciones recuerdan el sitio donde se ubica la raíz.
Llamamos altura de un árbol a la cantidad de vértices del sendero más largo que exista en el árbol (téngase en cuenta que un árbol aquí se ha definido como un grafo
dirigido, así que el sendero más largo debe partir de la raíz y terminar en una hoja). Para calcularla puede emplearse la siguiente función:
let rec altura = function
| ABvacio > 0
| ABcons(i,_,d) >
1 + (max (altura i) (altura d))
;;
Búsqueda en árboles binarios ordenados
Buscar un elemento en un árbol binario puede lograrse con más eficiencia si:
l La información que se mantiene en sus vértices2 cuenta con un criterio de orden (i.e con un orden total). Por ejemplo si se trata de enteros con las relaciones de
orden típicas entre enteros o en el caso de cadenas empleando orden lexicográfico.
l Se mantiene un invariante que permita mantener la información ordenada, por ejemplo que todo hijo izquierdo de todo vértice tenga información menor que la del
vértice y que todo hijo derecho tenga información mayor o igual.
En tal caso se habla de un árbol binario ordenado.
5. Figure 2.1: Ejemplo de árbol binario ordenado
Note que el árbol presentado en la figurar 2.1 es un árbol binario ordenado. Por ejemplo para buscar el 12 se requieren 3 comparaciones. Note además que un recorrido
en inorden da la secuencia de elementos ordenada.
La implementación de este tipo en Ocaml puede hacerse con base en el mismo tipo arbin antes presentado 3, pero ahora debe verificarse el siguiente invariante (no
expresable como parte de la sentencia type ):
open Arbin;;
type 'a arbinord = 'a arbin;;
let rec inv = function
| ABvacio > true
| ABcons(i,v,d) >
if (i<>ABvacio) then
(v>(raiz i)) && (inv i)
else
true
&&
if (d<>ABvacio) then
(v<=(raiz d)) && (inv i)
else
true
;;
El invariante expresado por esArbinord debe ser mantenido por funciones para insertar y eliminar, y puede ser aprovechado para efectuar búsquedas más rápidas para
ciertos árboles.
Árboles AVL: binarios, ordenados y balanceados
Note que un árbol binario ordenado podría tener todos sus elementos en un sendero iniciado en la raíz y terminado en una única hoja, en tal caso buscar el menor
elemento se realizará en tiempo proporcional al orden del árbol.
Para evitar situaciones como esta pueden agregarse condiciones al invariante de árbol binario ordenado, para mantener un árbol mejor distribuido, o en otras palabras un
árbol balanceado:
l No hay vértices repetidos 4
l En todo vértice la altura del subárbol izquierdo (digamos altura(i)) y la altura del árbol derecho (digamos altura(d)) deben cumplir:
altura(i)=altura(d) Ú altura(i)=altura(d)+1 Ú altura(i)+1=altura(d)
Si se logra mantener este invariante toda búsqueda se realizará con complejidad temporal O(log 2 n) que es la complejidad temporal óptima para el problema de
búsqueda.
Para lograr mantenerlo deben implementarse con atención las operaciones para insertar y eliminar elementos. Dado que estas operaciones deben mantener un invariante
que requiere conocer altura de subárbol derecho e izquierdo, puede mantenerse esta información actualizada en los vértices del árbol (lo cual puede disminuir tiempo
requerido para calcular la altura cuando se hace el balanceo, pero aumentar espacio empleado para mantener la información).
6. Figure 2.2: Ejemplo de árbol AVL
Una posibilidad es hacer el balanceo a medida que se inserta o elimina un nodo, otra opción es hacer una inserción o una eliminación como si el árbol fuera sólo un árbol
binario ordenado y después balancear el árbol resultante. Para realizar tal balance (tras haber insertado o eliminado sólo un dato) pueden considerarse casos como los
presentados en la figura 2.3 que permiten generalizar la solución.
Figure 2.3: Ejemplo de casos para balancear
Por ejemplo puede balancearse el primer caso haciendo como una rotación a la derecha para obtener el grafo que tiene en su raíz el 5, como subárbol izquierdo la hoja
con 8 y como subárbol izquierdo la hoja con 4.
2.1.2 Hacía la práctica
Visualizar un grafo (o un árbol) es un problema común, hay varias herramientas que ayudan entre las que se encuentra Graphviz que es multiplataforma, de libre
redistribución y que puede usarse con bastante facilidad.
Graphviz se basa en un formato de texto plano ( .dot ) que especifica un grafo y en un lenguaje que facilita la presentación gráfica de estos ( lefty ).
Entre las herramientas que ofrece este paquete están:
l dot que lee un grafo en formato .dot y genera una gráfica. Por ejemplo una vez generado el archivo grafo1.dot puede generarse una gráfica en formato
PostScript 5 con: dot Tps grafo1.dot > grafo1.ps . Una imagen en formato gif podría generarse de forma análoga usando Tgif para después convertirla
a jpeg 6 por ejemplo con convert grafo1.gif grafo1.png (convert es una herramienta que hace parte del paquete de libre redistribución ImageMagick).
l dotty que es una interfaz gráfica adaptable (con lefty ). Sin hacerle adaptaciones permite visualizar y manipular un grafo. Puede visualizarse el grafo grafo1.dot
con: dotty grafo1.dot .
Por ejemplo el grafo presentado como ejemplo en la guía 1 (ver [ejem1]), se escribió en un archivo ejem1.dot con el siguiente contenido:
7. digraph "ejemplo1" {
graph [rankdir = "LR"];
node [fontsize = "16" shape = "ellipse"];
"a"
"b"
"c"
"a" > "c" [dir="none"]
"c" > "b" [dir="none"]
}
Note que se comienza con digraph seguido del nombre del grafo, después entre corchetes se agregan elementos los cuales pueden ser:
l Vertices: Se declaran entre comillas pueden ir seguidos de atributos (declarados entre paréntesis cuadrados). Por ejemplo puede definirse el estilo y color de
relleno o atributos de las fuentes.
l Arcos: Se definen indicando vértice inicial y vértice final cada uno entre comillas, con los signos > entre ellos. Pueden ir seguidos de atributos como dirección y
estilo de línea.
l Otras especificaciones usadas por ejemplo para controlar el algoritmo empleado por dot para localizar vértices y arcos.
En la implementación del TAD árbol binario que acompaña esta guía se incluye una función dot_of_arbin: string > ('a > string) > 'a arbin que
permite generar un árbol binario en formato .dot .
2.1.3 Lecturas recomendadas
Desde el punto de vista de programación funcional, puede verse el uso de árboles de búsqueda en [5], desde el punto de vista de programación imperativa en [11].
Puede consultase más sobre Graphviz en [2].
2.1.4 Ejercicios para afianzar teoría
1. Usando el tipo presentado al comienzo de esta guía puede escribirse el árbol:
(ABcons(ABcons(ABvacio,3,ABvacio),2,
ABcons(ABcons(ABvacio,3,ABvacio),5,ABvacio)))
Dibuje una representación gráfica de este árbol, escriba las listas que resultan de recorrerlo en preorden, en inorden y en postorden.
2. Algunos lenguajes de programación facilitan la creación de estructuras polimórficas (en algunos contexto llamadas paramétricas o genéricas). Por ejemplo un TAD
Árbol Binario Ordenado en Ocaml podría basarse en el tipo presentado al comienzo de esta guía.
Que podría después emplearse con enteros, cadenas o cualquier otro tipo. Dado que Ocaml es un lenguaje con un recolector de basura, el programador en casos
sencillos no debe preocuparse por liberar memoria, detalle que puede ser difícil en lenguajes sin recolector de basura (y por tanto más eficientes en tiempo de
ejecución) como C o C++. ¿Como podría implementar un árbol polimórfico en C? ¿Cómo puede implementar una función para liberar memoria de una estructura
como la que plantee?
3. Hay diversas formas de implementar eliminación en un árbol binario ordenado, explique (o implemente) por lo menos dos algoritmos para esto.
4. Indique los árboles resultantes tras balancear los casos presentados en la figura (2.3).
5. Escriba en formato .dot el árbol 2.2 y visualicelo con dotty .
2.1.5 Ejercicios de programación
1. Implemente un TAD que representa un conjunto de enteros, empleando una lista. Las operaciones mínimas que debe tener este TAD son:
¡ ce_vacio: > conjent constructora que retorna un conjunto vacio
¡ ce_ad: conjent > int > conjent constructora que agrega un entero a un conjunto.
¡ ce_miembro: conjent > int > bool selectora que decide si un entero pertenece o no a un conjunto.
¡ ce_esVacio: conjent > bool selectora que decide si un conjunto es vació.
¡ ce_cardinalidad: conjent > int selectora que retorna la cantidad de elementos en el conjunto.
¡ ce_inv: conjent > bool que decide si la estructura de datos que recibe es un conjunto de enteros (por ejemplo verificando que no haya elementos
repetidos).
¡ ce_elim: conjent > int > conjent que elimina un elemento de un conjunto.
Fije algunos casos de pruebas de diversa longitud (e.g 0, 10, 100, 10000) y mida el tiempo que tarda la función ce_miembro al buscar un elemento que no está en
el conjunto (Ayuda: Para medir el tiempo que toma la ejecución de un programa digamos prog puede emplear desde la línea de comandos: time prog . Con
este método diferenciará hasta milésimas de segundo).
¿Cual es la complejidad de la función de búsqueda?
2. Implemente el TAD del ejercicio anterior pero empleando una árbol binario ordenado (Ayuda: puede emplear la implementación disponible con estas guías en
http://structio.sourceforge.net/guias/arbgraf/ ). Empleando los mismos casos de prueba del punto anterior determine tiempo que tarda la función
8. ce_miembro al buscar un elemento que no está en el conjunto. ¿Cual es la complejidad de esa función con esta implementación?
3. Implemente el TAD del ejercicio anterior, excepto la función de eliminar, pero empleando una árbol AVL . Realice pruebas análogas a las de los puntos anteriores
e indique la complejidad de la función ce_miembro .
2.2 Complejidad y Montículos
Indicadores de logro:
l indicador: Calcula informalmente complejidad de funciones sencillas
l indicador: Emplea montículos y diferencia sus ventajas con respecto a otras estructuras estudiadas
l indicador: Implementa y usa un TAD conjunto de enteros acotados eficiente
2.2.1 Teoría
Cálculo de complejidad
Note que nuestra métrica de complejidad es independiente de multiplicación por constantes, i.e para todo k¹ 0, O( (n))=O(k ∙ f (n)). Así que O(1) es la mismo que O(5),
f
mientras que O(n) es lo mismo que O(3n).
La complejidad temporal de una función puede determinarse contando cuantas veces se realiza cierta operación básica escogida (por ejemplo comparaciones o sumas o
productos), en el peor de los casos cuando el tamaño de la entrada es n. Considere la función esta del TAD Árbol binario ordenado:
let rec esta e a=
match a with
| ABvacio > false
| ABcons(i,v,d) >
if v=e then
true
else if (e<v) then
esta e i
else
esta e d
;;
Las búsquedas más ineficientes ocurren cuando buscamos en un árbol nada balanceado de n elementos, los cuales están sobre un mismo camino de la raíz a una única
hoja. En un árbol así podría buscarse un elemento que no esté y podría requerirse recorrer todos los nodos para decidirlo, de lo cual concluimos que la complejidad
espacial es orden de n i.e O(n).
Para comprobarlo formalmente puede plantearse un conteo del número de comparaciones realizadas en función del orden del árbol, tanto para el caso trivial como para
otros casos en la peor de las situaciones:
C(0)=0
C(n)=2+C(n1)
Note que se tiene una ecuación de recurrencia, para resolverla por ejemplo puede:
1. Probar con algunos valores buscando encontrar un patrón que después debe comprobar
2. Dependiendo del tipo de ecuación, buscar y emplear un método de solución apropiado (no para toda ecuación de recurrencia se conocen fórmulas cerradas que la
solucionen).
En este caso al probar con unos pocos valores de n puede sugerirse un patrón:
n C(n)
0 0
1 2
2 4
3 6
:
n 2n
La solución propuesta puede comprobarse, verificando que satisface la ecuación (i.e remplazando C(n)=2n en los 2 casos de la ecuación de recurrencia):
l Al remplazar en C(0)=0 se tiene 2 ∙ 0 = 0 que es cierto.
l Al remplazar en C(n)=2+C(n1) se tiene 2n=2+2(n1), igualdad que también se cumple.
Entonces la complejidad temporal será O(2n) que como se indicó antes es lo mismo que O(n).
Para determinar complejidad espacial puede emplearse un método análogo que permita contar cuanto espacio se emplea en el pero de los casos. Para el ejemplo de la
función esta como no se emplea espacio adicional la complejidad espacial es O(1).
9. Para comparar, note que en un AVL la altura será de la forma 1+log 2(n), como las búsquedas siempre recorren a lo sumo un camino de la raíz a una hoja, la complejidad
temporal será O(log 2 (n)).
Montículos
En el caso de árboles AVL la complejidad temporal de una función para extraer el mínimo elemento es O(log 2 (n)), en un montículo la complejidad de esta operación es
O(1), aunque la complejidad de una búsqueda en un montículo es O(n) mientras que en un AVL es O(log 2 n).
Así que un montículo es un tipo de árbol binario que privilegia la operación de determinar el mínimo elemento. Para esto mantiene el siguiente invariante:
1. No hay elementos repetidos
2. Todo vértice es menor que sus hijos izquierdo y derecho
3. Es un árbol binario balanceado
4. Todo subárbol es montículo
A continuación se anotan algunos detalles de las operaciones sobre esta estructura:
l menor : El menor elemento siempre está en la raíz
l buscar un elemento: Al buscar un elemento mayor que la raíz debe buscarse tanto en subárbol izquierdo como en derecho.
l insertar : Para mantener el invariante de balanceo debe insertar en el subárbol de menor altura. Para mantener el orden debe remplazarse en algunos casos la raíz
(si el elemento por insertar es menor) y en ese caso insertar la antigua raíz en el subárbol apropiado.
l eliminar : Al encontrar el elemento por eliminar puede ponerse como nueva raíz, la raíz del subárbol más alto y en ese subárbol entrar en recursión para eliminar
la raiz.
La complejidad de las operaciones mencionadas es:
Función Compl. temporal Compl. Espacial
menor O(1) O(1)
esta O(n) O(1)
inserta O(log 2 (n)) O(1)
elimina O(log 2 (n)) O(1)
Table 2.1: Complejidad de funciones sobre montículos.
2.2.2 Hacía la práctica
El TAD conjunto de enteros
Como ya se ha experimentado es posible implementar el TAD conjunto de enteros con las estructuras hasta ahora introducidas.
Sin embargo hay aplicaciones que requieren aún más eficiencia en espacio y en las diversas operaciones. Si por ejemplo sólo se usarán enteros entre 0 y 31 una
posibilidad es representar un conjunto de estos enteros con un entero de 32 bits. El bit iésimo en 1 indicaría que el número i1 está en el conjunto.
Por ejemplo el conjunto 0, 3, 8 se representa con el entero (en binario) 100001001 que tiene en 1 las posiciones (contando de derecha a izquierda) 1, 4 y 9.
Las diversas operaciones del TAD conjunto se implementan aprovechando operadores lógicos a nivel de bits. Por ejemplo para determinar si un entero v es miembro de
un conjunto, puede hacerse la conjunción lógica entre el entero que representa el conjunto y el entero que tiene el (v+1)ésimo bit en 1 y los demás en 0.
Esta idea es empleada por ejemplo en la implementación de conjuntos del generador de compiladores PCCTS (ver [7]). Este puede soportar cualquier rango de enteros
representando cada conjunto como una secuencia de enteros (y no como un sólo entero).
2.2.3 Lecturas recomendadas
Puede consultar más sobre montículos en [5]. PCCTS es un generador de compiladores de dominio público (antecesor de otro llamada Antares), puede ver más sobre
este en [7].
2.2.4 Ejercicios para afianzar teoría
1. Revise el TAD montículo en las implementaciones que acompañan estas guías, calcule la complejidad de la inserción y justifique su respuesta.
2. Haga tablas análogas a al tabla 2.1 que indiquen complejidad de las diversas funciones de los TADs árbol binario, árbol binario ordenado y AVL.
3. Escriba los montículos que se obtienen tras insertar los siguientes elementos a un montículo inicialmente vacío: 4, 3, 1, 2, 8, 6, 5
4. Descargue las fuentes de PCCTS, revise la implementación de conjuntos sobre bits de secuencias de enteros. La función set_deg retorna la cantidad de
10. elementos de un conjunto. Revisando esta función y el encabezado set.h descubra que representa el campo n de la estructura set y su relación con el campo
setword .
Opcional: ¿Cómo podría implementarse la función set_deg sin usar el vector bitmaks en una plataforma de 32 bits?
2.2.5 Ejercicios de programación
1. Pueden mezclarse de forma eficiente dos montículos (lo cual no es fácil en el caso de árboles binarios ordenados). Implemente una función mezcla: monticulo
> monticulo > monticulo que mezcle dos montículos en un tercero y calcule la complejidad temporal y espacial de su solución.
2. Escriba una función que reciba un montículo y retorne una lista con los elementos del montículo ordenados de menor a mayor.
3. Escriba una función que reciba una lista de elementos (ordenables) y que retorne un montículo con los mismos elementos. Calcule la complejidad de la función que
escribió.
Note que esta operación junto con la función del punto anterior, permiten ordenar una lista. Si esta operación se realiza de manera eficiente se tendrá una
implementación de un algoritmo conocido como HeapSort.
4. Usando las ideas presentadas en esta guía, implemente el TAD conjunto de enteros en el rango 0 a 31 con un entero de 32 bits (módulo Int32 en el caso de
Ocaml). Implemente por lo menos las funciones sugeridas en los ejercicios de la guía sobre árboles de búsquedas.
2.3 Árboles narios: árboles de sintaxis y árboles B
Indicadores de logro:
l indicador: Emplea árboles de sintaxis
l indicador: Emplea árboles B
2.3.1 Teoría
En un árbol nario cada nodo puede tener un número variable de subárboles, puede modelarse en Ocaml con un tipo como:
type 'a arbnario= ANvacio | ANcons of 'a * ('a arbnario) list;;
junto con un invariante para evitar representar con diversa sintaxis el mismo árbol: El constructor ANvacio no aparece en la lista de un constructor ANcons .
Árboles de sintaxis abstracta
Un árbol de sintaxis abstracta permite representar ciertos elementos de un lenguaje para después analizarlo o transformarlo. Por ejemplo para un lenguaje simple que
permita realizar operaciones aritméticas basta el siguiente árbol (note que algunos nodos son binarios, mientras que otros unarios y otros sólo permiten representar hojas 7):
type exprar = Const of int | InvAd of exprar | Suma of exprar * exprar |
Resta of exprar * exprar | Prod of exprar * exprar |
Div of exprar * exprar;;
usándolo, la expresión 3*(5+4) se representa como:
Prod(Const(3), Suma(InvAd(Const(5)),Const(4)))
Es posible hacer una función que reciba una cadena con la sintaxis apropiada y genere un árbol de estos. A tal función se le llama un reconocedor 8 para el lenguaje. De
esta manera será fácil implementar funciones que empleen tales expresiones.
El árbol de sintaxis para expresiones regulares del analizador léxico ocamllex es 9:
type regular_expression =
Epsilon
| Characters of char
| Eof
| Sequence of regular_expression * regular_expression
| Alternative of regular_expression * regular_expression
| Repetition of regular_expression
| Bind of regular_expression * string
En este árbol se representan expresiones regulares como:
l "pa" que sólo corresponderá con la cadena "pa" y que se representa con Sequence(Characters('p'),Characters('a')) .
l 'p' 'a'* 'l' que corresponderá entre otras con las cadenas "pl", "pal", "paal" y que se representa con Sequence(Characters('p'),Sequence
(Repetition(Characters('a')),Character('l'))) .
l ('p' 'a')* que corresponderá entre otras con la cadena vacía, con "pa", "papa", "papapa". Se representa con Repetition(Sequence(Characters('p'),
Characters('a'))) .
l ('p' | 'a')* que corresponderá entre otras con la cadena vacía, con "p", "pp", "aaaa", "ppaaap". Se representa con Repetition(Alternative
(Characters('p'), Characters('a'))) .
Un árbol de sintaxis para una porción del formato de graphviz puede ser 10 :
12. Pueden verse estas ideas en la práctica en la base de datos con fuentes de dominio público SQLite (ver [3]).
2.3.2 Hacía la práctica
Lineamientos de programación
Examinando las fuentes de SQLite (ver [3]) pueden inferirse los siguientes lineamientos, que resultan útiles para cualquier proyecto en C:
l Emplear TADs para implementar solución. Dividir las fuentes en módulos, para cada TAD se sugiere un módulo (preferiblemente independiente de la aplicación
para que sea reusable).
l Emplear nombres de variables y funciones dicientes (puede ser más efectivo que muchos comentarios en las fuentes).
l Emplear comentario sólo para aclarar porciones de código que no se entiendan con facilidad al ver nombres (de variables y funciones) y código.
l Adoptar y seguir un formato de indentación y nomenclatura.
l Distinguir entre precondiciones y situaciones de error. Las precondiciones son restricciones que el programador se impone a si mismo (por ejemplo que una función
sólo recibirá un entero positivo como entrada). Las situaciones de error se producen por entradas invalidas por parte del usuario o por condiciones del ambiente en
el que se ejecuta la aplicación (por ejemplo si se agota la memoria). Una situación de error en general debe manejarse para dar oportunidad al usuario de volver a
efectuar la operación, por otra parte una precondición no cumplida indica que hay una falla de programación (y en algunos casos la única alternativa para el
programa es terminar).
l Verificar precondiciones de cada función con assert .
l Para manejar error en lenguajes sin excepciones (como C) emplear el estilo UNIX:
¡ Hacer que el tipo retornado por toda función que pueda fallar sea int , un valor retornado 0 significa que no hay error, otros números corresponden a
códigos de error definidos para la aplicación (o estándar del sistema operativo). Otra información que deba ser retornada por la función, retornarla en
parámetros que deben llegar por referencia.
¡ Cada vez que se llame una función que pueda fallar, revisar si ha fallado o no. Si falló retornar el mismo código de error recibido y de requerirse liberar
memoria que hubiera sido localizada antes de llamar la función.
l Procurar escribir fuentes portables a diversas arquitecturas y sistemas operativos. Emplear lenguajes de programación estándares que hayan sido portados a varias
plataformas. En el caso de C/C++ emplear un módulo separado como interfaz con el sistema operativo (es decir se usan funciones de este módulo en lugar de
funciones del sistema operativo). De esta forma se facilita portar la aplicación de un sistema a otro.
2.3.3 Lecturas recomendadas
Puede consultar más sobre árboles B y B+ en [8], y una descripción de inserción y eliminación en árboles B en [1]. SQLite es un motor de bases de datos relacionales
implementado en C, es de dominio público, puede consultarse más en [3].
2.3.4 Ejercicios para afianzar teoría
1. Revise la implementación del TAD árbol nario que acompaña estas guías y proponga una definición alterna para la función altura.
2. En la documentación de Ocaml, revise la sintaxis de expresiones regulares que acepta ocamllex , después escriba ejemplos de cadenas que correspondan con
cada una de las siguientes y una expresión que las represente usando el árbol de sintaxis introducido en la guía:
¡ ['a' 'c']
¡ 'a'+
¡ 'b'('a'?)
¡ 'b' as x ('a'?)
3. De un ejemplo del uso del árbol de sintaxis para el formato de Graphviz, presentando la sintaxis concreta de su ejemplo y su representación usando la estructura de
datos dada.
4. Consulte un algoritmo para inserción en un árbol B y de un ejemplo.
5. Obtenga las fuentes de SQLite y revise la implementación de la función fileBtreeMoveto que busca una llave en un árbol B. Descubra para que se usa el cursor
pCur y el campo idx de tal cursor.
2.3.5 Ejercicios de programación
1. Implemente las siguientes funciones para árboles narios:
¡ orden que retorne el orden de un árbol
¡ balanceado que indique si un árbol nario está o no balanceado
¡ menores que decide si un árbol nario cumple este invariante: todo vértice tiene un valor menor que los valores en subárboles hijos.
2. Haga funciones que muestren la representación como cadenas de cada uno de los 3 árboles de sintaxis presentados como ejemplo.
3. Describa un lenguaje de programación sencillo (o parte de un lenguaje) que conozca y proponga un árbol de sintaxis abstracta para este. Después haga una función
13. que a partir de un árbol de esto retorne una representación como cadena.
4. Proponga un tipo para un árbol B, e implemente una función que verifique el invariante.
Opcional: Implemente una función de inserción.
1
Disponible como arbin.ml entre las implementaciones distribuidas con estas guías http://structio.sourceforge.net/guias/arbgraf/ .
2
Con propósitos prácticos, puede asociarse información a los vértices o a los arcos de un grafo.
3
Este TAD está disponible en las implementaciones que acompañan esta guía en el módulo arbinord.ml
4
Si se permiten vértices repetidos junto con la convención de que estén en subárbol derecho, no podrían balancearse muchos árboles con elementos repetidos. Si
no se sigue convención alguna i.e pueden haber repetidos a izquierda o derecha no podría aprovecharse que el árbol está ordenado en búsqueda de
ocurrencias del elemento repetido.
5
El formato PostScript es estándar de varias impresoras laser, es apropiado para incluir en documentos de TeX o LaTeX.
6
La conversión de GIF a otros formatos es fuertemente sugerida, pues GIF emplea un algoritmo de compresión patentado por Unisys.
7
Este ejemplo es solución a un ejercicio de la sección ``Tipos definidos por usuario'' de [10].
8
Del inglés parser.
9
Ver archivo lex/syntax.mli de la distribución de fuentes de Ocaml 3.07
10
El árbol presentado se basa en la implementación de [9].
Capítulo 3 Grafos
l logro: Emplea grafos para representar información y aplicar algoritmos eficientes
3.1 Expresiones regulares y autómatas finitos
Indicadores de logro:
l indicador: Define y emplea lenguajes y lenguajes regular
l indicador: Representa autómatas finitos con grafos e implementa algoritmos típicos
3.1.1 Teoría
Las expresiones regulares se emplean para describir una serie de cadenas de manera concisa. Se han introducido en guías anteriores y son usadas por diversas
herramientas (como grep , lex , sed , awk , perl ),
Como caso de estudio del uso de grafos en esta guía presentamos una forma de implementar un reconocedor de expresiones regulares usando autómatas finitos
determinísticos.
Además de introducir definiciones formales, se estudian algoritmos que permiten:
1. transformar una expresión regular primero a un autómata finito no determinístico
2. después transformar el autómata finito no determinístico a uno determinístico eficiente.
Lenguajes
Un lenguaje es un conjunto de cadenas sobre un alfabeto dado. Un alfabeto es un conjunto finito de símbolos, una cadena es una secuencia finita de tales símbolos.
Por ejemplo sobre el alfabeto S={0,2,5} un lenguaje posible es L={000,205,5,5555 }.
La cadena vacía se denota e, el lenguaje {e} contiene únicamente la cadena vacía.
Dados dos lenguajes A y B podemos definir algunas operaciones:
l Concatenación A.B={ab | aÎ A Ù bÎ B}.
14. l Iteración A 0=e. Si i>0, A i=A.A i1.
l Unión AÈ B={x | xÎ A Ú xÎ B}.
l Clausura A *=È inf A i
i=0
Un reconocedor para un lenguaje es un programa que dada una cadena s decide si pertenece o no al lenguaje.
Expresiones regulares
Una expresión regular denota un lenguaje1
l La expresión regular e denota el lenguaje {e}.
l La expresión regular a, donde a es un símbolo de un alfabeto, denota el lenguaje {a}.
l Si R y S son expresiones regulares que denotan los lenguajes LR y LS respectivamente:
¡ R|S denota L È L .
R S
¡ RS denota LR.LS
¡ R* denota L *
R
En cuanto a sintaxis concreta, de estos operadores, * tiene precedencia máxima, mientras que | tendrá precedencia mínima, pueden emplearse paréntesis para cambiar la
precedencia y como convención los caracteres de escape , *, |, ( y ) denotarán los caracteres , *, |, ( y ) respectivamente.
En la sección sobre árboles de sintaxis se introdujo un tipo que puede adaptarse (eliminando el constructor Eof ) para representar las expresiones regulares aquí
presentadas.
Autómatas Finitos
Un autómata finito es un grafo directo decorado 2. Se usa como reconocedor de lenguajes, los nodos representan posibles estados durante el reconocimiento, y los arcos
representan posibles transiciones de un estado a otro. Hay un nodo inicial y un conjunto de nodos finales (cuando se comienza el reconocimiento de un nuevo texto, el
autómata comienza en el estado inicial y la cadena pertenece al lenguaje si alcanza un estado final cuando la cadena se procesa por completo). Cada etiqueta tiene una
letra que indica cuando puede ocurrir una transición.
Figure 3.1: Ejemplo de autómata finito
En la figura 3.1, hay un arco del vértice 1 al vértice 2 con etiqueta a. El nodo inicial es 1 que se indica por el símbolo !, mientras que el nodo final es 2 que se indica por el
símbolo #.
Si durante un reconocimiento el estado es 1, irá al estado 2 sólo si el siguiente carácter del texto reconocido es a.
Hay dos clases de autómatas finitos: determinísticos (AFD) y nodeterminísticos (AFN), las diferencias son:
l Un AFN permite etiquetas vacías mientras que en un AFD todos los arcos deben estar decorados con un carácter.
l Un AFN permite que varios arcos que partan de un nodo tengan la misma etiqueta. En un AFD todas las etiquetas de los arcos que parten de un nodo deben ser
diferentes.
Para efectos de implementación es más sencillo hacer un reconocedor con un AFD, pues los cambios de estado se determinan con una sola comparación.
De expresiones regulares a autómatas finitos eficientes
Es relativamente fácil generar un AFN que reconozca el lenguaje representado por una expresión regular, basta aplicar las transformaciones de la figura ??.
15. Figure 3.2: Transformaciones de expresiones regulares en autómatas finitos no determinísticos
Una característica de esta transformación es que da un AFN que tienen un sólo estado final. En el primer caso, el de un símbolo, el estado inicial lo representamos con i
mientras que el estado final con f .
Note que a representa un símbolo cualquiera del alfabeto, mientras que R y S son expresiones regulares. Las elipses NR y NS representan el autómata finito que resulta de
transformar R y S respectivamente, el primero tiene como estado inicial i1 y como estado final f 1, los análogos para NS son i2 y f 2.
Por ejemplo para transformar la expresión regular "(ab)|c*": pueden crearse subgrafos siguiendo las reglas descritas hasta obtener el grafo que corresponde a la expresión
completa, como se presenta en la figura ??.
16. Figure 3.3: Ejemplo de transformación de la expresión (ab)|c*
Por facilidad y eficiencia al implementar un reconocedor de lenguajes regulares puede resultar buena opción transformar el AFN que se obtiene tras traducir la expresión
regular en un AFD mínimo, labor que puede hacer en dos pasos: (1) transformando el AFN en un AFD y (2) minimizando el número de estados del AFD.
Transformación de un AFN en un AFD
La idea es identificar muchos nodos de un AFN en un sólo nodo de un AFD. Todos los vértices de un AFN a los que pueda llegarse desde un nodo dado pasando sólo
por arcos con etiqueta e deben identificarse como un único nodo en el AFD (la clausura vacía de un conjunto de nodos N consta justamente de todos los nodos a los que
puede llegarse desde nodos de N pasando sólo por arcos con etiqueta e).
En el siguiente algoritmo D será el AFD que se construye, cada vértice de D será un conjunto de nodos del AFN.
I=clausura_epsilon(nodo inicial del AFN)
D=({I},{})
Si I contiene un nodo final marcarlo como vértice final de D
I no ha sido visitado
mientras (haya vértices no visitados en D)
N=Tomar un vértice (conjunto) no visitado de D
marcar N como visitado
para cada etiqueta posible l
S=conjunto de nodos a los que se puede llegar desde
nodos en N pasando por arcos con etiqueta l
T=clausura_epsilon(S)
si (T ya es vértice de D) entonces
agregar en D un arco de N a T con etiqueta l
sino
agregar T como vértice no marcado a D
si T contiene el vértice final del NFA marcar T como final
agregar un arco de N a T con etiqueta l
finsi
finpara
finmientras
Minimización de la cantidad de estados de un DFA
18. siguiendo el algoritmo descrito en la guía.
¡ xy*z*u
¡ db*(c(d|e))*
¡ (ab)|(de)*
¡ x*x*x*
6. Minimice los estados de cada uno de los AFDs del ejercicio anterior, siguiendo el algoritmo descrito en la guía.
3.1.4 Ejercicios de programación
1. Empleando el tipo
type expreg=
Epsilon
| Car of char
| Secuencia of expreg * expreg
| Alternativa of expreg * expreg
| Repeticion of expreg
;;
(adaptado de uno introducido en la guía anterior) escriba la función reconoce_expreg: int > int > string > expreg que recibe como tercer
parámetro una cadena con una expresión regular (con la sintaxis concreta descrita en esta guía), como primer parámetro una posición inicial dentro de la cadena y
como segundo parámetro una posición final dentro de la cadena. Retorna la expresión regular que puede reconocer entre las posiciones inicial y final (incluidas).
2. Implemente un tipo paramétrico ('a,'b) afd para representar AFDs con nodos tipo 'a y etiquetas tipo 'b , e implemente la función afd_reconocedor:
string > afd > bool que retorne verdadero sólo si el autómata reconoce la cadena que recibe.
3. Para representar autómatas finitos no determinísticos con un sólo estado inicial y un sólo estado final podría emplearse el tipo: type ('a,'b) afn='a list *
('a*'a*'b option) list * 'a option * 'a option;;
La primera lista es de vértices, la segunda de arcos etiquetados, el tercer elemento de la tupla es el vértice inicial y el último el vértice final. La etiqueta e se
representa con None. El NFA vacio sería ([],[],None,None), los demás NFAs deben tener nodo inicial y final diferentes a None (i.e Some x).
Usando este tipo (o uno análogo en un lenguaje diferente a Ocaml) y el tipo expreg del primer ejercicio de esta guía, implemente la función afn_de_expreg :
int > expreg > ((int,char) afn, int) que traduce una expresión regular a un AFN con el algoritmo descrito en la guía. Los vértices los numera con
enteros comenzando con el entero que recibe como segundo parámetro. Retorna el AFN y el siguiente entero que no se uso para numerar vértices.
Opcional: Los AFN que resultan del algoritmo de traducción descrito tienen una característica que puede aprovecharse para hacer una estructura de datos más
eficiente. De todo vértice salen a lo sumo dos arcos y cuando salen dos ambos tienen etiquetas e. Proponga una estructura de datos que aproveche esta
característica e indique que cambios deben hacerse a afn_de_expreg para que la use.
4. Empleando el tipo del ejercicio anterior, un tipo para AFDs ('a, 'b) afd, y un TAD conjunto de enteros conjent , implemente la función afd_de_afn: (int,
char) afn > (conjent, char) afd que transforme un AFN en un AFD con el algoritmo descrito en la guía.
Opcional: La eficiencia de esta transformación depende en buena medida del TAD conjunto de enteros que se emplee. Mejore la implementación de conjunto de
enteros como bits en enteros de 32 bits, para que emplee un vector de enteros en lugar de un sólo entero, y verifique que funciona con la función de este ejercicio.
5. Escriba una función que renombre los estados de un AFD para que sean enteros: numest_afd: ('a, 'b) afd > (int, 'b) afd . ¿Cuál es la complejidad
de esta función?
6. Escriba una función que implemente el algoritmo de minimización de estados de un AFD descrito en la guía: minest_afd: (int, char) afd > (conjent,
char) afd .
1
Los lenguajes que una expresión regular puede denotar se llaman lenguajes regulares.
2
Un grafo es decorado si a cada arco se le asocia un valor. Es decir un grafo dirigido decorado con valores en un conjunto D, es una tupla G=(V,A,ea) donde V es
conjunto de vértices, A es conjunto de arcos (parejas ordenadas de vértices) y ea:A D. Gráficamente puede representarse agregando sobre cada arco el valor
asociado.
Bibliografía
[1] Btree algorithms. http://www.semaphorecorp.com/btp/algo.html, 2004.
[2] Graphviz open source graph drawing software. http://www.research.att.com/sw/tools/graphviz/, 2004.
19. [3] Sqlite: An enbeddable sql database engine. http://www.sqlite.org/, 2004.
[4] Alfred V. Aho and Jeffrey D. Ullman. Principles of Compiler Design. Addison Wesley, 1978.
[5] Richard Bird. Introducción a la Programación Funcional con Haskell. Prentice Hall, 2000.
[6] Béla Boolobás. Graph Theory: An Introductory Course. Springer Verlag, 1979.
[7] Tom Moog. Pccts resources. http://www.polhode.com/pccts.html, 2004.
[8] John Morris. Data structures and algorithms. http://ciips.ee.uwa.edu.au/ morris/Year2/PLDS210/ds_ToC.html, 2004.
[9] Vladimir Támara Patiño. Restricted design spaces: Visualization and consistency tools. Technical Report 03/01, Sonderforschungsbereich 501, Teilprojekt B5,
Universität Kaiserslautern, "http://wwwmadlener.informatik.unikl.de/agmadlener/publications/2001/sfb5010301.ps.gz", 2001.
[10] Vladimir Támara and Carlos Fidel Rodriguez. Programación en ocaml. http://structio.sourceforge.net/guias/arbgraf, 2004.
[11] Jorge Villalobos, Alejandro Quintero, and Mario Otero. Estructuras de Datos: Un Enfoque desde Tipos Abstractos. McGraw Hill. Ediciones Uniandes, 1988.
Index
This document was translated from LA TEX by HEVEA.