Professional Documents
Culture Documents
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
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
. 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
. 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 =
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