You are on page 1of 71

Notas de Docencia Tecnicas Avanzadas de

Programacion
Hector Navarro
5 de agosto de 2011

Indice general
1 Introduccion 3
2 Estructuras de Datos Avanzadas 6
2.1 Heaps . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.2 Analisis amortizado . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.3 Heaps Binomiales . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.3.1 Estructura de datos . . . . . . . . . . . . . . . . . . . . . 12
2.3.2 Operaciones . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3.3 Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.4 Heaps de Fibonacci . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.4.1 Funcion de potencial . . . . . . . . . . . . . . . . . . . . . 16
2.4.2 Operaciones . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.5 Conjuntos disjuntos . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.5.1 Estructura de datos . . . . . . . . . . . . . . . . . . . . . 18
2.5.2 Implementacion de union-pertenencia . . . . . . . . . . . 18
2.6 Tablas Hash . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.6.1 Tipos de funciones hash . . . . . . . . . . . . . . . . . . . 21
3 Grafos 27
3.1 Estructuras de Datos para Grafos . . . . . . . . . . . . . . . . . . 27
3.2 B usqueda en profundidad (DFS - Depth First Search) . . . . . . 27
3.3 B usqueda en amplitud (BFS - Breadth First Search) . . . . . . . 32
3.4 Otros problemas de Grafos . . . . . . . . . . . . . . . . . . . . . 34
4 Divide y venceras 36
4.1 Ejemplo: multiplicacion de enteros muy grandes . . . . . . . . . . 36
4.2 Exponenciacion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
4.3 QSort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
4.4 B usqueda de la mediana . . . . . . . . . . . . . . . . . . . . . . . 37
4.5 Multiplicacion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4.6 Multiplicacion de Strassen . . . . . . . . . . . . . . . . . . . . . . 38
5 Algoritmos voraces (Greedy) 40
5.1 Algoritmo A* . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
1
6 Algoritmos probabilistas 47
7 Programacion dinamica 51
7.1 Fibonacci . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
7.2 Combinatoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
7.3 Vuelto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
7.4 KSP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
7.5 MaxSum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
7.6 Multiplicacion encadenada de matrices . . . . . . . . . . . . . . . 56
7.7 LCS - Longest Common Substring . . . . . . . . . . . . . . . . . 57
7.8 String Distance . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
7.9 LCS - Longest Common Sequence . . . . . . . . . . . . . . . . . 58
7.10 Longest Increasing Sequence . . . . . . . . . . . . . . . . . . . . . 58
8 Geometra Computacional 59
8.1 Representacion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
8.1.1 Puntos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
8.1.2 Rectas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
8.1.3 Segmentos . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
8.1.4 Polgonos . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
8.2 Intersecion de segmentos . . . . . . . . . . . . . . . . . . . . . . . 60
8.3 Par de puntos mas cercano . . . . . . . . . . . . . . . . . . . . . 61
8.3.1

Arboles KD . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.3.2 B usqueda de punto mas cercano . . . . . . . . . . . . . . 64
8.3.3 B usqueda por rango . . . . . . . . . . . . . . . . . . . . . 65
8.4 Capsula convexa . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
8.4.1 Gift Wrap . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
8.4.2 El metodo de exploracion de Graham . . . . . . . . . . . 67
2
Captulo 1
Introducci on
Existen diversos problemas que no son computables, es decir, problemas que
no pueden resolverse con un algoritmo. Por ejemplo el Problema de Parada de
Turing:
Dada la descripcion de un programa y su entrada inicial, determinar si el
programa cuando se ejecuta termina en alg un momento o se ejecuta para siempre
sin parar
Supongamos que existe esta funci on y la llamamos halt(p,i) y retorna verdad
si el programa p al ser ejecutado con la entrada i se detiene. Ahora construyamos
otro programa problema:
function problema(string s)
if (halt(s,s))
while(true) ;
else
return true;
Que sucede ahora con problema(problema)?
Si se detiene, esto signica que halt(problema,problema) retorn o falso, es
decir, que problema no se detiene . . .
Si no se detiene, esto signica que halt(problema, problema) retorn o ver-
dad, es decir, que problema se detiene . . . pero no lo hace
En conclusion halt no funciona correctamente!!!, no puede construirse este
programa!
En general diremos que cualquier problema que pueda resolverse en O(N
k
)
es tratable o facil, y los problemas que requieren tiempo superpolinomial se
3
conocen como intratables o difciles.
Existe un conjunto de problemas llamados NP que se desconoce si existe una
solucion de O(N
k
). No se han descubierto algoritmos que resuelvan estos pro-
blemas en O(N
k
) pero tampoco se ha demostrado que no existan estos algoritmos.
Algunos de estos problemas estan sumamente relacionados con problemas
con solucion polinomica. Por ejemplo:
Camino m as largo vs camino m as corto. (Camino mas corto se resuelve en
tiempo polinomial)
Ciclo de Euler vs ciclo de Hamilton. (Euler, todos los arcos, es polinomial)
2-SAT vs 3-SAT. (2-SAT se resuelve en tiempo polinomial)
P: problemas que tienen una solucion determinstica en tiempo polinomico
NP: problemas que tienen una soluci on no determinstica en tiempo polin omico.
Son vericables en tiempo polinomico, esto es, dada una posible solucion, es
posible vericar si efectivamente es una soluci on en tiempo polin omico. Todo P
es NP. P NP.
Informalmente, un problema est a en la clase NPC (NP-Completo) si es NP y
es tan difcil como cualquier problema NP. En la pr actica, para determinar si
un problema es NPC debe proveerse una forma de transformar instancias del
problema a instancias de cualquier otro problema NPC en tiempo polinomial.
El primer problema que se demostr o que es NPC fue CIRCUIT-SAT. Todos los
dem as problemas NPC se pueden reducir de alguna forma a este problema. Si se
encontrara una soluci on polin omica a cualquier problema NPC, todos los dem as
problemas NPC seran tambien P. No se ha determinado si P=NP, este sigue
siendo un problema abierto y es considerado el problema abierto mas importante
de las ciencias de la computaci on. La Fig. 1 muestra los dos escenarios posibles
para los conjuntos P y NP.
4
Los primeros problemas que fueron catalogados como NPC son los siguientes:
Clique: un clique en un grafo G(E, V ) no dirigido es un conjunto de vertices
tal que cada par de vertices en el conjunto est a conectado por un arco en E, es
decir, es un subgrafo completo de G. El problema Clique consiste en encontrar
el Clique maximo en un grafo.
Vertex cover: el vertex cover de un grafo no dirigido G(E, V ) es un conjunto
de vertices tal que permite cubrir todos los vertices de V . Un vertice cubre
a todos los vertices adyacentes a el. El problema de vertex cover consiste en
buscar el vertex cover mnimo de un grafo.
Subsetsum: dado un conjunto S de n umeros enteros, existir a un subconjun-
to S

S tal que

xS
x = k para alg un k.
Tarea: Para cada funci on f(n) y tiempo t en la tabla siguiente, determine el
mayor valor de n para que un problema pueda resolverse en tiempo t, asumiendo
que el algoritmo a resolver tarda f(n) microsegundos.
1 seg 1 min 1 hora 1 da 1 mes 1 a no 1 siglo
log(n)

n
n
nlog(n)
n
2
n
3
2
n
n!
5
Captulo 2
Estructuras de Datos
Avanzadas
2.1 Heaps

Arbol esencialmente completo: si todo nodo interno, con la posible excepcion


de un nodo especial tiene exactamente dos hijos. El nodo especial, si existe,
est a situado en el nivel k 1, y posee un hijo izquierdo pero no tiene hijo derecho.
Adem as todas las hojas se encuentran en el nivel k, o en los niveles k y k 1, y
ninguna hoja del nivel k 1 esta a la izquierda de un nodo interno del mismo
nivel. Intuitivamente, un arbol esencialmente completo es uno en el que los nodos
internos se han subido en el arbol lo mas posible, con los nodos internos del
ultimo nivel empujados hacia la izquierda.
Figura 2.1:

Arbol esencialmente completo
Si un arbol binario esencialmente completo tiene una altura k, entonces hay
un nodo (la raz) en el nivel 0, dos nodos en el nivel 1, y as sucesivamente hay
2
k1
nodos en el nivel k-1, y al menos uno, pero no m as de 2
k
en el nivel k. La
altura de un arbol que contiene n nodos es k = log n|
Estos arboles pueden almacenarse en un arreglo. Si la raz esta almacenada
en la posici on 1, entonces los hijos izq. y der. del nodo de la pos. p est an en 2k y
2k + 1 respectivamente. El padre del nodo k esta en
k
2
6
Un heap es un arbol binario esencialmente completo, con la propiedad de
que la clave de cada nodo es mayor que la clave de los nodos hijos, y los nodos
hijos son a su vez heaps.
Un heap es un arbol en donde los nodos tienen una relaci on de orden parcial
a diferencia de los arboles binarios de b usqueda en donde existe una relaci on de
orden total. La relaci on de orden parcial existe entre un nodo y sus desendientes,
y entre un nodo y sus ascendientes.
Figura 2.2: Heap
0 1 2 3 4 5 6 7 8 9 10 11 12 13
10 1 7 3 11 8 5 9 14 12 10
La posici on 0 del arreglo contiene la posici n del ultimo nodo del heap dentro
del arreglo.
La posici on 1 contiene a la raz del heap. Sus dos hijos est an en las posiciones 2i y
2i +1 respectivamente, o sea las posiciones 2y 3 del arreglo. Para cualquier nodo
sus hijos izq y der est an en las posiciones 2i y 2i +1, y sus padre est a en la pos.
i
2
.
Debido a la propiedad de los heaps, siempre en un heap encontrar el elemento
mayor tiene O(1).
Heap invertido: la propiedad es al reves, o sea que el valor de la raz de cada
sub-arbol es mayor que los valores de cada uno de sus hijos.
Insercion: insertar al nal y el elemento sube (otar)
Eliminaci on: se coloca de primero el elemento que era el ultimo, y el elemento
baja (undir)
Para insertar un elemento en el heap, este se coloca en la proxima posicion
disponible del arreglo, indicada por la pos. 0 del arreglo. Luego, este elemento
debe subir hasta que llegue hasta su posici on. Esta operaci on se llama otar. El
elemento que est a otando se compara con su padre. Si es menor que su padre,
se intercambian los valores del nodo con el de su padre, y el elemento sigue
otando hasta que sea mayor que su padre, o hasta que llegue a la raz.
Para eliminar un elemento del heap, se coloca en la raz del heap el elemento
7
que estaba de ultimo, y se decrementa la posicion 0 del arreglo. Luego este
elemento debe hundirse hasta que llegue a su posici on. Esta operaci on se llama
undir. El elemento que se esta undiendo es comparado contra el menor de sus
dos hijos. Si es mayor que el menor de sus dos hijos, entonces se intercambia con
el. As, el elemento va bajando en el heap hasta que se convierte en una hoja, o
es menor que sus dos hijos.
Heapify
Heapify(T[0..N])
{
for(i = n/2; i>0; i--)
undir(T, i);
}
HeapSort
HeapSort(T[0..N])
{
Heapify(T);
for(i = n; i > 1; i--)
{
swap(T[1], T[i]);
T[0]--;
undir(T, 1);
}
}
Ejercicios:
1. Es la siguiente secuencia un Heap? 3, 4, 3, 6, 7, 4, 5, 5
2. Si un arreglo est a ordenado ascendentemente puede considerarse un heap?
3. Considerando un heap H vaco, haga las siguientes operaciones:
H.Insertar(5)
H.Insertar(7)
H.Insertar(9)
H.Insertar(2)
H.Insertar(4)
H.Eliminar()
H.Eliminar()
H.Insertar(17)
H.Insertar(6)
H.Eliminar()
8
H.Insertar(5)
H.Eliminar()
H.Insertar(14)
H.Insertar(9)
H.Eliminar()
4. Dado un arbol binario, haga un algoritmo que verique si el arbol es un
heap.
2.2 Analisis amortizado
Es bien conocido que en el analisis de complejidad en tiempo muchas veces es
conveniente tomar en cuenta el peor caso de un algoritmo para tomarlo como
referencia. Algunas veces esto no se hace ya que el peor caso es mucho peor que
el caso promedio (por ejemplo QuickSort, el peor caso es de O(N
2
) y el caso
promedio es de O(N log N)). Muchas veces cuando se analiza la complejidad en
tiempo de las operaciones de algunas estructuras de datos es necesario observar
el comportamiento de la estructura de datos a traves del tiempo en lugar de
analizar una sola llamada a la operacion especca que se estudia. Un ejemplo
propuesto por Cormen (Referencia) supone la operacion Multipop(p,k) sobre
pilas, que realiza k operaciones de pop sobre la pila p. Si analizamos una sola
llamada a la operacion Multipop veremos que su complejidad en tiempo es de
O(k), en el peor caso es necesario sacar k elementos de la pila (considerando
que si s es el n umero de elementos en la pila, el peor caso es k = s). Ahora
bien, analicemos el comportamiento de N llamadas seguidas a Multipop. La
primera llamada es de O(k), sin embargoa partir de ese momento todas las
llamadas siguientes a la operacion seran de O(1) ya que no habran mas ele-
mentos en la pila. Finalmente, tenemos que la complejidad promedio sera de
O(s)+

N
i=2
O(1)
N
=
O(s)+O(N)
N
= O(1), ya que podemos suponer que N > s. Es-
te tipo de analisis se conoce como analisis amortizado ya que una operacion
est a comprando a credito el costo de las futuras operaciones. En el ejemplo, la
primera llamada a Multipop es muy costosa, pero a partir de ah las siguientes
llamadas son de O(1), haciendo que en promedio si se hacen muchas llamadas el
orden sea constante.
Existen diversas tecnicas formales para el analisis amortizado, pero estudia-
remos una de las mas exibles llamado el metodo del potencial.
Metodo del potencial: suponiendo una estructura de datos inicial D
0
sobre la
cual se realizan n operaciones, para cada i = 1, 2, . . . , n calculamos el costo c
i
de
realizar la operaci on i y obtener la estructura de datos D
i
resultante de aplicar
esta operaci on sobre la estructura D
i1
. Existe una funci on de potencial que
mapea cada estructura D
i
a un n umero real (D
i
), que es el potencial asociado
a D
i
. El potencial puede irse acumulando hasta que en alg un momento, con la
9
llamada a alguna operaci on este potencial es liberado. El costo amortizado c
i
de
la operacion i-esima se calcula como:
c
i
= c
i
+ (D
i
) (D
i1
) (2.1)
El costo total asociado a realizar n operaciones sera:
c
i
=
n

i=1
(c
i
+ (D
i
) (D
i1
))
=
n

i=1
c
i
+ (D
n
) (D
0
)
Es facil ver que si garantizamos que (D
n
) > (D
0
) entonces la expresion
anterior sera una cota superior de el costo total (

n
i=1
c
i
)
Para el ejemplo de la pila, la funci on de potencial puede denirse como el
n umero de elementos de la pila. Si la pila est a vaca el potencial es 0, a medida en
que se van apilando valores el potencial se incrementa hasta liberarlo cuando
se invoca a la funcion Multipop. Es claro que (D
n
) > (D
0
). Luego, el costo
amortizado de una operacion de push sera:
c
i
= 1 + (s (s 1)) = 1 + 1 = 2 (2.2)
Para la operacion Multipop(p,k), con k

= min(k, s), en donde s es el


n umero de elementos en la pila, k

objetos ser an sacados de la pila. El costo de


la operacion es k

. Luego:
c
i
= k

+ (s k

s) = k

= 0 (2.3)
De forma similar el costo amortizado para la operacion pop sera c
i
= 0, y
conclumos que para cada una de las operaciones, el costo asociado es de O(1).
Luego, hacer n operaciones arbitrarias sera de O(n) en promedio.
2.3 Heaps Binomiales
En algunos casos es importante tener operaciones que permitan mezclar heaps,
agregando las operaciones siguientes:
union(H
1
, H
2
): crea y retorna un nuevo heap que contiene todos los nodos
de H
1
y H
2
decrementarClave(H, x, k): modica el valor del nodo x del heap H
asignandole un valor k menor a su valor actual
eliminar(H, x): elimina el nodo x del heap H
10
Figura 2.3: Denicion recursiva de arboles binomiales
Figura 2.4:

Arboles binomiales B
0
hasta B
4
Nota: en las operaciones decrementarClave y eliminar se supone que ya
se tiene el apuntador x al nodo de interes. La operacion de b usqueda de una
clave en un heap es ineciente.
Un arbol binomial es un arbol ordenado denido recursivamente:
El arbol binomial B
0
consiste de un nodo simple
El arbol binomial B
k
consiste de dos arboles binomiales B
k1
enlazados:
la raz de uno es el hijo izquierdo de el otro
La Figura 2.3 muestra gracamente esta denicion recursiva. La Figura 2.4
muestra los primeros heaps binomiales.
Propiedades de arboles binomiales:
tiene 2
k
nodos
11
Figura 2.5: Ejemplo de Heap Binomial
tiene altura k
hay exactamente

k
i

nodos en la profundidad i
la raz tiene grado k, y es el nodo con mayor grado. Si los hijos de un nodo
los etiquetamos de izquierda a derecha k 1, k 2, . . ., 0, el hijo i-esimo
es la raz de un subarbol B
i
.
Un heap binomial H es un conjunto de arboles binomiales que satisfacen las
propiedades:
1. En cada arbol binomial H la clave de un nodo es mayor o igual a la clave
de su padre (min-heap)
2. Para cada entero no negativo k, hay cuando mucho un arbol binomial en
H cuya raz tiene gardo k
La Figura 2.5 muestra un ejemplo de un heap binomial con 13 nodos. Como
13 en binario se escribe 1101, necesitamos B
0
, B
2
y B
3
para construir el heap.
2.3.1 Estructura de datos
struct NodoHeapBin{
NodoHeapBin *p;
int k;
int grado;
NodoHeapBin *hijo;
NodoHeapBin *hermano;
};
p apunta al nodo padre, k es la clave del nodo, grado es el grado del nodo
(n umero de hijos), hijo apunta al hijo mas izquierdo del nodo y hermano apunta
al siguiente hermano del nodo. Visualmente una parte del heap binomial de la
Figura 2.5 se vera como se muestra en la Figura 2.6.
12
Figura 2.6: Representacion de heaps binomiales
2.3.2 Operaciones
Mnimo: para obtener el valor mnimo de un heap binomial es necesario comparar
las races de todos los arboles binomiales del heap. Puede demostrarse que
habra cuando mucho log n| + 1 arboles binarios (la demostracion es facil,
pensar en la representaci on binaria de n), por lo que la operaci on de mnimo es
de O(log n)
Union: una funcion basica que enlaza dos arboles binomiales con races que
tengan el mismo grado se muestra a continuacion:
enlace_binomial(NodoHeapBin * y, NodoHeapBin * z)
{
y.p = z;
y.hermano = z.hijo;
z.hijo = y;
z.grado++;
}
Para unir heaps binomiales primero es necesario mezclar ordenadamente los
dos heaps a unir, creando una lista de arboles binomiales en donde posiblemente
hayan pares de arboles con la misma cantidad de nodos. La lista se va recorriendo
en orden ascendente. En cada paso de la iteracion pueden ocurrir los casos
siguientes:
1. El arbol actual y el siguiente tienen grados distintos: en este caso simple-
mente nos movemos hacia el proximo nodo
2. El arbol actual y el siguiente tienen grados iguales: notese que es posible
que sean dos arboles iguales o tres arboles iguales. Si son dos arbol iguales
13
de 2
k
nodos cada uno, podemos unirlos para formar un nuevo arbol de
tama no 2
k+1
(en este caso es posible que ahora existan 3 arboles de 2
k+1
nodos). Ahora bien, cuando se van a unir los dos arboles es necesario
evaluar las claves de sus races para determinar cual de ellos debe ir como
raz. Si son tres arboles de igual tama no es necesario dejar el primer arbol
intacto y movernos al proximo arbol del mismo tama no.
La operacion de union de heaps binomiales es de O(log n)
Insercion: la operacion de insercion se implementa creando un heap binomial
con un solo nodo (el nodo que se quiere agreagar) y luego haciendo uni
1
2
n entre
estos heaps.
Extraccion del elemento mnimo: primero se debe buscar el menor elemento
(que debe estar como raz de alg un arbol binomial). Este nodo x es removido de
la lista de nodos races de H. Luego se crea una lista enlazada con los hijos de
x en sentido inverso y se crea un nuevo heap binomial H

con estos elementos.


Finalmente se unen los heaps binomiales H y H

. La complejidad en tiempo
sera entonces de O(log n)
Ejemplo de esto!
Decrementar una clave: algunos algoritmos requieren esta operacion ecien-
temente. Se asume que se conoce el apuntador al heap H, el apuntador al nodo a
decrementar x y el nuevo valor de la clave k. Es necesario vericar que el nuevo
valor de k sea menor al anterior. En caso positivo el nodo otar a hasta llegar a
su lugar. Es una operacion de O(log n)
Ejemplo de esto!
Eliminar un nodo: para eliminar un nodo x decrementaremos su clave hasta
para hacerlo subir hasta la raz y luego extraemos el elemento mnimo. Es
una operacion de O(log n)
2.3.3 Ejercicios
1. Un dibujo de un heap binomial y debe decir si es efectivamente un HB
2. Realice las operaciones siguientes sobre un Heap binomial: . . .
3. Dado un apuntador a un Heap binomial, haga un algoritmo que determine
si es efectivamente un Heap Binomial
2.4 Heaps de Fibonacci
Los heaps de Fibonacci tienen la ventaja sobre heaps binomiales de que las
operaciones en donde no se eliminen nodos son de O(1). Son sumamente utiles
cuando el n umero de operaciones de eliminacion son relativamente pocas en
relacion al total de operaciones. En particular el algoritmo de Dijkstra para
encontrar el camino mas corto tiene su implementacion mas eciente usando
heaps de Fibonacci.
Los heaps de Fibonacci son una coleccion de arboles ordenados, aunque no
son unicamente arboles binomiales. La Figura 2.7 muestra un ejemplo de un
14
Figura 2.7: Ejemplo de Heap de Fibonacci
Figura 2.8: Representacion de heaps de Fibonacci
Heap de Fibonacci. Notese el apuntador al mnimo. La representacion (Figura
2.8) tiene listas circulares haciendo todos los nodos iguales. Todo nodo tiene
un apuntador a alg un hijo (cualquiera) y un apuntador a su padre. La listas
circulares tienen como ventaja que puede removerse un nodo en O(1) y dos listas
circulares pueden concatenarse tambien con O(1).
La representacion sera la siguiente:
struct NodoHeapFib{
NodoHeapFib *p, *h;
NodoHeapFib *izq, *der;
int k;
int grado;
bool marca;
};
El campo nuevo aqu, marca, indica si el nodo ha perdido un hijo desde la
ultima vez que se convirti o en hijo de otro nodo. Los nodos recien creados tienen
este atributo en falso. Otro valor importante es el n umero n de nodos en u
Heap de Fibonacci. Luego veremos que el grado maximo de cualquier nodo en
15
un HF con n nodos es D(n) = O(log n)
2.4.1 Funcion de potencial
Cada arbol que este en la raz del Heap de Fibonacci va a sumar un punto a
la funcion de potencial, debido a la operacion de extraccion de valor mnimo
(trabaja sobre el n umero de arboles). Sin embargo tambien es necesario tomar
en cuenta el n umero de nodos que estan marcados, ya que en el peor caso,
si todos los nodos estan marcados, la operacion de decrementar clave debe
recorrer todos los nodos marcados. De ah se propone la siguiente funcion de
potencial: (D
i
) = T(D
i
) +m(D
i
), en donde T(D
i
) es el n umero de arboles en
la raz del heap y m(D
i
) es el n umero de nodos marcados. Esta funcion trae
problemas numericos que pueden solventarse usando la funcion de potencial
(D
i
) = T(D
i
) + 2m(D
i
)
2.4.2 Operaciones
Creacion de heaps de Fibonacci: para crear un HF vaco simplemente n = 0
y min[H] = NIL.
Insercion: para insertar un nodo x en un HF H, primero inicializamos los
valores del nodo:
x.p = NULL;
x.h = NULL;
x.izq = x.der = x;
x.grado = 0;
x.marca = falso;
Ahora, el nodo se enlaza con la lista de arboles de H:
min[H].izq.der = x;
min[H].izq = x;
if(min[H]==NULL || x.clave < min[H].clave) min[H] = x;
Mnimo valor: es trivial, ya que tenemos el apuntador min[H]
Union: la union consiste simplemente en concatenar las dos listas de los
heaps H
1
y H
2
a unir, y buscar el mnimo. La concatenaci on es de O(1) ya que
son listas doblemente enlazadas, y la b usqueda del valor mnimo es tambien
de O(1) ya que unicamente existen dos candidatos a ser el mnimo: min[H
1
] o
min[H
2
]
Extraccion del nodo mnimo: la extracci on del nodo mnimo es la opera-
cion mas complicada. Primero se localiza el nodo mnimo mediante min[H] y
todos sus hijos se pondr an directamente en la raz, como se muestra en la Figura
2.9.
Una vez que se ha hecho esto, el paso siguiente es consolidar el arbol para
reducir el n umero de arboles en la raz de H. Esto se hace con la funcion
16
Figura 2.9: Extracci on del nodo mnimo: los hijos del nodo mnimo se suben a la
raz
consolidar(H), la cual repite los siguientes pasos hasta que todos los nodos en
la raz tengan grado distinto:
1. Encontrar dos nodos x, y en la raz con el mismo grado (x.clave y.clave)
2. Enlazar y a x, removiendo y de la raz y colocandola como hijo de x. (el
grado de x de incrementa, y la marca de y de pone en falso)
Se hace uso de un arreglo auxiliar A de apuntadores a nodos tal que A[i]
apunta al ultimo nodo de grado i encontrado (o a nil si no se ha encontrado).
Puede demostrarse que el tama no de A nunca sera mayor a log n.
La Figura 2.4 muestra el proceso de consolidaci on para el ejemplo. El costo
amortizado de la operaci on de extracci on del mnimo es de O(D(n)) = O(log n).
Se puede hacer un an alisis amortizado, viendo que la complejidad del algoritmo
dependera del n umero de arboles en la raz del heap. Si antes haban T(H)
arboles, en el peor de los casos ahora habran T(H) + D(n) 1 (Los mismos
T(H) menos 1 (el nodo que estamos extrayendo, mas D(n) que es en el peor caso
el grado que tena el nodo que estamos extrayendo)). De ah podemos calcular
c
n
+ (D
n
) (D
n1
)
Decrementando una clave: para decrementar el valor de un nodo x en un
heap H primero es necesario comprobar que efectivamente el nuevo valor de la
clave es menor que el valor actual. En caso positivo se compara el nuevo valor
con el valor del padre de x (y). En caso de ser menor x se remueve de la lista de
hijos de y y se pone en la raz de H (poniendo la marca de x en f also). Ahora
el nodo y debe marcarse ya que perdio un hijo. En caso de que el ya hubiera
sido marcado previamente, hay que llevar al nodo y a la raz de H y repetir el
proceso (marcar al padre de y). La Figura 2.11 muestra un ejemplo de varias
extracciones en un Heaps de Fibonacci
El costo amortizado de esta operaci on de de O(1). Esto puede verse usando
el metodo del potencial. Suponiendo que haban c nodos seguidos que ya haban
sido marcados y fueron puestos como raz del heap, podemos ver que en este caso
(D
n
) = t(H) +c +m(H) c. t(H) +c arboles ya que haban t(h) y estamos
17
agregando m aximo c. Asmismo, hay m(h) c nodos marcados, ya que estamos
desmarcando c. El costo en s de la operacion de decrementar sera c.
Eliminacion de nodos: para eliminar un nodo x de un HF H simplemente
decrementamos la clave de x hasta y luego extraemos el mnimo nodo de H,
de forma similar a lo realizado con los heaps binomiales. Esta es una operaci on
de O(log n)
2.5 Conjuntos disjuntos
En a nos recientes se han estudiado bastante los algoritmos para procesamiento
de conjuntos, o clases de equivalencia para responder preguntas del tipo es x
equivalente a y?
Se denen dos tipos de operaciones basicas: union y pertenencia.
La estructura de datos a usar es un bosque de arboles. La uni on es equivalente
a unir dos arboles, y la pertenencia es equivalente a vericar si dos elementos se
encuentran en el mismo arbol
En un comienzo cada arbol del bosque contiene a un s olo elemento. Se supone
que en un comienzo cada elemento pertenece a un unico conjunto. Cuando se
hacen operaciones de uni on, se forman arboles. Por ejemplo, el arbol de la Figura
2.12 representa los conjuntos disjuntos A, B, C, D, E, F, G.
Podemos ahora unir los conjuntos que contienen a los elementos A y G, y
obtenemos el primer bosque de la Figura 2.13. Las demas lneas de la gura
muestras el resultado luego de aplicar las operaciones unir(A, D), unir(B, F),
unir(F, E).
2.5.1 Estructura de datos
Para el tipo de operaciones que soportan los conjutos disjuntos (union entre
conjuntos y pertenencia de dos elementos al mismo conjunto), no es necesario
tener apuntadores hacia los hijos, unicamente apuntadores desde los hijos ha-
cia los padres. La idea es que dos elementos estan en un mismo conjunto si
est an en el mismo arbol, esto es, si la raz del arbol al que pertenecen es la misma.
Si se usa un arreglo para almacenar los elementos, se usa un arreglo padre que
contiene los ndices de los padres de cada elemento. En caso de una estructura
dinamica, usaremos un apuntador al padre.
2.5.2 Implementaci on de union-pertenencia
La operacion de pertenencia busca la raz de los arboles en donde estan ambos
elementos. Si la raz coincide retorna verdadero y falso en caso contrario.
El parametro union indica si los conjuntos deben unirse (en caso de que los
elementos esten en conjuntos distintos)
18
int pertenencia(int x, int y, int union)
{
int i=x, j=y;
while(padre[i]>0) i=padre[i]; // buscar la raiz de este arbol
while(padre[j]>0) j=padre[j]; // y de este tambien
if (union && (i!=j)) padre[j]=i;
return i!=j;
}
La implementacion es bastante sencilla, aunque su comportamiento es muy
malo en el peor de los casos. cu al es la complejidad en tiempo de este algoritmo?
Entonces la idea es mantener el arbol lo mas peque no posible. Cuando se unen
dos arboles, uno de los nodos permanece como raz, y el otro (con sus descendien-
tes) baja un nivel. Para minimizar la altura de los arboles, parece razonable que
quede como raz el que tenga mayor n umero de descendientes. Esta idea se imple-
menta f acilmente manteniendo un arreglo con el n umero de descendientes de cada
elemento. Para no utilizar un arreglo adicional, una idea es que el mismo arreglo
de ndices de padres puede contener un ndice negativo en caso de que sea la raz
de un arbol. El valor absoluto de este ndice indica el n umero de nodos en el arbol.
Otra idea importante que ayuda a hacer las operaciones ecientes es la
compresion de caminos, que consiste en que cada vez que se recorre el arbol
en la operacion de U-P, todos los nodos recorridos se pueden poner como hijos
de la raz directamente, como se observa en la Figura 2.14.
int pertenencia(int x, int y, int union)
{
int i=x, j=y;
while(padre[i]>0) i=padre[i]; // buscar la raiz de este arbol
while(padre[j]>0) j=padre[j]; // y de este tambien
while(padre[x]>0) {t=x; x=padre[x]; padre[t]=i;}
while(padre[y]>0) {t=y; y=padre[y]; padre[t]=j;}
if (union && (i!=j))
{
if (padre[j] < padre[i])
{
padre[j]+=padre[i]-1; padre[i]=j;
} else
{
padre[i]+=padre[j]-1; padre[j]=I;
}
}
return i!=j;
}
19
No es facil analizar el tiempo necesario para realizar N operaciones de
union-pertenencia distintas.
Tarjan logro demostrar que realizar 1 operacion de union-pertenencia se
ejecuta en un tiempo proporcional a log n, logaritmo iterado de n (n umero de
veces que hay que aplicar log para llegar a 1)
Como es una funcion que crece tan lentamente, se considera constante, por
lo tanto, realizar N operaciones distintas de u-p, puede considerarse como lineal.
2.6 Tablas Hash
Un metodo que permite hacer directamente referencia a los registros en una
tabla por medio de transformaciones aritmeticas sobre la clave para obtener
direcciones en la tabla. Si se sabe que las claves son enteros distintos, entre
0 y N 1, puede almacenarse un registro con clave i en la i-esima posicion
de la tabla. La dispersion o hashing es una generalizacion de este metodo en
aplicaciones de b usqueda tpicas donde no se tiene ning un conocimiento concreto
sobre los valores de las claves.
Una b uqueda por dispersion consiste en dos pasos:
Evaluar la funcion de dispersion (Hash function)
Resolver las colisiones
Funciones de dispersion
Se necesita una funcion que transforme claves (habitualmente enteros o ca-
denas cortas de caracteres) en direcciones de la tabla en el intervalo [0, M 1],
en donde M es el n umero de resigtros que se pueden calcular en funcion de la
memoria disponible.
Una funcion de dispersion ideal debe:
Ser facil de calcular
Ser una aproximacion de una funcion aleatoria: que para cada entrada,
toda salida sea igualmente probable
El primer paso consiste en transformar las claves en n umeros, sobre los que
se realicen las operaciones aritmeticas. Para claves peque nas esto puede no
20
signicar trabajo alguno, si se utilizan las representaciones binarias internas de
los caracteres. Para representaciones mayores, se puede intentar extraer bits de
las cadenas de caracteres y empaquetarlos en una palabra.
El segundo paso consiste en tomar este n umero y aplicarle alguna transfor-
maci un para llevarlo al intervalo [0, M 1]. La forma mas sencilla de realizar
esto consiste en seleccionar M primo, y para cualquier clave k aplicar la trans-
formacion h(k) = k mod M.
Sup ongase por ejemplo que la palabra clave es CLAVE, y el tama no de la
tabla es M = 101, obteniendo la siguiente representacion:
0001101100000011011000101, equivalente a 3540677 en base 10.
Luego, 3540677 mod 101 = 21, por lo que a la palabra CLAVE le correspon-
de la posici on 21 de la tabla. Hay muchas claves posibles y relativamente pocas
posiciones de la tabla, por lo que a muchas claves le corresponden la misma
posicion.
Si la clave es muy larga, por ejemplo GRANCLAVE, obtendremos un n umero
de 9 5 = 45 bits que es demasiado largo!
Colisiones
Encadenamiento separado: cada entrada de la tabla contiene una lista
con todos los elementos que fueron mapeados a esa entrada. Para buscar un
elemento es necesario recorrer toda la lista hasta encontrarlo o determinar que
no esta.
Exploracion lineal (Si M > N): Si hay colision se busca en la proxima
posici on libre de la tabla. Cuando se este buscando un elemento, se busca hasta
encontrarlo o hasta llegar a una posicion vaca de la tabla. Si se llega a una
posicion vaca, signica que el elemento no esta en la tabla.
2.6.1 Tipos de funciones hash
Cuando se va a aplicar una funci on hash a una clave de tipo string es necesario
tomar decisiones:
Todos los caracteres
Caracteres al nal o en el medio
Metodos para strings
21
Metodo de adici on: se suman los valores de los caracteres del string y al nal
se aplica modulo de ser necesario.
unsigned char hash(char *str) {
unsigned char h = 0;
while (*str) h += *str++;
return h;
}
Metodo de o exclusivo (XOR): se aplican XORs sucesivos sobre los caracteres
del string:
unsigned char hash(char *str) {
unsigned char h = 0;
while (*str) h ^= *str++;
return h;
}
Metodo de la multiplicacion
Cuando M = 2
N
(la tabla es una potencia de 2). La clave es multiplicada
por una constante, y luego se extraen los bits necesarios para indexar la tabla.
Knuth recomienda utilizar el radio dorado (

51
2
)
1
para ayudar a encontrar la
constante. Por ejemplo, si la tabla tiene M = 32 = 2
5
entradas, y se utilizar a un
entero de 8 bits para indexarlo, el multiplicador a utilizar sera 2
8

51
2
= 158.
Esto escala el radio dorado para que el primer bit del multiplicador sea 1. Luego,
dada una clave K, se multiplica K158. El resultado obtenido tendra 16 bits, y
se toman los 5 bits m as signicativos del byte menos signicativo. Por ejemplo,
para K = 101, K158 = 101 158 = 15958 = (11111001010110)
2
. El byte menos
signicativo es 01010110, y se extraen los 5 bits mas signicativos, es decir:
01010. Luego, el resultado sera 10.
Metodo aleatorio
Introduce una distribuci on aleatoria para intentar dispersar mejor los datos:
unsigned char rand8[256];
unsigned char hash(char *str) {
unsigned char h = 0;
while (*str != NULL) h = rand8[h ^ *str++];
return h;
}
1
El radio dorado es la proporci on que la suma de dos n umeros es al mayor de esos n umeros
lo que el mayor de los n umeros es al menor. Esto es, si los n umeros son a y b, y a es mayor,
a
a+b
=
b
a
=
22
rand8 contiene los 256 caracteres distribudos aleatoriamente.
hashpjw (P. J. Weinbergers C compiler): Una funci on implementado
por Weinberger para usarla en tablas hash de compiladores (para manejar la
tabla de smbolos), sumamente usada a un:
#define M 211
int hashpjw(char *s)
{
char *p;
unsigned h=0, g;
for (p=s; *p!=0; p++)
{
h=(h<<4) + *p;
if (g=h & 0xf0000000)
{
h=h^(g>>24);
h=h^g;
}
}
return h;
}
Una forma de evaluar que tan bien funciona una funcion hash es contando
cuantos elementos hay que acceder para encontrar todos los elementos en la
tabla. Suponiendo que el n umero de elementos en la entrada i de la tabla es
B
i
, es f acil ver que para acceder a todos los elementos dentro de esa entrada se
necesitan

Bi
i=1
i =
Bi(Bi+1)
2
, ya que hay que acceder al primer elementos (un
acceso), luego al segundo (dos accesos), el tercero (tres accesos), y as sucesiva-
mente hasta llegar al elemento B
n
(B
n
accesos). Ahora bien, si queremos acceder
todos los elementos en todas las entradas de la tabla hash, el total de accesos
sera (suponiendo una tabla de tama no M):
M1

i=0
B
i
(B
i
+ 1)
2
(2.4)
23
Figura 2.10: Consolidacion del Heap de Fibonacci
24
Figura 2.11: Extraccion de Heaps de Fibonacci
Figura 2.12: Representacion de los conjuntos disjuntos
A, B, C, D, E, F, G
Figura 2.13: Secuencia de operaciones de union entre conjuntos disjuntos
25
Figura 2.14: compresion de caminos a partir del nodo 6
26
Captulo 3
Grafos
3.1 Estructuras de Datos para Grafos
Existen diversas formas de representar grafos. Muchas veces es necesario mante-
ner al mismo tiempo mas de una de estas representaciones para lograr m axima
eciencia. Sea G = (V, E) un grafo con vertices V y arcos E:
Matriz de adyacencia: Es una matriz M de [V [ [V [ en donde M
i,j
= w si
existe un arco de peso w entre los vertices i, j, y M
(
i, j) = 0 si no existen
arcos entre i, j. La inserciones de arcos y b usquedas de arcos son de O(1).
Recorrer a todos los vecinos del ve i es de O([V [)
Lista de arcos: Se almacenan los arcos linealmente en una lista de [E[
elementos. Realizar una b usqueda de un arco es de O([E[). Insertar un
arco es de O(1). Recorrer a todos los vecinos del vertice i es de O([E[)
Lista de adyacencia: Se tiene un arreglo L de [V [ posiciones. En cada
entrada del arreglo se encuentra una lista de nodos adyacentes incluyendo
los pesos de los arcos. Si
i
denota el grado del vertice i del grafo, la
b usqueda es de O(
i
), las inserciones de arco son de O(1), y recorrer todos
los vecinos del vertice i es de O(
i
).
3.2 B usqueda en profundidad (DFS - Depth First
Search)
Es un metodo para explorar los nodos de un grafo y comprobar cada arista de
modo sistematico. Pueden contestarse preguntas como:
El grafo es conexo?
Cuales son sus componentes conexas?
27
contiene ciclos?
La implementaci on mas sencilla para DFS es haciendo una funci on recursiva:
void visitar(int k)
{
nodo * t;
val[k] = id++;
for(t = ady[k]; t != null; t = t->prox)
if (val[t->v] == -1) visitar(t->v);
}
En donde val es un arreglo de tama no [V [ que indica cuales nodos han si-
do visitados, y en que orden. Inicialmente val[i] = 1 i. El valor inicial de id es 0.
La b usqueda en profundidad de un arbol representado con listas de adya-
cencia es O([V [ +[A[). La b usqueda en profundidad de un arbol representado
con listas de adyacencia es O([V [
2
). La Figura 3.1 muestra un ejemplo de una
b usqueda en profundidad en un grafo.
Eliminando la recursion
Es posible tener una pila en donde se almacenan los nodos que han sido
descubiertos pero no han sido procesados todava, obteniendo un algoritmo que
en la practica es mas rapido al eliminar recursividad.
Pila p(|V|);
void visitar(int k)
{
nodo * t;
p.push(k);
while(!p.empty())
{
k = p.pop();
val[k] = id++;
for(t = ady[k]; t!=null; t = t->prox)
if (val[t->v] == -1)
{
p.push(t->v);
val[t->v] == -2;
}
}
}
28
Figura 3.1: Ejemplo de b usqueda en profundidad
Inicialmente el arreglo val est a inicializado con 1. Cada vez que se encuentra
un nodo nuevo (pero no se visita todava), este es apilado, pero debe marcarse con
un valor distinto a 1 para estar seguros de que no ser a apilado de nuevo. Si el
mismo nodo es conseguido posteriormente (antes de ser visitado) no ser a agregado
a la pila.
Un backtracking puede verse como un DFS en el que cada llamada recursiva
es un nodo del algoritmo.
Algunos algoritmos faciles de construir basados en DFS:
Componentes conexas: se puede tener un contador de componentes conexas
que se incrementa externamente po cada llamada que se haga al DFS
Biconexi on de grafos: Un grafo es biconexo si existen al menos dos caminos
entre cada par de nodos. Un punto de articulacion de un grafo conexo es
un vertice que si es removido dividira al grafo en 2 o mas componentes
29
Figura 3.2: Grafo bixonexo
conexas. Si un grafo conexo no tiene puntos de articulacion, entonces es
biconexo. La Fig. 3.2 tiene como puntos de articulaci on los vertices A, G,
H y J.
Un vertice x no es punto de articulacion si todos sus hijos y tienen alg un
nodo descendiente conectado a un nodo mas alto que x. En el caso de la
raz del arbol, es un punto de articulaci on si tiene mas de un hijo (ya que
no existe un camino alternativo para llegar al segundo hijo).
Para detectar un punto de articulacion entonces simplemente debemos
hacer un DFS y ver para cada vertice si alguno de sus hijos no tiene
descendientes con un arco hacia un vertice que fue visitado anteriormente.
Esto signicara que no existe un camino para unir las dos componentes
biconexas. El algoritmo siguiente ilustra esto:
bool articulacion[N] = {false, ..., false};
int visitar(int k)
{
nodo * t;
int m, min;
min = id; // cual es el minimo id con el que existe un arco
val[k] = id++;
for(t = ady[k]; t!=NULL; t=t->prox)
{
if(val[t->v]==-1)
{
m = visitar(t->v);
if(m < min) min = m; // para acumular en m al minimo vertice
// encontrado
if(m >= val[k]) articulacion[k] = true; // si este hijo no tiene
// un arco hacia arriba,
// entonces k es un pto
// de articulacion
30
Figura 3.3: Grafo con varias componentes biconexas y su arbol de llamadas
respectiva
}
else if (val[t->v] < min)
min = val[t->v]; // encontramos un arco a un vertice ya visitado,
// si esta mas arriba que min, entonces ese es
// nuestro nuevo min
}
return min;
}
Se puede ver un ejemplo de este algoritmo en la Figura 3.3. Puede observarse
como los vertices 1, 2 y 6 son puntos de articulacion ya que ninguno de
sus hijos tiene arcos hacia vertices por encima de ellos.
Ordenamiento topologico: un ordenamiento topologico de un grafo G di-
rigido es un ordenamiento lineal de todos los vertices de G tal que si G
contiene un arco(u, v), entonces u aparece antes que v en el ordenamiento.
Si el grafo no es acclico, no es posible hacer un ordenamiento lineal. Un
ordenamiento topologico de un grafo puede verse como un ordenamiento
de sus vertices a lo largo de una linea horizontal de forma tal que todos
los arcos dirigidos van de izquierda a derecha. Los grafos dirigidos acclicos
(DAGs) son usados en muchas aplicaciones para indicar precedencia entre
eventos. Para cada DAG existen posiblemente varios posibles ordenamien-
tos topologicos posibles.
Es importante resaltar que no se puede hacer el DFS comenzando desde
cualquier vertice.

Unicametne se puede comenzar desde vertices fuentes
(vertices sin nodos incidentes) ya que estos no tienen ning un predecesor.
Si en un DFS se imprimen los nombres de los vertices apenas se entra a la
31
funci on visitar, se obtienen los nombres de los vertices en orden topol ogico.
Si se imprime el nombre del vetice justo antes de que la funcion visitar
retorne, se obtienen los nombres de los nodos impresos en orden topol ogico
inverso.
Ciclos en grafos: si en alg un momento se llega un nodo que ya haba sido
visitado, entonces hay ciclos (ya que el nodo no es el ancestro directo en el
arbol)
Generaci on aleatoria de laberintos: si el orden en que se visitan los vertices
vecinos se hace aleatorio, el resultado ser a un laberinto aleatorio, suponiendo
que cada vertice equivale a una posicion del laberinto.
3.3 B usqueda en amplitud (BFS - Breadth First
Search)
En este metodo de b usqueda se exploran todos los hijos de un nodo antes de
explorar a sus descendientes (hacer un dibujo)
Su implementacion se basa en utilizar una cola en lugar de una pila para
almacenar los nodos a un sin explorar.
Cola c(|V|);
void visitar(int k)
{
nodo * t;
c.push(k);
while(!c.empty())
{
k = c.pop();
val[k] = id++;
for(t = ady[k]; t!=null; t = t->prox)
if (val[t->v] == -1)
{
c.push(t->v);
val[t-v] == -2; // <- ojo explicar esto!
}
}
}
Es util cuando se esta buscando la mejor solucion a un problema. Cuando
el nodo solucion es encontrado, estamos seguros de que es la mejor solucion (si
evaluamos las soluciones seg un el n umero de nodos recorridos).
32
Por ejemplo: se tienen 3 vasos cada uno con una medida distinta. Por ejemplo,
7 lt, 11 lt y 5 lt. Como obtener k litros exactos?
Flujo maximo en redes: dado un grafo que representa un sistema de ujo
(por ejemplo tuberas), cu al es la cantidad m axima de ujo posible?. La red se
dene como un grafo dirigido con dos vertices especiales: la fuente, y el pozo.
Los pesos de los arcos representan la capacidad m axima de una tubera. El ujo
se dene como un conjunto de pesos en los arcos tal que el ujo del arco no
sobrepasa a la capacidad de ese arco. El valor del ujo de la red es igual al ujo
que sale de la fuente o que entra en el pozo.
El metodo de Ford-Fulkerson desarrollado en 1962 permite incrementar un
ujo dado siempre que esto sea posible, y est a basada en la siguiente propiedad:
si todos los caminos desde la fuente hacia el pozo tienen un arco de ida lleno o
un arco de vuelta vaco, el ujo es maximo.
En cada iteraci on se verica si existe un camino desde la fuente hasta el pozo,
y si existe se aumenta el ujo en cada arco de ese camino seg un el mnimo ujo
disponible en todos los arcos del camino. Notese que es posible recorrer arcos
en sentido contrario, rest adole al valor del ujo del arco. El problema que surge
es cu al camino tomar?. Edmonds y Karp sugierieron en 1972 tomar el camino
mas corto disponile en cada momento, buscandolo por medio de un BFS. De
esta forma, apenas se alcanza el pozo, ya el camino fue encontrado. El algoritmo
de Edmons-Karp tiene O([V [[E[
2
)
int C[N][N]; // Matriz de capacidad
int F[N][N] = {0,...,0}; // Matrix de flujo
int P[N];
int s,t; // fuente y pozo
int f = 0;
int m;
do{
P = bfs(s,t,m);
if(m)
{
f += m;
v = t;
while(v!=s)
{
u = P[v];
F[u][v] += m;
F[v][u] -= m;
v = u;
}
33
}
} while(m!=0);
function bfs(int s, int t, int &f) // f es el flujo encontrado
{
int P[N]; // arreglo de indices de antecesores
int M[N]={0,...0}; // flujo encontrado en cada nodo
queue<int> Q;
for(int u=0; u<n; u++) P[u] = -1;
M[s] = INF;
P[s] = -2;
Q.push(s);
while(!Q.empty())
{
u = Q.pop();
for(nodo * n = ady[u]; n!=NULL; n=n->prox)
{
v = n->v;
if (P[n->v]==-1 && C[u][n->v]-F[u][n->v]>0)
{
P[->v] = u;
M[v] = min(M[u], C[u][n->v] - F[u][n->v]);
if(v!=t)
Q.push(v);
else
{
f = M[t];
return P;
}
}
}
}
f = 0;
return P;
}
3.4 Otros problemas de Grafos
Vericacion de planaridad: el problema de planaridad se reere a la posibili-
dad de dibujar un grafo sin que arcos se crucen. Es un problema importante que
aparece entre otros, en aplicaciones de dise no de circuitos integados. El algoritmo
de Hopcroft y Tarjan para vericar planaridad es de O([V [ +[E[) basado en DFS.
Isomorsmo de grafos: dos grafos son isomorfos si puede encontrarse una
34
transformaci on de las etiquetas de los nodos de uno de los grafos a las etiquetas
del otro grafo, de forma tal que los dos grafos sean identicos. No se ha demos-
trado que el isomorsmo de grafos sea P o NP. La forma trivial de resolver el
problema consiste en encontrar todas las posibles transformaciones de los vertices
de un grafo a los vertices del otro grafo y vericar si los grafos son iguales. Este
algoritmo es claramente de O(N!), ya que hay N! combinaciones distintas. Si
el grafo es planar (puede dibujarse sin que existan arcos que se intersecten con
otros arcos) existe una solucion de O(N).
Clausura transitiva:
Dado un grafo G = (V, E), la clausura transitiva de G es un grafo G+ = (V, E+)
en donde E+ es el conjunto de arcos (u, v) con peso w si el camino mnimo de u
a v en G tiene peso w. La forma mas conocida de calcular la clausura transitiva
es con el algoritmo de Floyd-Warshall.
El algoritmo de Floyd-Warshal esta basado en la formula recursiva siguiente:
caminoMasCorto(u, v, k) =

arco(u, v) si k = 0
min(caminoMasCorto(u, v, k 1), en caso contrario
caminoMasCorto(u, k, k 1)+
caminoMasCorto(k, v, k 1))
en donde u, v son los vertices y el parametro k indica que unicamente se
utilizan vertices del conjunto 1, . . . k para construir el camino entre u y v.
De aqu es facil obtener un algoritmo de orden O(N
3
) que itera sobre cada
par de vertices, usando k = 0, . . . , N. Suponiendo que la matriz de adyacencia
contiene el peso del arco si existe el arco o en caso contrario:
for(k=1; k<N; k++)
for(u=0; u<N; u++)
for(v=0; v<N; v++)
A[u][v] = min(A[u][v], A[u][k] + A[k][v]);
35
Captulo 4
Divide y venceras
Es una tecnica de dise no de algoritmos que consiste en despomponer los casos
complejos en casos mas peque nos y faciles de resolver, y luego combinar las
soluciones obtenidas para construir la solucion del problema original
4.1 Ejemplo: multiplicacion de enteros muy gran-
des
4.2 Exponenciacion
Como calcular ecientemente a
k
? La forma natural de resolver esta operacion
es realizando k 1 multiplicaciones. Sin embargo, esto puede mejorarse vien-
do que cuando k es par, a
k
= (a
k/2
)
2
. De esta forma podemos plantear la relaci on:
a
k
=

1 si k = 0
(a
k/2
)
2
si k es par
a (a
k1
) si k es impar
Por ejemplo, si queremos calcular a
1
7, podemos ver que a
17
= a a
16
.
Luego a
16
= (a
8
)
2
, y a
8
= (a
4
)
2
, . . . . Finalmente, deberamos calcular a
1
(una
multiplicaci on), a
2
(dos multiplicaciones), a
4
(tres multiplicaciones), a
8
(cuatro
multiplicaciones) y a
16
(cinco multiplicaciones) para nalmente obtener a
17
(seis
multiplicaciones)
36
4.3 QSort
4.4 B usqueda de la mediana
El calculo de la mediana requiere ordenar los elementos (O(nlog n)) y luego
seleccionar el elemento central. Otro problema parecido es el problema de
seleccio el cual consiste en conseguir el n-esimo elemento mas peque no de un
arreglo. Si existira una funci on seleccionar(A, n) que resolviera este problema,
calcular la mediana se reducira a una llamada a esta funcion. Curiosamente,
la funci on seleccionar tambien puede construirse en base a la funci on mediana.
Recordemos que para Qucksort se usa una funci on llamada pivote(A, p, k, l)
el cual utiliza al elemento p como pivote del arreglo A, haciendo que los elementos
menores a p esten en las posiciones 0 . . . k 1, y los elementos mayores en las
posiciones l . . . n 1. Ahora, si buscamos el n-esimo elemento mas peque no de
A, pueden suceder tres casos:
1. n = k: el elemento buscado es p!
2. n < k: el elemento buscado esta en la mitad izquierda de A
3. n > l: el elemento buscado esta en la mitad derecha de A
Una forma iterativa de resolver este problema es:
funcion seleccion(A,s)
i=0; j=n-1;
mientras verdad
p = mediana(A[i..j])
pivote(A[i..j],p,k,l)
si s<k entonces
j=k; // buscar ahora en el lado izq
sino si s>l entonces
i=l; // buscar en el lado derecho
sino
retornar p; // bingo!
fsi
fmientras
ffuncion
Este algoritmo tiene el problema basico de que cada llamada a mediana es
de O(nlog n), sin embargo, esto puede mejorarse bastante si nos damos cuenta
de que el lugar de seleccionar p=mediana, podemos seleccionar p = A[i] en cada
iteraci on y el algoritmo funcionar a igualmente, ya que cualquier elemento puede
ser pivote, obteniendo un algoritmo que en el peor caso es de O(n
2
), pero en
tiempo promedio tiene Onlog n al igual que Quicksort.
37
4.5 Multiplicacion
La multiplicaci on tradicional de n umeros enteros, por ejemplo 324 1390, tiene
O(n
2
) (n es el n umero de dgitos en los n umeros). Sin embargo esto puede
mejorarse, como se vera en el ejemplo:
0324 1390, podemos separarlo en: w = 03, x = 24, y = 13, z = 90. Ahora,
podemos ver que 324 = 10
2
w +x y 1390 = 10
2
y +z. Ahora bien:
324 = (10
2
w +x) (10
2
y +z) = 10
4
wy + 10
2
(wz +xy) +xz
Ahora hagamos r = (w +x) (y +z) = wy +wz +xy +xz
Podemos ahora denir tambien:
p = wy
q = xz
r = (w +x)(y +z)
Y nalmente el resultado lo podemos calcular como: 324 1390 = 10
4
p +
10
2
(r p q) +q
De 16 multiplicaciones hemos reducido a 3 multiplicaciones (obviando las
multiplicaciones por potencias de 10 que en realidad son desplazamientos). En
general, si los n umeros tienen n dgitos, podemos separarlos en n umeros de
n
2
dgitos, y cada una de las 3 multiplicaciones que aparazcan pueden de nuevo
calcularse por el mismo metodo, hasta llegar a alg un caso base (por ejemplo
multiplicaciones de dos dgitos las cuales se podran almacenar en una matriz o
n umeros de 16 bits que pueden multiplicarse directamente)
4.6 Multiplicacion de Strassen
Sean A y B dos matrices de n n a multiplicar, y sea C = AB. El algoritmo
clasico de multiplicacion de matrices es de O(n
3
). Strassen logro mejorar este
tiempo a nales de los 60. El mismo principio anterior puede tambien aplicarse a
matrices. Supongamos que A y B los separamos en cuatro submatrices A
11
, B
22
A =

A
1,1
A
1,2
A
2,1
A
2,2

, B =

B
1,1
B
1,2
B
2,1
B
2,2

, C =

C
1,1
C
1,2
C
2,1
C
2,2

Calcular los C
i
M
1
:= (A
1,1
+A
2,2
)(B
1,1
+B
2,2
)
M
2
:= (A
2,1
+A
2,2
)B
1,1
M
3
:= A
1,1
(B
1,2
B
2,2
)
M
4
:= A
2,2
(B
2,1
B
1,1
)
M
5
:= (A
1,1
+A
1,2
)B
2,2
M
6
:= (A
2,1
A
1,1
)(B
1,1
+B
1,2
)
M
7
:= (A
1,2
A
2,2
)(B
2,1
+B
2,2
)
Ahora:
C
1,1
= M
1
+M
4
M
5
+M
7
C
1,2
= M
3
+M
5
C
2,1
= M
2
+M
4
C
2,2
= M
1
M
2
+M
3
+M
6
38
Este algoritmo permite mejorar de O(n
3
) a O(n
2,807
). Sin embargo se han
hecho mejoras sucesivas usando enfoques parecidos. Actualmente el algoritmo
CoppersmithWinograd (2008) permite multiplicar matrices con O(n
2,376
). Sin
embargo, para realmente obtener ventajas al multiplicar matrices estas deben
ser grandes
39
Captulo 5
Algoritmos voraces
(Greedy)
Son algoritmos miopes que toman decisiones basados en informaci on local sin
tomar en cuenta los efectos que estas decisiones tendr an en el futuro. Suelen ser
faciles de implementar y cuando funcionan son muy ecientes.
Pueden utilizarse cuando el problema es optimizar alguna funci on. Se dispone
de un conjunto de candidatos a pertenecer a la solucion. Cuando el algoritmo
avanza se van formando dos conjunto: candidatos en la solucion y candidatos
fuera de la soluci on. Existe una funci on que indica si un conjunto de candidatos
es una solucion. Otra funcion indica si es factible agregar un candidato a la
solucion. La funcion de seleccion indica cual es el candidato mas prometedor.
La funcion objetivo retorna el valor de la solucion hallada.
Funcion voraz(C:conjunto):conjunto
// C es el conjunto de candidatos
S=vacio
Mientras C<>vacio y no solucion hacer
X=seleccionar(C)
C=C - {X}
Si factible(S U {X}) entonces S=S U {X}
Fmientras
Si solucion (S) retornar S
Sino retornar vacio
Ejemplo: dar vuelto... funciona por ejm. Para 100, 25, 10, 5, 1... (cantidad
ilimitada)
no funciona para: 7, 3, 2 (por ejm.) dando vuelto 11
40
Candidatos: conjunto de monedas Funcion de solucion: Es factible: Selecci on:
Funcion objetivo: evalua la solucion que se obtuvo
Kruskal:
Algoritmo para encontrar el arbol de expansi on mnimo de un grafo G = (V,
E).
Kruskal(Grafo G)
{
T= Vacio // arbol resultado
mientras no arbol completo hacer
e = (v, w) = arco de costo minimo
Si T U e no tiene ciclos entonces
T = T U e
Fsi
fmientras
}
Orden: O(E) * (O(tiene ciclos) + O(arco de costo mnimo))
Si se utiliza un Heap:
O(arco de costo mnimo) = O(1).
Sacar el arco e del Heap O(n log n) = O(log E)
Pero
Es necesario crear el Heap H al comienzo:
H= Vaco
e E hacer
H.insertar(e);
O([E[log([E[))
Luego el orden de Kruskal es:
O([E[log([E[)) +O([E[) (O(tieneciclos) +O(log[E[))
Como saber ecientemente si el grafo tiene ciclos?
Metodo 1: DFS (sumamente ineciente)
Metodo 2:
41
Sea D un Conjunto disjunto en donde inicialmente hay V conjuntos
disjuntos (cada componente conexa es un conjunto disjunto).
Vericar si T e tiene ciclos es equivalente a vericar si v, w est an en el mismo
conjunto o son disjuntos. En caso de que sean disjuntos unir estos conjuntos.
Esto se reduce a una operaci on de uni on-pertenencia de conjuntos disjuntos.
En la practica hacer N operaciones de Union-pertenencia es de O(N).
Como saber si el arbol esta completo?
Un arbol de n vertices tiene n 1 arcos
Algoritmo nal:
Kruskal(Grafo G)
{
N = |V|-1; // numero de arcos que quedan pos insertar
T = Vacio // arbol resultado
H = Vac
1
2
o // H es un Heap
e E hacer
H.insertar(e);
Inicializar(D);
mientras N>0 hacer
e = (v, w) = H.pop();
si D.UnionPertenencia(v, w) entonces
T = T U e;
N = N-1;
fmientras
}
Luego, el algoritmo de Kruskal es de
O([E[log([E[)) +O([E[) +O([E[ log[E[)) = O([E[ log[E[)
Disjktra
S: camino
D[i]: camino mas corto hasta ahora al v
1
2
rtice i
C[i,j]: matriz de adyacencia
P: camino
Disjktra
{
42
S:= {1}
For i:=2 to n do
D[i]:=C[1,i]; // infinito si C[1,i] == 0
P[i] := 1
For(i=1 to n-1)
{
elige un vertice w enV-S tal que D[w] sea m
1
2
nimo
agrega w a S
for cada vertice v en V-S hacer
D[v]:=min(D[v],D[w]+C[w,v])
Si D[w] + C[w,v] < D[v] ent.
P[v] := w
}
}
5.1 Algoritmo A*
Casi cualquier problema puede resolverse (dados recursos innitos) usando un
programa de b usqueda, siempre que se encuentre una forma de describir el
problema en terminos de estados. Cada estado es un nodo de un arbol, en
donde el comienzo del problema se encuentra en la raz y hay que buscar un
camino hasta el nodo soluci on. El mayor problema es que incluso para problemas
peque nos este arbol es muy grande para hacer una b usqueda. El algoritmo A*
usa heursticas para buscar en el arbol de la forma mas eciente posible. La
heurstica retorna una evaluaci on sobre que tan bueno es cada estado, para poder
dirigir la b usqueda mas facilmente. Puede verse como un BFS en el que no se
utiliza una cola para almacenar los nodos, sino una cola de prioridad, ordenada
por alg un valor que permita decidir cuales nodos son mas prometedores que otros.
Un ejemplo, el 8 puzzle. (DIBUJARLO) Existe 9!=362880 estados distintos,
y para encontrar la soluci on debe encontrarse una ruta a lo largo del arbol. Un
estado se puede representar simplemente como una lista de valores, por ejemplo:
(1,2, ,5,4,8,6,7,3). El objetivo es: (1,2,3,4,5,6,7,8, )
En cada estado se pueden aplicar reglas para pasar a otros estados. Las reglas
son sencillas.
Posiblse heursticas:
n umero de piezas en su lagar de destino
sum. De distancia entre cada pieza y su lugar destino.
43
Encontrar una ruta en un mapa. Identicar estado y reglas.
Implementacion
Hay que llevar el control de dos listas de estados (llamadas nodos). La primera
es la lista OPEN, que almacena los nodos que han sido generados, pero que no
han sido explorados. La segunda lista contiene los nodos que han sido generados
y explorados. En cada estado se almacenan posiblemente datos adicionales, as co-
mo un apuntador al padre para reconstruir la solucion. El nodo tiene ademas
un costo, el estimado de la heurstica, y un valor total f que es la suma de los
anteriores. El costo depende de problema, y representa lo que cuesta llegar hasta
ese estado a partir del inicio. Por ejemplo, para el caso del laberinto simple, su
valor es 1+el costo del nodo padre. La heurstica puede ser tan compleja como
sea necesario. Por ejemplo, distancia hacia el destino.
bool AStarSearch()
{
priorityqueue OPEN
list CLOSED
NODE node
node.application_stuff = (start conditions of application specific stuff)
node.cost = 0;
node.heuristic = GetNodeHeuristic( node )
node.f = node.cost + node.heuristic
node.parent = null
OPEN.insert(node)
while OPEN is not empty
{
node = OPEN.eliminar()
if node is a goal node
{
construct path (by following the parent pointers)
return success
}
NodePushSuccessors( OPEN, CLOSED, node ); // see below
push node onto CLOSED list // as we have now examined this node
}
44
// if we got here we emptied the OPEN list and found no GOAL states
// the search failed
return FALSE
}
// Create the successors for the given node and add them to the open or closed
// lists as required
void NodePushSuccessors( priorityqueue OPEN, list CLOSED, parent_node )
{
por cada regla que se puede aplicar crear un nodonuevo
{
nodonuevo.cost = (application specific cost of this node) + parent_node.cost
nodonuevo.heuristic = GetNodeHeuristic(nodonuevo)
nodonuevo.f = nodonuevo.cost + nodonuevo.heuristic
// OJO: O(1) si es posible
if the nodonuevo is on CLOSED but the node on CLOSED has a lower f
{
continue;
}
// OJO: O(1) si es posible
if the nodonuevo is on OPEN but the node on OPEN has a lower f
{
continue;
}
remove nodonuevo from the OPEN list if it is on it
remove nodonuevo from the CLOSED list if it is on it
nodonuevo.parent = parent_node
OPEN.insert(nodonuevo)
}
}
Notas: Si la heurstica = 0 siempre, entonces unicamente el costo tiene in-
uencia, y el algoritmo se convierte en Dijkstra.
Si h(n) es siempre menor o igual al costo de llegar de n a la meta, A* consigue
un camino mas corto.
Si algunas veces h(n) al costo de llegar de n a la meta, A* puede que no
consiga siempre el mejor camino, aunque consigue una solucion mas rapido.
45
A* es usado como algoritmo de b usqueda de caminos en juegos diversos
juegos para guiar a los jugadores controlados por la computadora. Algunas
veces se incluyen incluso algunos cambios en el algoritmo para que los caminos
encontrados sean esteticamente mejores, por ejemplo, haciendo que las esquinas
sean mas suaves.
Volviendo a greedy...
Seleccion de actividades:
Dado un conjunto de actividades, indicando la hora de inicio y de comienzo de
cada una (S
i
, F
i
) respectivamente, seleccionar la cantidad m axima de actividades
que pueden realizarse. Dos actividades i, j son compatibles si S
j
F
i
o S
i
F
j
,
e incompatibles en caso contrario.
Tip:, ordenar por hora de nalizacion
Asignacion de actividades usando la menor cantidad de salas:
Dado un conjunto de actividades, especicadas de la misma manera, intentar
asignar salas en las cuales estas se llevar an a cabo, minimizando la cantidad de
salas necesarias.
Se puede crear un grafo en donde cada actividad es un vertice, y existen arcos
entre cada par de actividades incompatibles. Este grafo es llamado grafo de
intervalos. Y el problema original se puede reducir a buscar como colorear los
vertices del grafo de forma tal que no existan dos vertices conectados con un
arco que tengan el mismo color.
46
Captulo 6
Algoritmos probabilistas
Son algoritmos que est an basados en probabilidades para tomar decisiones. Mu-
chas veces, cuando un algoritmo debe tomar una decisi on, si el costo asociado a
la toma de decisi on es muy grande, es preferible tomar la decisi on aleatoriamente.
Existen tres tipos de algoritmos probabilistas:
Algoritmos numericos: producen un intervalo de conanza de la forma con
una probabilidad de 0.9, la respuesta correcta es 59 3. Mientras mas
tiempo se ejecute el algoritmo menor ser a el intervalo de conanza, por lo
que la respuesta es m a s exacta.
Algoritmos de Monte Carlo: Dan una respuesta exacta con bastante proba-
bilidad, pero a veces proporcionan una respuesta inexacta. El problema
es que, por lo general, no se puede decidir si la respuesta es correcta o
no. Se puede reducir la probabilidad de que la respuesta sea incorrecta,
aumentando el tiempo de ejecucion del algoritmo.
Algoritmos de Las Vegas: l algoritmo reconoce cuando una respuesta es
incorrecta, por lo que ignora ese tipo de respuestas, ejecut andose de nuevo.
Por ejemplo:
En que a no se descubrio America?
Algoritmo numerico: Entre 1490 y 1500, entre 1485 y 1495, entre 1491 y 1501,
entre 1480 y 1490, entre 1489 y 1499, . . .
Algoritmo de Monte Carlo: 1492 1492 1503 1492 1492 1492 2011 1492 1492 . . .
Algoritmo de Las Vegas: 1492 1492 Perd
1
2
n! 1492 1492 1492 perdon! . . .
47
Algoritmos probabilistas numericos:
Buon
Integracion numerica:(es bastante malo...) Integracion de montecarlo. Suele
usarse para integrales de 4 o + dimensiones . . .
Integral(f, n, a, b)
{
para i=1 hasta n hacer
x = U(a,b)
suma=suma + f(x)
fpara
return(b-a)*(suma/n)
}
Algoritmos de Monte Carlo
Teorema menor de Fermat: Sea n primo. a
n1
modn = 1, para cualquier a
1 a n 1
El contrarecproco es: Sean a, n: si a
n1
modn ,= 1, ent. n no es primo
Function Fermat(n)
A = U(1, N-1)
Si a^(n-1)%n==1 ent. return 1
Return 0
Si retorna falso n no es primo
Si retorna verdad No se puede concluir nada!
Sin embargo, la probabilidad promedio de error, en caso de que retorne
verdad, es de 0,033 (para n < 1000)... es incluso menor para n umeros mayores
Alg. De Las Vegas: Ejm:
8 reinas.
Generacion de n umeros pseudo-aleatorios:
Los generadores de n umeros pseudo-aleatorios son elementos de software o
de hardware que permiten generar secuencias de n ueros que parecen aleatorias.
Existen algunos procesadores que tienen una instrucci on que retorna un n umero
pseudo-aleatorio, en base a la temperatura actual del procesador, aunque gene-
ralmente se hace va software.
48
Recurrencias lineales: Sean m, k n umeros enteros positivos, y los coecientes
a
i
0, 1, ..., m1, un generador recursivo m ultiple se dene como:
x
n
= (a
1
x
n1
+a
2
x
n2
+... +a
k
x
nk
)modm
u
n
=
xn
m
Para m primo, y secuencias bien seleccionadas de n umeros a
i
, esta secuencia
tiene un periodo m aximo de = m
k
1 n
1
2
meros antes de que la secuencia se
empiece a repetir. Seg
1
2
n Knuth, esto puede lograrse si existen
1
2
nicamente
dos valores a
i
distintos a cero, lo cual origina la recurrencia mas simple:
x
n
= (a
r
x
nr
+a
k
x
nk
)modm
El generador congruente lineal (GCL) cl
1
2
sico puede derivarse de aqu, para
el caso k=1:
x
i+1
= (ax
i
+c)modm
En particular, el caso del generador de n umeros aleatorios de C/C++ (rand).
Por ejemplo, para Visual C++, m = 0x7f o 32767!!!
Generadores de n umeros aleatorios portables: existe buena evidencia de que
tomando c = 0 en un GCL los resultados son tan buenos como los obtenidos si
c ,= 0. Esto es: x
i+1
= ax
i
modm
. Park y Miller proponen usar a = 7
5
= 16807 y m = 2
3
1 1 = 2147483647. Se
ha demostrado que estos valores proveen n
1
2
meros aleatorios buenos, pero este
algoritmo es poco portable ya que ax
i
muchas veces requiere 64 bits para su
c
1
2
lculo. Sin embargo, usando la factorizaci on de Schrage es posible sobrellevar
este problema:
m = aq +r, q = [m/a], r = mmoda
Puede demostrarse que si r < q y 0 < z < m1, entonces:
az mod m =

a(z mod q) r[z/q] (a(z mod q) r[z/q] > 0)


a(z mod q) r[z/q] +m (encasocontrario)
La implementaci on de este algoritmo en C (tomado de Numerical Recipes:)
PAG279
El periodo de ran0 es 2
31
2. Si se selecciona como semilla para ran0 el
n umero 0, toda la secuencia de n umeros aleatorios sera 0. Es por esto que se
realiza en XOR para permitir este valor como semilla. (por supuesto la semilla
ahora no puede ser 123459876), pero se ha que los programadores tienden a
usr 0 como semilla. El principal defecto de este algoritmo (ademas del tama no
demostradode su periodo) es que n umeros grandes tender an a estar seguidos de
n umeros muy peque nos. La rutina ran1 elimina este problema barajeando el
resultado de ran0. (PAG280)
49
ran1 para todos los tests estadsticos comunes para determinar n umeros
aleatorios, hasta que se generan mas de 10
8
n umeros (aprox. 5 % del periodo de
ran1 (10
1
60)).
Cuando se requieren periodos mas largos, LEcuyer propuso combinar dos
generadores de n umeros aleatorios con periodos mas cortos para obtener un
generador con un periodo igual al mnimo com un m ultiplo entre los periodos
de ambos generadores peque nos. Para mayor informacion buscar rand2 en Nu-
merical Recipes. Se piensa que para la precision que tienen los puntos otante
actualmente, rand2 genera n umeros aleatorios perfectos. La denicion de
perfecto en NR es que ellos pagaran 1000$ a cualquier persona que encuentre
un test estadstico que haga que rand2 falle.
50
Captulo 7
Programaci on dinamica
Es una colecci on de herramientas matem aticas usadas para analizar procesos de
tomas de decisiones.
La programaci on din amica toma ventaja de la duplicaci on de sub problemas
para resolver cada subproblema una sola vez, almacenando sus soluciones para
uso posterior. Es una tecnica bottom-up a diferencia de divide and conquer.
En muchas situaciones esto puede llevar a conseguir soluciones en tiempos
polin omicos aunque en otros casos puede ser necesario enumerar completamente
todas las posibles soluciones.
Principio de optimalidad
Es un principio que deben cumplir los problemas para poder ser resueltos
usando programacion dinamica.
la solucion optima a un problema es una combinacion de soluciones de
algunos de sus subproblemas.
Metodo basico
Plantear una solucion recurrente al problema
Convertir la solucion recurrente en una solucion en donde se combinan
soluciones a instancias peque nas del problema para obtener soluciones a
instancias mayores del problema
Idear una forma para almacenar los resultados previos
51
Figura 7.1:

Arbol de llamadas para Fib(6)
Otra forma de resolver este tipo de problemas es a traves de memorizacion,
que consiste en resolver el problema con una estrategia top-down, pero almace-
nando en alguna estructura de datos los resultados a subproblemas ya resueltos,
con el n de evitar calcular dos veces estos valores. El mayor problema de esta
estategia es que la cantidad de memoria requerida suele ser muy grande.
7.1 Fibonacci
La forma recursiva de implementar Fibonacci es implementando la formula
recurrente:
fib(n) =

0 x = 0
1 x = 1
fib(n 1) +fib(n 2) x > 1
La Figura 7.1 muestra el arbol de llamadas correspondiente a Fib(6):
El gran problema de la implementacion recursiva de Fibonacci es que crece
exponencialmente. Calcular fib(100) tomara a nos si el algoritmo se implementa
de esta forma. El problema principal con esta implementacion es que se deben
resolver varias veces problemas que fueron resueltos previamente. Usando pro-
gramacion dinamica, es posible implementar Fibonacci en O(N), simplemente
almacenando los dos valores anteriores de fib. Partiendo de fib(0) y fib(1), estos
valores se van combinando hasta obtener el resultado deseado.
int fib(int n)
{
if(n==0) return 0;
if(n==1) return 1;
int f0 = 0, f1 = 1, r;
for(int i=1;i<n;i++)
{
52
r = f1 + f0; // f1 += f0
f0 = f1; // f0 = f1 - f0
f1 = r;
}
return r;
}
7.2 Combinatoria
Una manera de calcular

n
m

es usando la ecuacion:

n
m

=
n!
(nm)!m!
Sin embargo, para valores grandes de n y m muchas veces se excede el lmite
de representaci on de la m aquina al calcular n!, aunque el resultado nal si pueda
representarse en la maquina. Por esto, suele utilizarse la siguiente formula de
recurrencia:

n
m

1 m = 0
1 n = m

n 1
m1

n 1
m

1 < m < n
Esta formula da origen al Triangulo de Pascal para calcular los factores del
Binomio de Newton:
n = 0: 1
n = 1: 1 1
n = 2: 1 2 1
n = 3: 1 3 3 1
n = 4: 1 4 6 4 1
n = 5: 1 5 10 10 5 1
n = 6: 1 6 15 20 15 6 1
La Figura ?? muestra el arbol de llamadas para

5
3

:
Si el algoritmo se implementa recursivamente, el tiempo para ejecutarlo
crecer a exponencialmente. La soluci on de nuevo es hacer una implementaci on en
donde se almacenen las soluciones a las instancias mas peque nas del problema.
int comb3(int n, int m)
53
Figura 7.2:

Arbol de llamadas para C
5,3
{
for(int i = 0; i <= n; i++)
{
_comb[0] = _comb[i] = 1; // marca los casos base
int ant = _comb[0];
for(int j = 1; j < i; j++)
{
int aux = _comb[j];
_comb[j] = ant + _comb[j];
ant = aux;
}
}
return _comb[m];
}
Esta implementacion es de O(NM).
Por ejemplo, intentar calcular

35
25

usando las tres versiones... que dife-


rencias habra en cada una.
7.3 Vuelto
Ya vimos que el problema de dar vuelto puede resolverse usando un algoritmo
voraz para algunos conjuntos monedas pero no para otros. Como puede plan-
tearse recursivamente una solucion para dar vuelto?
vuelto _ 0 = 0
vuelto (h:t) v = v < 0 : 999999
h > v : min (1+vuelto(h:t)(v-h)) (vuelto t v)
h <= v : vuelto t v
Este algoritmo efectivamente resuelve el problema, pero de una manera muy
ineciente. La razon de la ineciencia se deriva (de nuevo) del hecho de que se
54
calculan varias veces soluciones que ya han sido resueltas previamente.
Podra construirse una tabla en donde se almacenen las soluciones ya encon-
tradas? obviamente la respuesta es SI!
Podemos usar una tabla vue[K][V] (K es el n umero de monedas y V es el
vuelto que se desea entregar) en donde vue[i][j] representa la soluci on al problema
dar vuelto j usando las primeras i monedas del arreglo de monedas. Es obvio
que vue[i][0] = 0, y vue[0][j] = para j no m ultiplo de M[0].
Ahora puede construirse la tabla por las. La soluci on nal se encontrara en
la posicion vue[k 1][v]. Este algoritmo es de O(KV ).
7.4 KSP
El problema de la mochila o Knapsack Problem. Dados un conjunto de objetos
con peso w
i
y valor v
i
, seleccionar cu ales deben transportarse en una mochila con
capacidad m axima k, maximizando el total del valor transportado. Existen varias
versiones del problema: bounded (cuando hay un lmite en cuanto al n umero
de cada tipo de objetos), unbounded (cuando no hay un lmite), 0 1 (cuando
unicamente hay uno de cada tipo de objeto).
Unbounded: aqu podemos construir una f ormula recursiva para el c alculo
del KSP. ksp(i, j) es el maximo valor transportable cuando se han considerado
unicamente los primeros i objetos y la capacidad de la mochila es j. Podemos
ver que en general siempre podemos tomar la decision de poner o no poner el
objeto i. Si decidimos colocarlo, ahora quedar a en la mochila disponible j w
i
por lo que en ese caso ksp(i, j) = ksp(i, j w
i
) + v
i
. Si decidimos no colocar
el objeto, no se ve afectado ni el espacio disponible en la mochila ni el valor
transportado, pero hay que considerar que hacer con los otros i 1 objetos por
lo que en este caso ksp(i, j) = ksp(i 1, j). como decidir cual accion tomar?
Simplemente ksp(i, j) = max(ksp(i, j w
i
) +v
i
, ksp(i 1, j))
Para poder almacenar las soluciones previas podemos usar una tabla en
donde T[i][j] = ksp(i, j). Esta tabla se va llenando por las desde i = 0 hasta
i = n 1. La solucion nal queda almacenada en T[n 1][k]. Esta solucion es
de O(NK)
7.5 MaxSum
Dada una secuencia de n umeros enteros S
1
, . . . , S
n
, cual sera la subsecuencia
cuyos valores sumen un m aximo valor? Si todos los n umeros de la secuencia son
positivos, la suma maxima es la suma de todos los elementos de la secuencia,
55
pero cuando hay n umero involucrados, esto puede no ser cierto. La solucion de
O(N
2
) es trivial, y consiste en calcular todas las subsecuencias y ver cual es la
maxima.
Denamos M(j) como la suma m axima de todas las secuencias que terminan
en j. Por ejemplo, sis la secuencia es S = 3, 1, 2, 4, 1, 1, 4, 3, 1, 2, M(0) = 3,
M(1) = 4, M(2) = 2, M(3) = 4. Al nal queremos buscar el maximo de M(j).
Podemos ver que M(j) = max(M(j 1) +S[j], S[j]). Es decir, en cada posici on
hay dos opciones: comenzar una nueva secuencia en este punto (que termine en
j), o continuar con la secuencia anterior (que terminaba en j 1). Ojo, mejor
poner otro ejemplo, este no es muy bueno!!!
Piensen en como hacer lo mismo, ahora en una matriz.
7.6 Multiplicacion encadenada de matrices
La multiplicaci on de dos matrices A
nm
, B
mp
es una operaci on que involucra
n mp multiplicaciones simples (si se hace una implementacion trivial del
algoritmo de multiplicacion de matrices). Ahora bien, si deseamos multiplicar
varias matrices, por ejemplo A
53
B
39
C
92
, podemos hacerlo de varias
formas. En el caso del ejemplo es posible resolver (AB) C o A(B C).
Cu al de estas formas ser a m as conveniente? Obviamente aquella que involucre
la menor cantidad de multiplicaciones de enteros. En el primer caso, se necesi-
tan 135 multiplicaciones para resolver AB y luego 90 multiplicaciones para
(A B) C teniendo un total de 225 multiplicaciones. En el segundo caso se
necesitan 54 multiplicaciones para calcular B C y luego 30 multiplicaciones
para A(B C), para un total de 84 multiplicaciones.
Podemos enumerar las n matrices desde la 0 hasta la n 1. De esta for-
ma, para obtener la forma optima de multiplicar desde la matriz i hasta la
j podemos hacer la multiplicacion en j i puntos distintos. Por ejemplo, pa-
ra multiplicar desde la matriz i = 2 hasta la j = 5, podemos hacerlo as:
(A
2
) (A
3
A
4
A
5
), (A
3
A
4
) (A
4
A
5
), (A
2
A
3
A
4
) (A
5
), de tres
maneras distintas. Recursivamente debemos entonces calcular c omo multiplicar
optimamente cada una de las multiplicaciones que aparecen ah y luego que-
darnos con la mnima. Deniremos entonces una funcion M(i, j) que retorna
como calcular optimamente desde al matriz i hasta la j i j. Es obvio que
M(i, i) = 0 y M(i, i + 1) = fil(i) col(i) col(i + 1), en donde fil(i) es el
n umero de las de la matriz i, y col(i) es el n umero de columnas.
M(i, j) = min
j1
k=i
M(i, k) +M(k + 1, j) +fil(i)col(k)col(j)
Para la implementaci on puede hacerse una matriz M que se va llenando por
diagonales hasta llegar a M[0][n 1] en donde queda almacenado el resultado.
56
Es un algoritmo de O(n
3
)
7.7 LCS - Longest Common Substring
Dados un par de strings, encontrar la secuencia com un a ellos mas larga. Por
ejemplo, si S =xygxxzyfa y T =xyxzyfx, LCS(S, T) =xzyf. Para este
problema debemos ver que basta con encontrar el sujo com un mas largo para
todos los pares de prejos de los strings. Si S tiene n caracteres y T tiene m
caracteres, en total hay n prejos posibles para S y m para T. El n umero de
pares de prejos posibles asciende a n m.
Ahora bien, llamemos S
n
al prejo de S que contiene n caracteres. Comen-
zaremos por examinar el ultimo caracter de S
n
y T
m
. Si son iguales, entonces
LCS(S
n
, T
M
) = 1 + LCS(S
n1
, T
m1
). En caso de que sean distintos simple-
mente LCS(S
n
nT
m
) = 0. Para los casos base tenemos que LCS(S
i
, T
0
) =
LCS(S
0
, T
i
) = 0.
La implementaci on se lleva a cabo sobre una tabla en donde se va calculando
cada la (aunque cada columna tambien funciona). Idealmente se va llevando el
control sobre el mayor valor encontrado hasta el momento en la tabla.
7.8 String Distance
Dados un par de strings A, B, la distancia entre ellos (distancia de Levenshtein)
se dene como la menor cantidad de cambios que deben hacersele al string A para
convertirlo en el string B. Un cambio puede ser eliminar un caracter, agregar
un caracter, o modicar un caracter. Por ejemplo, el string A =casa puede
transformarse en azar haciendo tres cambios (eliminando la c, agregando la
r y cambiando la s por z.
Son posibles varios casos. Supongamos que n es el tama no de A y m el
tama no de B. Si n = 0 entonces sd(A, B) = m ya que hay que insertar los m
caracteres de B. Si m = 0, sd(A, B) = n ya que hay que insertar los n caracteres
de A. En cualquier otro caso debemos revisar el ultimo caracter del string. Si A[n-
1]==B[m-1] entonces los cambios hay que hacerlos en los caracteres anteriores
y sd(A, B) = sd(A1, B 1), en donde A1 indica el string A sin el ultimo
caracter. Cuando los ultimos caracteres son diferentes, el posible hacer cada una
de las tres acciones:
1. Modicar el caracter de B: en este caso estamos haciendo un cambio,
y debemos calcular la distancia entre los strings sin el ultimo caracter
sd(A, B) = 1 +sd(A1, B 1)
2. Eliminar el ultimo caracter de B: en este caso hay que contar un cambio
adicional, y quitarle el ultimo caracter a B: sd(A, B) = 1 +sd(A, B 1)
57
3. Insertar un nuevo caracter a B: hay que contar un cambio adicionar y
agregarle un caracter a B: sd(A, B) = 1 +sd(A1, B)
La implementacion consiste en una tabla T de (n + 1) (m+ 1) en donde
T[i][j] indica la distancia de Levenshtein entre los primeros i caracteres de A y
los primeros j caracteres de B.
7.9 LCS - Longest Common Sequence
Dado un conjunto (generalmente de dos) secuencias, encontrar la subsecuencia
com un a todas las secuencias. Por ejemplo, para las secuecias S = 5, 4, 6, 7, 3, 2, 7, 9, 2, 6, 4, 2
y T = 8, 4, 5, 6, 11, 7, 9, 6, 3, 14, 1, 2, la subsecuencia com un mas larga sera
4, 6, 7, 9, 6, 2.
Examinemos que sucede con el ultimo elemento de la secuencia. Usaremos
la notacion S
n
para denotar el prejo de S que contiene a los primeros n
elementos de la secuencia. Por ejemplo, S
1
= 5, S
3
= 5, 4, 6. Entonces, si
el ultimo elemento de S
n
es igual al ultimo elemento de T
m
, tenemos que
LCS(S
n
, T
m
) = LCS(S
n1
, T
m1
) + 1, ya que el elemento es com un a ambas
secuencias.
Cuando los ultimos elementos no coinciden, es necesario ver si se puede hacer
coincidir el ultimo elemento de S con la secuencia T o el ultimo elemento de T
con la secuencia S. Es decir, es necesario ver el valor de LCS(S
n
, T
m1
) y de
LCS(S
n1
, T
m
). Luego, si los ultimos elementos de la secuencia no coinciden:
LCS(S
n
, T
m
) = max(LCS(S
n
, T
m1
), LCS(S
n1
, T
m
))
7.10 Longest Increasing Sequence
Dada una secuencia de n umeros enteros, la secuencia incremental mas larga
es una subsecuencia de n umeros que estan ordenados ascendientemente. La
subsecuencia no es necesariamente contgua. Por ejemplo si la secuencia es
S = 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15, una posible secuencua incremen-
tal mas larga es 0, 2, 6, 9, 13, 15 con longitud 6, aunque no es la unica secuencia
incremental mas larga.
Denamos q
k
(S) como la secuencia incremental m as larga de S que termina
en S
k
. Supongamos que estamos considerando la posici on i de la secuencia. En
cada iteracion se debe tomar la decision sobre incluir o no s
i
a la secuencia
incremental. Podemos ver que q
k
(S) = max
i<k
(q
k1
(S)[S
i
< S
k
). Es decir, si
estamos considerando el elemento S
k
de la secuencia, debemos ver a cual de las
secuencias anteriores podemos concatenarle S
k
. Esto solo podremos hacerlo a
los valores S
i
< S
k
.
58
Captulo 8
Geometra Computacional
Geometra computacional se reere a un conjunto de algoritmos y estructuras
de datos para soportar la resolucion de diversos problemas geometricos.
8.1 Representaci on
Algunas primitivas geometricas comunes en 2D son las siguientes. Siempre es
posible llevarlas a 3D de ser necesario.
8.1.1 Puntos
struct Punto
{
float x, y;
};
8.1.2 Rectas
Aunque intuitivamente uno tiende a querer representar rectas como una pendiente
y una distancia (estilo y = mx +b), esto trae muchos problemas ya que existen
casos de borde (pendiente innita, problemas de redondeo) que son difciles de
atacar. La mejor forma de representar una recta es con la ecuaci on implcita de
la recta p = o +td, en donde o es un punto cualquiera que esta sobre la recta,
d es un vector (vector director de la recta), y t es un parametro que se mueve
entre y .
struct Recta
{
Punto origen;
Vector direccion;
};
59
8.1.3 Segmentos
Los segmentos se pueden representar como un par de puntos p
1
, p
2
.
struct Segmento
{
Punto p1, p2;
};
8.1.4 Polgonos
Un polgono de n lados se representa como un arreglo de puntos:
Punto poligono[n];
8.2 Interseci on de segmentos
Para intersectar segmentos primero debemos conocer el concepto de orientaci on
de puntos, que se reere a la forma en que tres puntos est an orientados entre s.
Tres puntos siempre pueden estar:
Sobre una misma recta
Orientados en sentido de las agujas del reloj (Clockwise)
Orientados en sentido contrario a las agujas del reloj (counterclockwise)
Ahora, para determinar si dos segmentos se intersectan veamos la Figura 8.1.
Podemos simplicar el problema viendo que L y S se intersectan siempre que
S
0
y S
1
no esten del mismo lado del segmento L y L
0
, L
1
tampoco estan del
mismo lado del segmento S. C omo saber si dos puntos est an del mismo lado de
un segmento? Veamos la Figura 8.2. Es claro que si los puntos L
0
, P, L
1
estan
ordenados en el mismo sentido que los puntos L
0
, Q, L
1
, entonces los puntos
estan del mismo lado del segmento L. El caso contrario puede observarse en la
gura 8.3.
Entonces, podemos resumir el algoritmo para intersectar segmentos de la
siguiente manera:
bool interseccion(segmento L, segmento S)
{
retornar !mismolado(L, S.p1, S.p2) && !mismolado(S, L.p1, L.p2;)
}
o lo que es lo mismo:
60
Figura 8.1: Interseccion de segmentos
Figura 8.2: Dos puntos del mismo lado del segmento
bool interseccion(segmento L, segmento S)
{
retornar ccw(L.p1, S.p1, L.p2)!=ccw(L.p1, S.p2, L.p2) &&
ccw(S.p1, L.p1, S.p2)!=ccw(S.p1, L.p2, S.p2);
}
Veamos ahora como calcular si tres puntos a, b, c estan orientados en ccw
o cw. Para esto analizaremos las pendientes de las rectas ab y ac. Recordemos
que la pendiente m
ab
=
byay
bxax
y m
ac
=
cyay
cxax
. Deniremos ahora d1x = b
x
a
x
,
d1y = b
y
a
y
, d2x = c
x
a
x
y d2y = c
y
a
y
. Analicemos que pasa cuando
m
ac
> 0 y m
ab
> 0. Tal como se muestra la gura, en este caso
8.3 Par de puntos mas cercano
Este problema consiste en determinar, de una nube de puntos, cual es el par
de puntos que estan mas cercanos entre s. La solucion directa a este problema
consiste en encontrar la distancia entre todo par de puntos, y quedarse con el
mnimo, con una complejidad de O(N
2
). Una soluci on Divide y Vencer as permite
obtener una solucion con O(N log(N)).
Podemos primero pensar en c omo resolver el problema en 1D. Es f acil ver que
no es necesario hacer N
2
comparaciones, sino que podemos resolve el problema en
tiempo lineal, ordenando los puntos y compar andolos unicamente con sus vecinos.
61
Figura 8.3: Dos puntos en lados distintos del segmento
Figura 8.4: Para ver como estan orientados tres puntos analizaremos las dos
rectas que ellos forman
Para la versi on 2D debemos ordenar los puntos a lo largo de una coordenada,
por ejemplo el eje X. Podemos entonces dividir los puntos en dos mitades con la
misma cantidad de puntos. Por ejemplo, tomando la mediana de los puntos (en
X) como punto separador. Si el punto mediana es P
m
, podemos entonces trazar
una recta y = P
m,x
como se ve en la Figura 8.5.
A continuaci on esta divisi on de hace recursivamente hasta llegar a un espacio
con solamente uno o dos puntos. Si hay dos puntos, el par m as cercano son esos
dos puntos, en caso de que haya un solo punto debe retornarse innito ya que
no se ha encontrado soluci on. A partir de ah la recursi on se va devolviendo. En
cada caso es posible que el par mas cercano este del lado izquierdo, del lado
derecho y que se forme entre un punto del lado izquierdo y uno del lado derecho,
como se observa en la Figura 8.6. Este es el caso mas interesante que pasaremos
a describir a continuacion.
Primero debemos ver que los posibles puntos candidatos a formar un par
mas cercano pueden estar como m aximo a distancia = min(
1
,
2
) de la lnea
divisoria y = P
m,x
, ya que de estar mas lejos no podr an ser un par mas cercano
(al ser su distancia mayor a ). Ahora bien, es posible que todos los puntos
esten dentro de esta franja denida por y = P
m,x
. Es necesario entonces
tener los puntos ordenados no unicamente por coordenada X sino tambien por
62
Figura 8.5: Division del plano en dos mitades iguales
Figura 8.6: Ejemplo en donde la distancia mnima es la union de un punto del
lado izquierdo y otro del lado derecho
coordenada Y . De esta forma, cuando estemos examinando un punto, para ver
si forma parte de un par mas cercano, podemos movernos hacia arriba o hacia
abajo en una distancia igual a . En base a esto, como podemos ver en la Figura
8.7, siempre habra un maximo de 5 puntos a examinar.
8.3.1

Arboles KD
Un arbol KD es una estructura de datos que separa el espacio para poder ubicar
rapidamente puntos en el espacio. Los arboles KD tienen puntos de K dimen-
siones. Un arbol KD subdivide el espacio a lo largo del eje X en un nivel y
en el proximo nivel a lo largo del eje Y , y as con cada uno de los ejes hasta
que vuelve a usar la primera coordenada. En cada nodo se almacena el punto
que se utilizo para dividir el espacio. La Figura 8.8 muestra la subdivision del
63
Figura 8.7: En el peor de los casos hay que examinar 5 puntos
espacio correspondiente luego de usar la mediana como punto de corte. El arbol
correspondiente se muestra en la Figura 8.9.
struct NodoKD
{
Punto P;
NodoKD * izq, Nodo KD * der;
};
Para construir el arbol KD se mantienen dos ordenamientos de los puntos,
tanto por coordenada X como por coordenada Y . Se toma la mediana de los
puntos (en la coordenada correspondiente) como punto de corte y se traza una
recta que pasa por ese punto. Luego se clasican los puntos que estan a la
izquierda y los que estan a la derecha y se hacen dos llamadas recursivas con
cada uno de estos conjuntos de puntos. Este procedimiento se repite hasta que
se llega a tener un solo punto.
8.3.2 B usqueda de punto mas cercano
Una aplicacion de arboles KD es dado un punto p cualquiera en el espacio,
buscar el punto perteneciente al arbol que esta mas cerca de p. Inicialmente se
64
Figura 8.8: Subdivision del espacio con un arbol KD
Figura 8.9:

Arbol KD correspondiente a la Figura 8.8
busca el punto en el arbol como se hara en una b usqueda binaria. Si el punto se
encuentra en el arbol, la b usqueda naliza. En caso contrario se marca el nodo
en que haya nalizado la b usqueda como punto m as cercano y se va subiendo en
el arbol. Cada vez que se sube en el arbol hay que ver si es posible que el nodo
actual este m as cerca que el que ya se haba encontrado, de ser as se reemplaza
este punto. Supongamos que en un momento estamos parados en un nodo N
con hijos N
i
y N
d
, y que acabamos de explorar el nodo N
d
. Primero vemos si N
est a m as cerca de P que M (M es el mnimo punto encontrado hasta ahora). De
ser as, hacemos M = N para actualizar el mnimo. Ahora bien, es posible que
el punto m as cercano este del lado N
i
que no hemos explorado, pero es siempre
necesario explorar esta rama del arbol?

Unicamente si la frontera entre N
i
y N
d
esta dentro de un crculo de radio dist(M, P) centrado en P.
8.3.3 B usqueda por rango
Otra aplicacion com un de arboles KD es la b usqueda por rango lo cual buscar
responder a la pregunta cuales o cuantos puntos estan dentro de un cierto
rango? Por ejemplo, en la Figura 8.10 se muestra una posible b usqueda por
rango. En cada nodo hay que ver si sus hijos est an dentro del rango comparando
los lmites del rango con la lnea divisoria. Si alg un hijo no est a dentro del rango
no debe hacerse la llamada recursiva con ese nodo. En caso contrario se hace la
65
Figura 8.10: B usqueda por rango con un KD-Tree
llamada. Para el ejemplo inicialmente podemos ver que debemos explorar el lado
izquierdo del arbol, pero en la segunda llamada hay que explorar ambos lados
(nodos 4 y 5). En el nodo 4 tambien hay que llamar con ambos hijos al igual
que con el nodo 5. Ahora bien, el hecho de que hagamos una llamada recursiva
no implica que el punto asociado a ese nodo este dentro del rango. Siempre es
necesario vericar esto.
8.4 Capsula convexa
La c apsula convexa (convex hull) de un conjunto de puntos es el polgono convexo
mas peque no que encierra a los puntos.
8.4.1 Gift Wrap
Este metodo es el mas natural para el humano ya que funciona como un humano
buscara la capsula convexa. Partiendo de un punto que se sabe que esta en la
c apsula convexa, se intenta envolver sistem aticamente al conjunto de puntos. Este
punto que se sabe que est a en la c apsula convexa puede ser cualquiera con una
coordenada extrema, por ejemplo, el que tenga la menor coordenada en y, como se
muestra en la Figura 8.11. La Figura 8.12 muestra varias etapas de este algoritmo.
En cada etapa del algoritmo debemos calcular cu al es el punto que forma el
menor angulo con el ultimo segmento que hemos calculado de la c apsula convexa.
Debido a que el c alculo de angulos generalmente involucra costosas operaciones
trigonometricas, muchas veces propensas a casos de borde como divisiones por
cero, se usa una aproximacion al angulo, que se muestra a continuacion:
float theta(Punto & p1, Punto & p2)
66
Figura 8.11: Seleccion del punto inicial
Figura 8.12: Varias etapas del algoritmo de Gift Wrap
{
int dx,dy,ax,ay;
float t;
dx = p2.x - p1.x; ax = abs(dx);
dy = p2.y - p1.y; ay = abs(dy);
t = (ax+ay==0)?0:(float)dy/(ax+ay);
if(dx<0) t = 2-t; else if (dy<0) t = 4+t;
return t*90.0;
}
Este algoritmo retorna una aproximaci on al angulo formado por los segmentos
OP
1
y OP
2
, como se muestra en la gura 8.13. Retorna un valor entre 0 y 360
que se aproxima bastante al angulo real. Usando esta funci on es posible entonces
buscar en cada etapa del algoritmo de Gift Wrap el punto con el menor angulo
respecto al punto actual. Con esto obtenemos un algoritmo de O(N
2
)
8.4.2 El metodo de exploraci on de Graham
Este algoritmo es mucho mas eciente que el algoritmo de Gift Wrap. Se basa
en el hecho de que en un polgono convexo, si se siguen los puntos en orden,
67
Figura 8.13: La funci on theta calcula el angulo formado por los segmentos OP
1
y OP
2
Figura 8.14: Izquierda: los puntos son ordenados de acuerdo al angulo que
forman con respecto al punto pivote. Derecha: primeras iteraciones de Graham.
Todos los puntos son agregados al convex hull ya que todos los cambios de
direccion son hacia la izquierda (ccw)
todos est an orientados de la misma forma (o todos en sentido horario o todos en
sentido antihorario).
El algoritmo comienza igual que el algoritmo de Gift Wrap, buscando un
punto que est a seguro sobre la c apsula convexa, por ejemplo, el punto con menor
coordenada y (punto pivote). En caso de existir varios puntos con coordenada
y mnima, se busca el que tenga tambien coordenada x mnima. Una vez iden-
ticado este punto, todos los demas puntos del conjunto seran ordenados de
acuerdo al angulo que forman respecto al pivote (usando la funci on theta). Una
vez ordenados los puntos, el primero de estos puntos estar a siempre dentro de la
c apsula convexa (es f acil darse cuenta de esto). A partir de ah en cada iteraci on
se toman en cuenta los ultimos dos puntos agregados a la capsula convexa y
el siguiente punto (seg un el orden). Si estos tres puntos estan ordenados en
forma antihoraria (ccw), se sigue considerando que todos forman parte de la
c apsula convexa. Pero si en alg un momento los puntos est an ordenados en sentido
horario (cw), se detecta un punto que fue agregado a la capsula convexa, pero
no pertenece a ella, por lo que debe ser removido.
La complejidad del algoritmo recae en el algoritmo para ordenar los puntos
(O(N log(N)). Una vez que los puntos est an ordenados, el recorrido el lineal ya
que cada punto es agregado m aximo una vez o sacado de la c apsula convexa una
vez. Al nal el algoritmo es entonces de O(N log(N)) +O(N) = O(N log(N))
68
Figura 8.15: Izquierda: Se encuentra un cruce a la derecha (cw) por lo que el
punto es removido del convex hull. Centro: otro punto mas es removido del
convex hull debido a otro cruce a la derecha (cw). Derecha: el proximo punto
es agregado al convex hull ya que el proximo cruce es a la izquierda (ccw)
69
Bibliografa
[1] Alfred Aho, Jerey Ullman, and John Hopcroft. Data structures and Algo-
rithms. Addison Wesley, 1st edition edition, 1983.
[2] Gilles Brassard. Fundamentos de Algoritmia. Prentice Hall, 1ra edicion
edition, 2000.
[3] Thomas Cormen. Introduction to Algorithms. The MIT Press, 3rd edition
edition, 2009.
[4] Donald Knuth. Art of Computer Programming. Addison Wesley Professional,
3rd edition edition, 1998.
[5] William Press, Brian Flannery, Saul Teukolsky, and William Vetterling.
Numerical Recipes in C. Cambridge University Press, 2nd edition edition,
1992.
[6] Robert Sedgewick. Algorithms. Addison Wesley Professional, 4th edition
edition, 2011.
[7] Steve Skiena. The Algorithm Design Manual. Springer, corrected edition
edition, 1997.
70