Sei sulla pagina 1di 454

Algortmica

Rosa Ma Jimnez Conrado Martnez


U. Politcnica Catalunya

27 de septiembre de 2013

Queda prohibida la reproduccin parcial o la modicacin, por cualesquiera medios, de estas transparencias sin el consentimiento expreso de los autores. Puede accederse a la copia principal de este documento electrnico en la web, enlazarse, realizarse cuntas copias se desee y difundirlas pblicamente a travs de la web u otros medios, siempre y cuando no se altere su contenido en absoluto. El consentimiento para la reproduccin, modicacin o uso total o parcial de estas transparencias con nes exclusivamente docentes est automticamente garantizado, tanto en la Univ. Politcnica de Catalunya (UPC) como en cualquier otra institucin de enseanza, superior o no, de cualquier pas y siempre y cuando: a) se respete el debido crdito a los autores; b) los derechos de copia, reproduccin, modicacin y uso total o parcial aplicables a los materiales derivados sean los mismos aqu expresados. Por favor, envie un e-mail a alg -at- lsi.upc.edu si est interesado en la reproduccin, modicacin o uso total o parcial de estas transparencias con cualquier n, includos los nes docentes.

Parte I Repaso de Conceptos Algortmicos


1

Anlisis de Algoritmos Divide y vencers rboles binarios de bsqueda rboles balanceados (AVLs) Tablas de Hash Colas de prioridad Grafos y Recorridos

Anlisis de Algoritmos Divide y vencers rboles binarios de bsqueda rboles balanceados (AVLs) Tablas de Hash Colas de prioridad Grafos y Recorridos

Eciencia de un algoritmo = consumo de recursos de cmputo: tiempo de ejecucin y espacio de memoria Anlisis de algoritmos ! Propiedades sobre la eciencia de algoritmos
Comparar soluciones algortmicas alternativas Predecir los recursos que usar un algoritmo o ED Mejorar los algoritmos o EDs existentes y guiar el diseo de nuevos algoritmos

segn el caso):

A su eciencia o coste (en tiempo, en espacio, en nmero de operaciones de E/S, etc.) es una funcin T de A en N (o Q o R,
T :A!N ! T ( )

En general, dado un algoritmo A cuyo conjunto de entradas es

Ahora bien, caracterizar la funcin T puede ser muy complicado y adems proporciona informacin inmanejable, difcilmente utilizable en la prctica.

Sea An el conjunto de entradas de tamao n y Tn funcin T restringida a An . Coste en caso mejor:

: An ! N la

Tmejor (n) = m nfTn ( ) j 2 An g:


Coste en caso peor:

Tpeor (n) = m axfTn ( ) j 2 An g:


Coste promedio:

Tavg (n) =
=

2A
X

Pr( ) Tn ( )

k0

k Pr(Tn = k):

Para todo n  0 y para cualquier 2 An

Tmejor (n)  Tn ( )  Tpeor (n):


2

Para todo n  0

Tmejor (n)  Tavg (n)  Tpeor (n):

Estudiaremos generalmente slo el coste en caso peor:


1

Proporciona garantas sobre la eciencia del algoritmo, el coste nunca exceder el coste en caso peor Es ms fcil de calcular que el coste promedio

Una caracterstica esencial del coste (en caso peor, en caso mejor, promedio) es su tasa de crecimiento

Ejemplo
1 2

Funciones lineales: f (n) = a n + b ) f (2n)  2 f (n) Funciones cuadrticas: q(n) = a n2 + b n + c ) q(2n)  4 q(n)

Se dice que las funciones lineales y las cuadrticas tienen tasas de crecimiento distintas. Tambin se dice que son de rdenes de magnitud distintos.

log2 n

log2 n

2n

1 2 3 4 5 6
` `

2 4 8 16 32 64
N

2 4 8 8 16 64 24 64 512 64 256 4096 160 1024 32768 384 4096 262144


L C Q

6;87 1010 4;72 1021


E E

4 16 256 262144

+1

2N

2(L + N )

4C

8Q

Los factores constantes y los trminos de orden inferior son irrelevantes desde el punto de vista de la tasa de crecimiento: p p.e. 30n2 + n tiene la misma tasa de creciemiento que 2n2 + 10n ) notacin asinttica Dada una funcin f : R+ ! R+ la clase O(f ) (O-grande de f ) es O(f )=fg:R+ !R+ j 9n0 9c 8nn0 :g(n)cf (n)g En palabras, una funcin g est en O(f ) si existe una constante c tal que g < c f para toda n a partir de un cierto punto (n0 ).

Denicin

Aunque O(f ) es un conjunto de funciones por tradicin se escribe a veces g = O(f ) en vez de g 2 O(f ). Sin embargo, O(f ) = g no tiene sentido. Propiedades bsicas de la notacin O:
1 2 3 4

Si l mn!1 g (n)=f (n) < +1 entonces g = O(f ) Es reexiva: para toda funcin f : R+ ! R+ , f = O(f ) Es transitiva: si f

= O(g ) y g = O(h) entonces f = O(h) Para toda constante c > 0, O(f ) = O(c f )

Los factores constantes no son relevantes en la notacin asinttica y los omitiremos sistemticamente: p.e. hablaremos de O(n) y no de O(4 n); no expresaremos la base de logaritmos (O(log n)), ya que podemos pasar de una base a otra multiplicando por el factor apropiado:

logc x =

logb x logb c

Otras notaciones asintticas son


(omega) y (zita). La primera dene un conjunto de funciones acotada inferiormente por una dada:

(f )=fg:R+ !R+ j 9n0 9c>0 8nn0 :g(n)cf (n)g

La notacin
es reexiva y transitiva; si l mn!1 g (n)=f (n) > 0 entonces g =
(f ). Por otra parte, si f = O(g ) entonces g =
(f ) y viceversa.

Se dice que O(f ) es la clase de las funciones que crecen no ms rpido que f . Anlogamente,
(f ) es la clase de las funciones que crecen no ms despacio que f . Finalmente,

(f ) =
(f ) \ O(f )

es la clase de la funciones con la misma tasa de crecimiento que f . La notacin es reexiva y transitiva, como las otras. Es adems simtrica: f = (g ) si y slo si g = (f ). Si l mn!1 g (n)=f (n) = c donde 0 < c < 1 entonces g = (f ).

Propiedades adicionales de las notaciones asintticas (las inclusiones son estrictas):


1

Para cualesquiera constantes < , si f es una funcin creciente entonces O(f )  O(f ). Para cualquier constante c > 1, si f es creciente, O(f )  O(cf ).

Para cualesquiera constantes a y b > 0, si f es creciente, O((log f )a)  O(f b).

Los operadores convencionales (sumas, restas, divisiones, etc.) sobre clases de funciones denidas mediante una notacin asinttica se extienden de la siguiente manera:

A
B = fh j 9f 2 A ^ 9g 2 B : h = f
gg;
donde A y B son conjuntos de funciones. Expresiones de la forma f
A donde f es una funcin se entender como f f g
A. Este convenio nos permite escribir de manera cmoda expresiones como n + O(log n), nO(1) , (1) + O(1=n).

Regla de las sumas:

(f ) + (g ) = (f + g ) = (m axff; g g):


Regla de los productos:

Reglas similares se cumplen para las notaciones O y


.

(f ) (g ) = (f g ):

Anlisis de algoritmos iterativos

1 2

Si el coste de S1 es f , el de S2 es g y el coste de evaluar B es h entonces el coste en caso peor de if B then S1 elseS2 end if es m axff + h; g + hg.

Si el coste de un fragmento S1 es f y el de S2 es g entonces el coste de S1 ; S2 es f + g .

El coste de una operacin elemental es (1).

Si el coste de S durante la i-sima iteracin es fi , el coste de evaluar B es hi y el nmero de iteraciones es g entonces el coste T de while B do

end while es

Si f

fi (n) + hi (n): i=1 = m axffi + hi g entonces T = O(f g ).

T (n) =

i= g(n) X

Anlisis de algoritmos recursivos


El coste (en caso peor, medio, . . . ) de un algoritmo recursivo T (n) satisface, dada la naturaleza del algoritmo, una ecuacin recurrente: esto es, T (n) depender del valor de T para tamaos menores. Frecuentemente, la recurrencia adopta una de las dos siguientes formas:

T (n) = a T (n c) + g(n); T (n) = a T (n=b) + g(n):


La primera corresponde a algoritmos que tiene una parte no recursiva con coste g (n) y hacen a llamadas recursivas con subproblemas de tamao n c, donde c es una constante. La segunda corresponde a algoritmos que tienen una parte no recursiva con coste g (n) y hacen a llamadas recursivas con subproblemas de tamao (aproximadamente) n=b, donde b > 1.

Teorema
Sea T (n) el coste (en caso peor, en caso medio, ...) de un algoritmo recursivo que satisface la recurrencia
(

T (n) =

f (n) a T (n c) + g(n)

si 0  n < n0 si n  n0 ,

donde n0 es una constante, c  1, f (n) es una funcin arbitraria y g (n) = (nk ) para una cierta constante k  0. Entonces 8 k > si a < 1 > <(n ) k +1 T (n) = >(n ) si a = 1 > :(an=c ) si a > 1:

Teorema
Sea T (n) el coste (en caso peor, en caso medio, . . . ) de un algoritmo recursivo que satisface la recurrencia
(

T (n) =

f (n) a T (n=b) + g(n)

si 0  n < n0 si n  n0 ,

donde n0 es una constante, b > 1, f (n) es una funcin arbitraria y g (n) = (nk ) para una cierta constante k  0. Sea = logb a. Entonces si < k k T (n) = >(n log n) si = k > : (n ) si > k:
8 k > > <(n )

Anlisis de Algoritmos Divide y vencers rboles binarios de bsqueda rboles balanceados (AVLs) Tablas de Hash Colas de prioridad Grafos y Recorridos

Introduccin

El principio bsico de divide y vencers (en ingls, divide and conquer; en cataln, dividir per conquerir) es muy simple:
1

Si el ejemplar (instancia) del problema a resolver es sucientemente simple, se encuentra la solucin mediante algn mtodo directo. En caso contrario, se divide o fragmenta el ejemplar dado en subejemplares x1 ; : : : ; xk y se resuelve, independiente y recursivamente, el problema para cada uno de ellos. Las soluciones obtenidas y1 ; : : : ; yk se combinan para obtener la solucin al ejemplar original.

El esquema de divide y vencers expresado en pseudocdigo tiene el siguiente aspecto:


procedure DIVIDE _ Y _ VENCERAS(x) if x es simple then return SOLUCION _ DIRECTA(x) else

end if end procedure

:=1 to k do y := DIVIDE _ Y _ VENCERAS(x ) end for return COMBINA(y1 ; y2 ; : : : ; y )


k

for

hx1 ; x2 ; : : : ; x i := DIVIDE(x)
i

Ocasionalmente, si k = 1, se habla del esquema de reduccin.

El esquema de divide y vencers se caracteriza adicionalmente por las siguientes propiedades: No se resuelve ms de una vez un mismo subproblema El tamao de los subejemplares es, en promedio, una fraccin del tamao del ejemplar original, es decir, si x es de tamao n, el tamao esperado de un subejemplar cualquiera xi es n=ci donde ci > 1. Con frecuencia, esta condicin se cumplir siempre, no slo en promedio.

Coste de los algoritmos divide y vencers


El anlisis de un algoritmo divide y vencers requiere, al igual que ocurre con otros algoritmos recursivos, la resolucin de recurrencias. En su caso ms simple las recurrencias son de la forma
(

T (n) =

f (n) + a T (n=b) b(n)

si n > n0 , si n  n0 .

Este tipo de recurrencia corresponde al coste T (n) de un algoritmo donde conocemos la solucin directa siempre que n, el tamao de x, sea menor o igual a n0 , y en caso contrario, se descompone el ejemplar en subejemplares cuyo tamao es aproximadamente n=b, haciendose a llamadas recursivas, siendo f (n) el coste conjunto de las funciones de dividir y de combinar. Si bien es habitual que a = b, existen muchas situaciones en las que esto no sucede.

Teorema
Si g (n) = (nk ) entonces la solucin de la recurrencia
(

T (n) =

g(n) + a T (n=b) f (n)


8 > > <(g (n))

si n > n0 , si n  n0 ,

donde a  1 y b > 1 son constantes, satisface

T (n) = >(g(n) log n) > : (n )


siendo = logb a.

si < k, si = k si > k,

1 n: Supondremos que n = n bj . Aplicando la Demostraci 2 0 recurrencia repetidamente,

T (n) = gf (n) + a T (n=b) = g (n) + a f (n=b) + a2 T (n=b2 )

n k A j i @ = a i + a f (n0 ) b 0i<j
X  0

= = g (n) + a g (n=b) + + aj 1 g (n=bj 1 ) + aj T (n=bj ) X = ai g(n=bi ) + aj T (n0 ) 0i<j


0 1 1

= @ nk

0i<j

a i A j + a f (n0 ): bk

Puesto que f (n0 ) es una constante y tenemos que el segundo trmino es (n ) ya que

aj = alog (n=n0 ) = alog n a log = (blog alog n ) = (nlog a ) = (n ):


b b b b b

n0

Ahora tenemos tres casos diferentes a considerar: segn que a=bk sea menor, igual o mayor que 1. Alternativamente, ya que = logb a, segn que sea mayor, igual o menor que k. Si k > (equivalentemente, a=bk < 1) entonces la suma que aparece en el primer trmino es una serie geomtrica acotada por una constante, es decir, el primer trmino es (nk ). Como asumimos que k > , es el primer trmino el que domina y T (n) = (nk ) = (g(n)).

Si = k, tendremos que a=bk = 1 y la suma vale j ; ya que j = logb (n=n0 ) = (log n), conclumos que T (n) = (nk log n) = (g(n) log n). Finalmente, si k < entonces a=bk > 1 y el valor de la suma es
X   j a i (a=bk )j 1 a = = bk a=bk 1 bk n = ( k ) = (n k ): n !

0i<j

Por tanto, es el segundo trmino el que en este caso domina y T (n) = (n ).

Bsqueda dicotmica
Problema: Dado un vector A[1::n] de n elementos, en orden creciente, y un elemento x, determinar el valor i, 0  i  n, tal que A[i]  x < A[i + 1] (convendremos que A[0] = 1 y A[n + 1] = +1). Si el vector A fuese vaco la respuesta es sencilla, pues x no est en el vector. Pero para poder obtener una solucin recursiva efectiva, debemos realizar la siguiente generalizacin: dado un segmento o subvector A[` + 1::u 1], 0  `  u  n + 1, ordenado crecientemente, y tal que A[`]  x < A[u], determinar el valor i, `  i  u 1, tal que A[i]  x < A[i + 1]. Este es un ejemplo clsico de inmersin de parmetros. Ahora s, si ` + 1 = u, el segmento en cuestin no contiene elementos y i = u 1 = `.

. Llamada inicial: B IN S EARCH A; x; ; n procedure B IN S EARCH(A, x, `, u) Require:  ` < u  n ,A` x<Au Ensure: Retorna i tal que A i  x < A i y`i<u if ` u then return ` else m ` u div if x < A m then return B IN S EARCH A; x; `; m else return B IN S EARCH A; x; m; u end if end if end procedure

0 +1=

( +1 [ ] [] 2

0 + 1) [] [ + 1]

:= ( + ) [ ]

( (

) )

La llamada inicial es B IN S EARCH(()A; x; 0; A:SIZE() + 1), donde A:SIZE() nos da n, el nmero de elementos del vector A. Prescindiendo de la pequea diferencia entre el tamao real de los subejemplares y el valor n=2, el coste de la bsqueda binaria o dicotmica viene descrito por la recurrencia

B (n) = (1) + B (n=2); n > 0;


y B (0) = b0 , ya que en cada llamada recursiva slo se hace otra, sea en el subvector a la izquierda, sea el subvector a la derecha del punto central. Empleando el Teorema 1, = log2 1 = 0 y por lo tanto B (n) = (log n).

Algoritmo de Karatsuba y Ofmann

El algoritmo tradicional de multiplicacin tiene coste (n2 ) para multiplicar dos enteros de n bits, ya la suma de dos enteros de (n) bits tiene coste (n). 231 x 659 = + = = 9 x 231 + 5 x 231 x 10 6 x 231 x 100 2079 + 11550 + 138600 152229

1 Tambi 1 2 es de coste cuadr 2 tico el algoritmo de multiplicacin a la rusa:

end if end while


.z

:= 0 =X ^y =Y while x 6= 0 do . Inv: z + x y = X Y if x es par then x := x div 2; y := 2 y else x := x 1; z := z + y


z .x

=X Y

Supongamos que x e y son dos nmeros positivos de n = 2k bits (si n no es una potencia de 2 x e y no tienen ambos la misma longitud, podemos aadir 0s por la izquierda para que as sea). Una idea que no funciona:

x y = (A 2n=2 + B ) (C 2n=2 + D) = A C 2n + (A D + B C ) 2n=2 + B D:

Efectuada la descomposicin x = hA; B i y y = hC; Di, se calculan 4 productos (A B , A D, B C y B D) y se combinan las soluciones mediante sumas y desplazamientos (shifts). El coste de estas operaciones es (n). Por lo tanto, el coste de la multiplicacin mediante este algoritmo divide y vencers viene dado por la recurrencia

M (n) = (n) + 4 M (n=2); n > 1


cuya solucin es M (n) = (n2 ). En este caso = 1, a = 4 y b = 2, y = 1 < c = log2 4 = 2.

El algoritmo de Karatsuba y Ofmann (1962) realiza la multiplicacin dividiendo los dos nmeros como antes pero se realizan las siguientes 3 multiplicaciones (recursivamente)

U =AC V =BD W = (A + B ) (C + D)
Y el resultado se calcula a partir de las tres soluciones obtenidas

x y = U 2n + (W (U + V )) 2n=2 + V:

El algoritmo requiere realizar 6 adiciones (una de ellas es de hecho una resta) frente a las 3 adiciones que se empleaban en el algoritmo anterior. El coste mejora pues el coste de las funciones de divisin y de combinacin del algoritmo de Karatsuba y Ofmann sigue siendo lineal y disminuye el nmero de llamadas recursivas.

M (n) = (n) + 3 M (n=2) M (n) = (nlog2 3 ) = (n1;5849625::: )


La constante oculta en la notacin asinttica es grande y la ventaja de este algoritmo frente a los algoritmos bsicos no se maniesta hasta que n es relativamente grande (del orden de 200 a 250 bits por multiplicando).

procedure M ULT(x,y , i, j , i0 , j 0 ) Require:  i  j  n,  i0  j 0  n, j 0 i0 j i Ensure: Devuelve el producto de x i::j por y i0 ::j 0

if n < M then Usar un mtodo simple para calcular el resultado else m i j div m0 i0 j 0 div
U MULT x; y; m ; j; m0 ; j0 0 0 V MULT x; y; i; m; i ; m U:DECALA _ IZQUIERDA n W1 x i::m y i0 ::m0 W2 xm ::j y m0 ::j 0 Aadir un bit a W1 W2 si es necesario W M ULT W1 ; W2 ; : : : W W U V W:DECALA _ IZQUIERDA n div return U W V

:= j i + 1 := ( := := := := := :=

[ ]

end if end procedure

+ ) 2; := ( + ) 2 ( +1 +1 ) ( ) () [ ]+ [ ] [ +1 ]+ [ +1 ] ( ) ( + ) ( 2) + +

Ordenacin por fusin


El algoritmo de ordenacin por fusin (mergesort) fue uno de los primeros algoritmos ecentes de ordenacin propuestos. Diversas variantes de este algoritmo son particularmente tiles para la ordenacin de datos residentes en memoria externa. El propio mergesort es un mtodo muy ecaz para la ordenacin de listas enlazadas. La idea bsica es simple: se divide la secuencia de datos a ordenar en dos subsecuencias de igual o similar tamao, se ordena recursivamente cada una de ellas, y nalmente se obtiene una secuencia ordenada fusionando las dos subsecuencias ordenadas. Si la secuencia es sucientemente pequea puede emplearse un mtodo ms simple (y ecaz para entradas de tamao reducido).

Supondremos que nuestra entrada es una lista enlazada L conteniendo una secuencia de elementos x1 ; : : : ; xn . Cada elemento se almacena en un nodo con dos campos: info contiene el elemento y next es un apuntador al siguiente nodo de la lista (indicaremos que un nodo no tiene sucesor con el valor especial next = null). La propia lista L es, de hecho, un apuntador a su primer nodo. La notacin apuntador ! campo indica el campo referido del objeto apuntado como en C y C++. Es equivalente a la expresin p.campo de Pascal y Modula-2.

procedure S PLIT(L, L0 , n) Require: L `1 ; : : : ; `m ; m  n Ensure: L `1 ; : : : ; `n ; L0 `n+1 ; : : : ; `m

end procedure procedure M ERGE S ORT(L, n) if n > then m n div S PLIT L; L0 ; m M ERGE S ORT L; m M ERGE S ORT L0 ; n m . fusiona las listas L y L0 L M ERGE L; L0 end if end procedure

:= L while n > 1 do p := p ! next n := n 1 end while L0 := p ! next; p ! next := null


p

=[ =[

] ] =[

1 :=

:=

( ( (

Para fusionar razonamos de la siguiente forma: Si L L0 es vaca la lista resultante es la otra lista, es decir, la que eventualmente es no vaca. Si ambas son no vacas comparamos sus respectivos primeros elementos: el menor de los dos ser el primer elemento de la lista fusionada resultante y a continuacin vendr la lista resultado de fusionar la sublista que sucede a ese primer elemento con la otra lista.
procedure M ERGE(L, L0 ) if L null then return L0 end if if L0 null then return L end if if L ! inf o  L0 ! inf o then L ! next M ERGE L ! next; L0 return L else L0 ! next M ERGE L; L0 ! next return L0 end if end procedure

= =

:=

) )

:=

Cada elemento de L y L0 es visitado exactamente una vez, por lo que el coste de la operacin M ERGE es proporcional a la suma de los tamaos de las listas L y L0 ; es decir, su coste es (n). La funcin denida es recursiva nal y podemos obtener con poco esfuerzo una versin iterativa algo ms eciente. Observese que si el primer elemento de la lista L es igual al primer elemento de L0 se pone en primer lugar al que proviene de L, por lo que M ERGE S ORT es un algoritmo estable. El coste de M ERGE S ORT viene descrito por la recurrencia

M (n) = (n) + M



n 


= (n) + 2 M

n
2

+M



n 
2

cuya solucin es M (n) = (n log n), aplicando el segundo caso del Teorema 1.

Algoritmo de Strassen

El algoritmo convencional de multiplicacin de matrices tiene coste (n3 ) para multiplicar dos matrices n n.
for i to n do for j to n do

end for end for end for

:= 1 := 1 C [i; j ] := 0 for k := 1 to n do C [i; j ] := C [i; j ] + A[i; k] B [k; j ]

Para abordar una solucin con el esquema divide y vencers se descomponen las matrices en bloques:

A11 A12 B11 B12 = A21 A22 B21 B22 donde C11 = A11 B11 + A12 B21 , etc.

C11 C12 C21 C22

Cada bloque de C requiere dos multiplicaciones de bloques de tamao n=2 n=2 y dos sumas cuyo coste es (n2 ). El coste del algoritmo divide y vencers as planteado tendra coste

M (n) = (n2 ) + 8 M (n=2);


es decir, M (n) = (n3 ). Para conseguir una solucin ms eciente debemos reducir el nmero de llamadas recursivas.

Strassen (1969) propuso una forma de hacer justamente esto. En el libro de Brassard & Bratley se detalla una posible manera de hallar la serie de frmulas que producen el resultado deseado. Un factor que complica las cosas es que la multiplicacin de matrices no es conmutativa, a diferencia de lo que sucede con la multiplicacin de enteros. Se obtienen las siguientes 7 matrices n=2 n=2, mediante 7 productos y 14 adiciones/sustracciones

M1 = (A21 + A22 A11 ) (B11 + B22 B12 ) M2 = A11 B11 M3 = A12 B21 M4 = (A11 A21 ) (B22 B12 ) M5 = (A21 + A22 ) (B12 B11 ) M6 = (A12 A21 + A11 A22 ) B22 M7 = A22 (B11 + B22 B12 B21 )

Mediante 10 adiciones/sustracciones ms, podemos obtener los bloques de la matriz resultante C :

C11 = M2 + M3 C12 = M1 + M2 + M5 + M6 C21 = M1 + M2 + M4 M7 C22 = M1 + M2 + M4 + M5


Puesto que las operaciones aditivas tienen coste (n2 ), el coste del algoritmo de Strassen viene dado por la recurrencia

M (n) = (n2 ) + 7 M (n=2);


cuya solucin es (nlog2 7 ) = (n2;807::: ).

El algoritmo de Strassen tuvo un enorme impacto terico ya que fue el primer algoritmo de multiplicacin de matrices cuya complejidad era o(n3 ); lo cual tena tambin implicaciones en el desarrollo de algoritmos ms ecientes para el clculo de matrices inversas, de determinantes, etc. Adems era uno de los primeros casos en que las tcnicas algortmicas superaban lo que hasta el momento pareca una barrera infranqueable. En aos posteriores se han obtenido algoritmos todava ms ecientes. El ms eciente conocido es el de Coppersmith y Winograd (1986) cuyo coste es (n2;376::: ). El algoritmo de Strassen no es competitivo en la prctica, excepto si n es muy grande (n  500), ya que las constantes y trminos de orden inferior del coste son muy grandes.

QuickSort
Q UICK S ORT (Hoare, 1962) es un algoritmo de ordenacin que usa el principio de divide y vencers, pero a diferencia de los ejemplos anteriores, no garantiza que cada subejemplar tendr un tamao que es fraccin del tamao original. La base de quicksort es el procedimiento de PARTICIN: dado un elemento p denominado pivote, debe reorganizarse el segmento como ilustra la gura.

El procedimiento de particin sita a un elemento, el pivote, en su lugar apropiado. Luego no queda ms que ordenar los segmentos que quedan a su izquierda y a su derecha. Mientras que en M ERGE S ORT la divisin es simple y el trabajo se realiza durante la fase de combinacin, en Q UICK S ORT sucede lo contrario. Para ordenar el segmento A[`::u] el algoritmo queda as
procedure Q UICK S ORT(A, `, u) Ensure: Ordena el subvector A `::u if u `  M then usar un algoritmo de ordenacin simple else PARTICIN A; `; u; k

+1

[ ]

. A `::k

Q UICK S ORT A; k end if end procedure

1]  A[k]  A[k + 1::u] Q UICK S ORT((A; `; k 1)

+ 1; u)

En vez de usar un algoritmo de ordenacin simple (p.e. ordenacin por insercin) con cada segmento de M o menos elementos, puede ordenarse mediante el algoritmo de insercin al nal: Q UICK S ORT(A; 1; A:SIZE()) I NSERT S ORT(A; 1; A:SIZE()) Puesto que el vector A est quasi-ordenado tras aplicar Q UICK S ORT, el ltimo paso se hace en tiempo (n), donde n = A:SIZE(). Se estima que la eleccin ptima para el umbral o corte de recursin M oscila entre 20 y 25.

Existen muchas formas posibles de realizar la particin. En Bentley & McIlroy (1993) se discute un procedimiento de particin muy eciente, incluso si hay elementos repetidos. Aqu examinamos un algoritmo bsico, pero razonablemente ecaz. Se mantienen dos ndices i y j de tal modo que A[` + 1::i 1] contiene elementos menores o iguales que el pivote p, y A[j + 1::u] contiene elementos mayores o iguales. Los ndices barren el segmento (de izquierda a derecha, y de derecha a izquierda, respectivamente), hasta que A[i] > p y A[j ] < p o se cruzan (i = j + 1).

procedure PARTICIN(A, `, u, k) Require: `  u Ensure: A `::k  A k  A k ::u i ` ;j u; p A` while i < j do while i < j ^ A i  p do

end if end while

[ 1] [ ] [ + 1 ] := + 1 := := [ ] +1 +1 [ ] i := i + 1 end while while i < j + 1 ^ A[j ]  p do j := j 1 end while if i < j + 1 then A[i] :=: A[j ] [ ] :=: [ ] := j

A` Aj;k end procedure

El coste de Q UICK S ORT en caso peor es (n2 ) y por lo tanto poco atractivo en trminos prcticos. Esto ocurre si en todos o la gran mayora de los casos uno de los subsegementos contiene muy pocos elementos y el otro casi todos, p.e. as sucede si el vector est ordenado creciente o decrecientemente. El coste de la particin es (n) y entonces tenemos

Q(n) = (n) + Q(n 1) + Q(0) = (n) + Q(n 1) = (n) + (n 1) + Q(n 2)


= =
n X i=0
0

(i) = @

= (n2 ):

0in

iA

Sin embargo, en promedio, el pivote quedar ms o menos centrado hacia la mitad del segmento como sera deseable justicando que quicksort sea considerado un algoritmo de divide y vencers.

Para analizar el comportamiento de QUICKSORT slo importa el orden relativo de los elementos. Tambin podemos investigar exclusivamente el nmero de comparaciones entre elementos, ya que el coste total es proporcional a dicho nmero. Supongamos que cualquiera de los n! ordenes relativos posibles tiene idntica probabilidad, y sea qn el nmero medio de comparaciones.

qn =
=

1j n
X

E[# compar. j pivote es j -simo] Prfpivote es j -simog

1j n

(n 1 + qj 1 + qnj ) 1
X X

= n + O(1) + = n + O(1) +

(q + q ) n 1j n j 1 nj

n 0j<n

qj

Para resolver esta recurrencia emplearemos el denominado continuous master theorem (CMT).

El CMT considera recurrencias de divide y vencers con el siguiente formato

Fn = tn +

0j<n

!n;j Fj ;

n  n0

para un entero positivo n0 , una funcin tn , denominada funcin de peaje, y unos pesos !n;j  0. Los pesos deben satisfacer dos condiciones adicionales:
1 2

El paso fundamental es hallar una funcin de forma ! (z ) que aproxima los pesos !n;j .

Wn = 0j<n !n;j  1 P j ! < 1. Zn = 0j<n n W


n;j n

Denicin
Dado un conjunto de pesos !n;j , ! (z ) es una funcin de forma para el conjunto de pesos si R1 1 0 ! (z ) dz  1 2 existe una constante  > 0 tal que
0j<n
X !n;j

Z (j +1)=n

j=n

!(z ) dz

= O(n )

Un mtodo simple y que funciona usualmente para calcular funciones de forma consiste en sustituir j por z n en !n;j , multiplicar por n y tomar el lmite para n ! 1.

!(z ) = nl m n !n;zn !1

Las extensiones a los nmeros reales de muchas funciones discretas son inmediatas, p.e. j 2 ! z 2 . Para los nmeros binomiales se puede emplear la aproximacin ! zn (z n)k

 k! :

La extensin de los factoriales a los reales viene dada por la funcin gamma de Euler (z ) y la de los nmeros armnicos (z ) es la funcin (z ) = d lndz . Por ejemplo, en quicksort los pesos son todos iguales: 2 . La funcin de forma correspondiente es !n;j = n !(z ) = l mn!1 n !n;zn = 2.

Teorema (Roura, 1997)


Sea Fn descrita por la recurrencia

Fn = tn +

0j<n

!n;j Fj ;

sea ! (z ) una funcin de forma correspondiente a los pesos !n;j , y tn = ( na (log n)b ), para a  0 y b >R 1 constantes. R1 1 ! (z )z a ln z dz . Sea H = 1 0 ! (z )z a dz y H0 = (b + 1) 0 Entonces

+ o(tn ) si H > 0, Fn = > H0 ln n + o(tn log n) si H = 0 y H0 6= 0, > : (n ) si H < 0,


t
n

8 tn > > <H

donde x = es la nica solucin real no negativa de la ecuacin Z 1 1 ! (z )z x dz = 0: 0

Consideremos qn . Ya hemos visto que los pesos son !n;j = 2=n y tn = n 1. Por tanto !(z ) = 2, a = 1 y b = 0. Puede comprobarse fcilmente que se dan todas las condiciones necesarias para la aplicacin del CMT. Calculamos

H=1 H0 H0 =
qn =
Z 1

Z 1

2z dz = 1 z 2 = 0; z =0

z =1

por lo que tendremos que aplicar el caso 2 del CMT y calcular

2z ln z dz =

z2
2

z 2 ln z

z =1

1 = : 2 z =0

Por lo tanto,

n ln n + o(n log n) = 2n ln n + o(n log n) 1=2 = 1;386 : : : n log2 n + o(n log n):

QuickSelect
El problema de la seleccin consiste en hallar el j -simo de entre n elementos dados. En concreto, dado un vector A con n elementos y un rango j , 1  j  n, un algoritmo de seleccin debe hallar el j -simo elemento en orden ascendente. Si j = 1 entonces hay que encontrar el mnimo, si j = n entonces hay que hallar el mximo, si j = bn=2c entonces debemos hallar la mediana, etc. Es fcil resolver el problema con coste (n log n) ordenando previamente el vector y con coste (j n), recorriendo el vector y manteniendo los j elementos menores de entre los ya examinados. Con las estructuras de datos apropiadas puede rebajarse el coste a (n log j ), lo cual no supone una mejora sobre la primera alternativa si j = (n). Q UICK S ELECT (Hoare, 1962), tambin llamado F IND y one-sided Q UICK S ORT, es una variante del algoritmo Q UICK S ORT para la seleccin del j -simo de entre n elementos.

Supongamos que efectuamos una particin de un subvector A[`::u], conteniendo los elementos `-simo a u-simo de A, y tal que `  j  u, respecto a un pivote p. Una vez nalizada la particin, supongamos que el pivote acaba en la posicin k. Por tanto, en A[`::k 1] estn los elementos `-simo a (k 1)-simo de A y en A[k + 1::u] estn los elementos (k + 1)-simo au-simo. Si j = k hemos acabado ya que hemos encontrado el elemento solicitado. Si j < k entonces procedemos recursivamente en el subvector de la izquierda A[`::k 1] y si j > k entonces encontraremos el elemento buscado en el subvector A[k + 1::u].

procedure Q UICK S ELECT(A, `, j , u) Ensure: Retorna el j ` -simo menor elemento de A `::u ,


` u then return A ` end if PARTICIN A; `; u; k if j k then return A k end if if j < k then return Q UICK S ELECT A; `; j; k else return Q UICK S ELECT A; k ; j; u end if end procedure

ju
if `

( +1 ) )

[ ]

[]

[]

( (

1) +1 )

Puesto que Q UICK S ELECT es recursiva nal es muy simple obtener una versin iterativa eciente que no necesita espacio auxiliar. En caso peor, el coste de Q UICK S ELECT es (n2 ). Sin embargo, su coste promedio es (n) donde la constante de proporcionalidad depende del cociente j=n. Knuth (1971) ha (j ) demostrado que Cn , el nmero medio de comparaciones necesarias para seleccionar el j -simo de entre n es:
(j ) Cn = 2 (n + 1)Hn (n + 3 j )Hn+1j (j + 2)Hj + n + 3

El valor mximo se alcanza para j (j ) Cn = 2(ln 2 + 1)n + o(n).

= bn=2c; entonces

Consideremos ahora el anlisis del coste promedio Cn suponiendo que j adopta cualquier valor entre 1 y n con igual probabilidad.

Cn = n + O(1)
+ 1

n 1kn E

[nm. de comp. j el pivote es el k-simo] ;

puesto que el pivote es el k-simo con igual probabilidad para toda k.

La probabilidad de que j = k es 1=n y entonces ya habremos acabado. La probabilidad de continuar a la izquierda es (k 1)=n y entonces se habrn de hacer Ck1 comparaciones. Anlogamente, con probabilidad (n k)=n se continuar en el subvector a la derecha y se harn Cnk comparaciones.

Cn = n + O(1) +

k1 nk Ck1 + C n 1kn n n nk 2 X k = n + O(1) + C: n 0k<n n k


1
X

Aplicando el CMT con la funcin de forma

2zn n l  m = 2z n!1 n n
R1 2 obtenemos H = 1 0 2z dz

= 1=3 y Cn = 3n + o(n).

Podemos obtener un algoritmo cuyo coste en caso peor sea lineal si garantizamos que cada paso el pivote escogido divide el vector en dos subvectores cuya talla sea una fraccin de la talla del vector original, con coste O(n) (incluyendo la seleccin del pivote). Entonces, en caso peor,

C (n) = O(n) + C (p n);


donde p < 1. Puesto que log1=p 1 = 0 < 1 conclumos que C (n) = O(n). Por otra parte, es bastante obvio que C (n) =
(n); luego, C (n) = (n). Este es el principio del algoritmo de seleccin de Rivest y Floyd (1970).

La nica diferencia entre el algoritmo de seleccin de Hoare y el de Rivest y Floyd reside en la manera en que elegimos los pivotes. El algoritmo de Rivest y Floyd obtiene un pivote de calidad empleando el propio algoritmo, recursivamente, para seleccionar los pivotes! De hecho, el algoritmo calcula una pseudomediana y elige sta como pivote. Se subdivide el subvector A[`::u] en bloques de q elementos (excepto posiblemente el ltimo bloque), con q constante e impar, y para cada uno de ellos se obtiene su mediana. El coste de esta fase es (n) y como resultado se obtiene un vector con dn=q e elementos. Sobre dicho vector se aplica el algoritmo de seleccin para hallar la mediana. Como el pivote seleccionado es la pseudomediana de los n elementos originales, ello garantiza que al menos n=2q q=2 = n=4 elementos son mayores que el pivote.

Por lo tanto, el coste C (n) en caso peor satisface

C (n) = O(n) + C (n=q) + C (3n=4):


Utilizando la versin discreta del CMT, C (n) = O(n) si

3 1 + < 1: 4 q
Rivest y Floyd usaron el valor q de este algoritmo.

= 5 en su formulacin original

Anlisis de Algoritmos Divide y vencers rboles binarios de bsqueda rboles balanceados (AVLs) Tablas de Hash Colas de prioridad Grafos y Recorridos

Un rbol binario de bsqueda T es un rbol binario tal que o es vaco o bien contiene un elemento x y satisface
1

Denicin

Los subrboles izquierdo y derecho, L y R, respectivamente, son rboles binarios de bsqueda.

Para todo elemento y de L, CLAVE(y ) < CLAVE(x), y para todo elemento z de R, CLAVE(z ) > CLAVE(x).

Lema

Un recorrido en inorden de un rbol binario de bsqueda T visita los elementos de T por orden creciente de clave. Vamos a considerar ahora el diseo del algoritmo de bsqueda en rboles binarios de bsqueda (BSTs), en lo sucesivo). Dada la naturaleza recursiva de la denicin de BST es razonable abordar el diseo de este algoritmo de manera recursiva. Sea T el BST que representa al diccionario y k la clave buscada. Si T = entonces k no se encuentra el diccionario y ello se habr de indicar de algn modo conveniente. Si T no es vaco entonces tendremos que considerar la relacin que existe entre la clave del elemento x que ocupa la raz de T y la clave dada k.

Si k = CLAVE(x) la bsqueda ha tenido xito y nalizamos, retornando el elemento x (o la informacin asociada a x que nos interesa). Si k < CLAVE(x), se sigue de la denicin de los BSTs, que si hay un elemento en T cuya clave es k entonces dicho elemento se habr de encontrar en el subrbol izquierdo de T , por lo que habr que efectuar una llamada recursiva sobre el hijo izquierdo de T . Anlogamente, si k > CLAVE(x) entonces la bsqueda habr de continuar recursivamente en el subrbol derecho de T .

template <typename Clave, typename Valor> class Diccionario { public: ... void busca(const Clave& k, bool& esta, Valor& v) const throw(error); void inserta(const Clave& k, const Valor& v) throw(error); void elimina(const Clave& k) throw(); ... private: struct nodo_bst { Clave _k; Valor _v; nodo_bst* _izq; nodo_bst* _der; // constructora de la clase nodo_bst nodo_bst(const Clave& k, const Valor& v, nodo_bst* izq = NULL, nodo_bst* der = NULL); }; nodo_bst* raiz; static nodo_bst* busca_en_bst(nodo_bst* p, const Clave& k) throw(); static nodo_bst* inserta_en_bst(nodo_bst* p, const Clave& k, const Valor& v) throw(error); static nodo_bst* elimina_en_bst(nodo_bst* p, const Clave& k) throw(); static nodo_bst* juntar(nodo_bst* t1, nodo_bst* t2) throw(); static nodo_bst* reubicar_max(nodo_bst* p) throw(); ... }

El mtodo BUSCA emplea el mtodo privado BUSCA _ EN _ BST. ste ltimo recibe un apuntador p a la raz del BST en el que se ha de hacer la bsqueda y una clave k. Devuelve un apuntador, o bien nulo, si k no est presente en el BST, o bien que apunta al nodo del BST que contiene la clave k.
template <typename Clave, typename Valor> void Diccionario<Clave,Valor>::busca(const Clave& k, bool& esta, Valor& v) const throw(error) { nodo_bst* p = busca_en_bst(raiz, k); if (p == NULL) esta = false; else { esta = true; v = p -> _v; } }

La implementacin recursiva del mtodo privado BUSCA _ EN _ BST es casi inmediata a partir de la denicin de BST. Si el rbol es vaco su raz contiene la clave k, devolvemos el apuntador p ya que apunta al lugar correcto (a nulo o al nodo con la clave). En caso contrario, se compara k con la clave almacenada en el nodo raz al que apunta p y se prosigue recursivamente la bsqueda en el subrbol izquierdo o derecho, segn toque.
// implementacin recursiva template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_bst* Diccionario<Clave,Valor>::busca_en_bst(nodo_bst* p, const Clave& k) throw() { if (p == NULL or k == p -> _k) return p; // p != NULL and k != p -> _k if (k < p -> _k) return busca_en_bst(p -> _izq, k); else // p -> _k < k return busca_en_bst(p -> _der, k); }

Puesto que el algoritmo de buqueda recursivo es recursivo nal es inmediato obtener una versin iterativa.
// implementacin iterativa template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_bst* Diccionario<Clave,Valor>::busca_en_bst(nodo_bst* p, const Clave& k) throw() { while (p != NULL and k != p -> _k) { if (k < p -> _k) p = p -> _izq; else // p -> _k < k p = p -> _der; } return p; }

El algoritmo de insercin es tambin extremadamente simple y se obtiene a partir de un razonamiento similar al utilizado para desarrollar el algoritmo de bsqueda: si la nueva clave es menor que la clave en la raz entonces el nuevo elemento se ha de insertar (recursivamente) en el subrbol izquierda; si es mayor, la insercin se realiza en el subrbol derecho. El mtodo pblico INSERTA se apoya en otro mtodo privado de clase llamado INSERTA _ EN _ BST. ste recibe un apuntador a la raz del BST donde se debe insertar el par hk; v i y nos devuelve un apuntador a la raz del BST resultante de la insercin. Si k no aparece en el BST, se aade un nodo con el par hk; v i en el lugar apropiado. Si k ya exista, entonces el mtodo modica el valor asociado a k, reemplazando el valor anterior por v .

Antes de pasar a la implementacin de las inserciones, veamos la implementacin (trivial) de la constructora de la clase nodo_bst:
template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_bst(const Clave& k, Valor& v, nodo_bst* izq, nodo_bst* der) throw(error) : _k(k), _v(v), _izq(izq), _der(der) { }

template <typename Clave, typename Valor> void Diccionario::inserta(const Clave& k, const Valor& v) throw(error) { raiz = inserta_en_bst(raiz, k, v); } // implementacin recursiva template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo* Diccionario<Clave,Valor>::inserta_en_bst(nodo_bst* p, const Clave& k, const Valor& v) throw(error) { if (p == NULL) return new nodo_bst(k, v); // // // if p != NULL, contina la insercin en el subrbol apropiado o reemplaza el valor asociado si p -> _k == k (k < p -> _k) p -> _izq = inserta_en_bst(p -> _izq, k, v); else if (p -> _k < k) p -> _der = inserta_en_bst(p -> _der, k, v); else // p -> _k == k p -> _v = v; return p; }

La versin iterativa es ms compleja, ya que adems de localizar la hoja en la que se ha de realizar la insercin, deber mantenerse un apuntador padre al que ser padre del nuevo nodo.

// implementacin iterativa template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_bst* Diccionario<Clave,Valor>::inserta_en_bst(nodo_bst* p, const Clave& k, const Valor& v) throw(error) { // el BST est vaco if (p == NULL) return new nodo_bst(k, v); // el BST no est vaco nodo_bst* padre = NULL; nodo_bst* p_orig = p; // buscamos el punto de insercin while (p != NULL and k != p -> _k) { padre = p; if (k < p -> _k) p = p -> _izq; else // p -> _k < k p = p -> _der; } // // // if insertamos el nuevo nodo como hoja o modificamos el valor asociado, si ya haba un nodo con la clave k dada (p == NULL) { if (k < padre -> _k) padre -> izq = new nodo_bst(k, v); else // k > padre -> _k padre -> der = new nodo_bst(k, v);

} else // k == p -> _k p -> _v = v; return p_orig; }

Slo nos queda por considerar la eliminacin de elementos en BSTs. Si el elemento a eliminar se encuentra en un nodo cuyos dos subrboles son vacos basta eliminar el nodo en cuestin. Otro tanto sucede si el nodo x a eliminar slo tiene un subrbol no vaco: basta hacer que la raz del subrbol no vaco quede como hijo del padre de x.

El problema surge si hay que eliminar un nodo que contiene dos subrboles no vacos. Podemos reformular el problema de la siguiente forma: dados dos BSTs T1 y T2 tales que todas las claves de T1 son menores que las claves de T2 obtener un nuevo BST que contenga todas las claves: T = JUNTAR(T1 ; T2 ). Obviamente:

T; ) = T JUNTAR ( ; T ) = T
JUNTAR (

En particular, JUNTAR(

; )=

template <typename Clave, typename Valor> void Diccionario<Clave,Valor>::elimina( const Clave& k) throw() { raiz = elimina_en_bst(raiz, k); } template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_bst* Diccionario<Clave,Valor>::elimina_en_bst(nodo_bst* p, const Clave& k) throw() { // la clave no est if (p == NULL) return p; if (k < p -> _k) p -> _izq = elimina_en_bst(p -> _izq, k); else if (p -> k < k) p -> _der = elimina_en_bst(p -> _der, k); else { // k == p -> k nodo_bst* to_kill = p; p = juntar(p -> _izq, p -> _der); delete to_kill; } return p; }

Sea z + la mayor clave de T1 . Puesto que es mayor que todas las dems en T1 pero al mismo tiempo menor que cualquier clave de T2 podemos construir T colocando un nodo raz que contenga al elemento de clave z + , a T2 como subrbol derecho 0 como y al resultado de eliminar z + de T1 llammosle T1 subrbol izquierdo. Adems puesto que z + es la mayor clave de T1 el correspondiente nodo no tiene subrbol derecho, y es el nodo ms a la derecha en T1 , lo que nos permite desarrollar un procedimiento ad-hoc para eliminarlo.

T1 z+ z

T2

template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_bst* Diccionario<Clave,Valor>::juntar(nodo_bst* t1, nodo_bst* t2) throw() { // // if if si uno vaco, (t1 == (t2 == de los dos subrboles es la implementacin es trivial NULL) return t2; NULL) return t1;

// t1 != NULL y tambin t2 != NULL nodo_bst* z = reubica_max(t1); z -> _der = t2; return z; // alternativa: z = reubica_min(t2); // z -> _izq = t1; // return z; }

El mtodo privado REUBICA _ MAX recibe un apuntador p a la raz de un BST T y nos devuelve un apuntador a la raz de un nuevo BST T 0 . La raz de T 0 es el elemento mximo de T . El subrbol derecho de T 0 es nulo y el subrbol izquierdo de T 0 es el BST que se obtiene al eliminar el elemento mximo de T . La nica dicultad estriba en tratar adecuadamente la situacin en la que el elemento mximo del BST se encuentra ya como raz del mismodicha situacin debe detectarse y en ese caso el mtodo no debe hacer nada.
template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_bst* Diccionario<Clave,Valor>::reubica_max( nodo_bst* p) throw() { nodo_bst* padre = NULL; nodo_bst* p_orig = p; while (p -> _der != NULL) { padre = p; p = p -> _der; } if (padre != NULL) { padre -> _der = p -> _izq; p -> _izq = p_orig; } return p; }

Un razonamiento anlogo nos lleva a una versin de JUNTAR en la que se emplea la clave mnima z del rbol T2 para que ocupe la raz del resultado, en el caso en que T1 y T2 no son vacos. Se ha evidenciado experimentalmente que conviene alternar entre el predecesor z + y el sucesor z del nodo a eliminar en los borrados (por ejemplo, mediante una decisin aleatoria o con bit que va pasando alternativamente de 0 a 1 y de 1 a 0) y no utilizar sistemticamente una de las versiones.

Un BST de n elementos puede ser equivalente a una lista, pues puede contener un nodo sin hijos y n 1 con slo un hijo (p.e. si insertamos una secuencia de n elementos con claves crecientes en un BST inicialmente vaco). Un BST con estas caractersticas tiene altura n. En caso peor, el coste de una bsqueda, insercin o borrado en dicho rbol es (n). En general, una bsqueda, insercin o borrado en un BST de altura h tendr coste (h) en caso peor. Como funcin de n, la altura de un rbol puede llegar a ser n y de ah que el coste de las diversas operaciones es, en caso peor, (n).

Pero normalmente el coste de estas operaciones ser menor. Supongamos que cualquier orden de insercin de los elementos es equiprobable. Para bsquedas con xito supondremos que buscamos cualquiera de las n claves con probabilidad 1=n. Para bsquedas sin xito o inserciones supondremos que nalizamos en cualquiera de las n + 1 hojas con igual probabilidad. El coste de una de estas operaciones va a ser proporcional al nmero de comparaciones que habr que efectuar entre la clave dada y las claves de los nodos examinados. Sea C (n) el nmero medio de comparaciones y C (n; k) el nmero medio de comparaciones si la raz est ocupada por la k-sima clave.

C (n) =

1kn

C (n; k) P[raz es k-sima] :

C (n) =

n 1kn
1 1

C (n; k)
  k1 nk 0 + n C (k 1) + n C (n k) n

=1+ =1+ =1+

X X X

n 1kn n2 n2
2
0k<n 0k<n

(k C (k) + (n 1 k) C (n 1 k))

k C (k ):

Otra forma de plantear esto es calcular I (n), el valor medio de la longitud de caminos internos (IPL). Dado un BST su longitud de caminos internos es la suma de las distancias desde la raz a cada uno de los nodos.

C (n) = 1 +

I (n) n

La longitud media de caminos internos satisface la recurrencia

I (n) = n 1 +

n 0k<n

I (k);

I (0) = 0:

(1)

Esto es as porque cada nodo que no sea la raz contribuye al IPL total 1 ms su contribucin al IPL del subrbol en que se encuentre. Otra razn por la que resulta interesante estudiar el IPL medio es porque el coste de construir un BST de tamao n mediante n inserciones es proporcional al IPL.

Podemos asociar a cada ejecucin de Q UICK S ORT un BST como sigue. En la raz se coloca el pivote de la fase inicial; los subrboles izquierdo y derecho corresponden a las ejecuciones recursivas de Q UICK S ORT sobre los subvectores a la izquierda y a la derecha del pivote. Consideremos un subrbol cualquiera de este BST. Todos los elementos del subrbol, salvo la raz, son comparados con el nodo raz durante la fase a la que corresponde ese subrbol. Y recprocamente el pivote de una determinada fase ha sido comparado con los elementos (pivotes) que son sus antecesores en el rbol y ya no se comparar con ningn otro pivote. Por lo tanto, el nmero de comparaciones en las que interviene un cierto elemento no siendo el pivote es igual a su distancia a la raz del BST. Por esta razn I (n) = Q(n).

Para resolver la recurrencia de I (n) (longitud de caminos internos en BSTs) calculamos (n + 1)I (n + 1) nI (n):

(n + 1)I (n + 1) nI (n) = (n + 1)n n(n 1) + 2I (n) = 2n + 2I (n); (n + 1)I (n + 1) = 2n + (n + 2)I (n)

I (n

+ 1) = =

2n
n

+1 +1

+ +

n n

+2 +1

I (n) n

2n
n

2n
n

2(n

1)(

+1 +

2(n

1)(
n

+ 2)

+ 2)

n(n

+ 1)
i=k X

2n
n

+1

+ 2(n + 2)

2n
n

+1

+ 2(n + 2)

=1 = X
i i n i

(n

2)( + 2) + + 2 ( 2) 1) 1 +2 + 1)( + 2) + + 1 (
2(n
n n n(n i I n n n i n i n k I n i

n(n

+ 1)

+2
n

I (n

1)

k)

O(1) + 2(

+ 2)

X 

=1
i

(i + 1)(i + 2)

1 
2
n

+2
2 1
n

1
i

+1

O(1) + 2( =2 4
nHn

n n

+ 2)(

+2

+ 4 Hn +

O(1)

+1

+ Hn

2)

donde Hn = 1in 1=i. Puesto que Hn = ln n + O(1) se sigue que

I (n) = Q(n) = 2n ln n + O(n) = 1;386 : : : n log2 n + O(n)


y que C (n) = 2 ln n + O(1).

Los BSTs permiten otras varias operaciones siendo los algoritmos correspondientes simples y con costes (promedio) razonables. Algunas operaciones como las de bsqueda o borrado por rango (e.g. busca el 17o elemento) o de clculo del rango de un elemento dado requieren una ligera modicacin de la implementacin estndar, de tal forma que cada nodo contenga informacin relativa a tamao del subrbol en l enraizado. Concluimos esta parte con un ejemplo concreto: dadas dos claves k1 y k2 , k1 < k2 se precisa una operacin que devuelve una lista ordenada de todos los elementos cuya clave k est comprendida entre las dos dadas, esto es, k1  k  k2 .

Si el BST es vaco, la lista a devolver es tambin vaca. Supongamos que el BST no es vaco y que la clave en la raz es k. Si k < k1 entonces todas las claves buscadas deben encontrarse, si las hay, en el subrbol derecho. Anlogamente, si k2 < k se proseguir la bsqueda recursivamente en el subrbol izquierdo. Finalmente, si k1  k  k2 entonces puede haber claves que caen dentro del intervalo tanto en el subrbol izquierdo como en el derecho. Para respetar el orden creciente en la lista, deber buscarse recursivamente a la izquierda, luego listar la raz y nalmente buscarse recursivamente a la derecha.

// El mtodo en_rango se ha de declarar como // mtodo pblico de la clase Diccionario. // Dicho mtodo usa a otro auxiliar, // en_rango_en_bst, que se habra declarado // como mtodo privado de clase (static). template <typename Clave, typename Valor> void Diccionario<Clave,Valor>::en_rango( const Clave& k1, const Clave& k2, list<pair<Clave,Valor> >& result) const throw(error) { en_rango_en_bst(raiz, k1, k2, result); } template <typename Clave, typename Valor> void Diccionario<Clave,Valor>::en_rango_en_bst(nodo_bst* p, const Clave& k1, const Clave& k2, list<pair<Clave,Valor> >& result) const throw(error) { if (p == NULL) return; if (k1 <= p -> _k) en_rango_en_bst(p -> _izq, k1, k2, result); if (k1 <= p -> _k and p -> _k <= k2) result.push_back(make_pair(p -> _k, p -> _v)); if (k <= p -> _k) en_rango_en_bst(p -> _der, k1, k2, result); }

Anlisis de Algoritmos Divide y vencers rboles binarios de bsqueda rboles balanceados (AVLs) Tablas de Hash Colas de prioridad Grafos y Recorridos

rboles equilibrados

El problema de los BSTs estndar es que, como resultado de ciertas secuencias de inserciones y/o borrados, pueden quedar muy desequilibrados. En caso peor la altura de un BST de tamao n es (n) y en consecuencia el coste en caso peor de las operaciones de bsqueda, insercin y borrado es (n). A n de evitar este problema, se han propuesto diversas soluciones; la ms antigua y una de las ms elegantes y sencillas, es el equilibrado en altura, propuesto en 1962 por Adelson-Velskii y Landis. Los rboles de bsqueda resultantes se denominan AVLs en honor a sus inventores.

Un AVL T es un rbol binario de bsqueda tal que o es vaco o bien cumple que
1

Denicin

Los subrboles izquierdo y derecho, L y R, respectivamente, son AVLs. Las alturas de L y de R dieren a lo sumo en una unidad:

jaltura(L) altura(R)j  1

La naturaleza recursiva de la denicin anterior garantiza que la condicin de equilibrio se cumple en cada nodo x de un AVL. Si denotamos bal(x) la diferencia entre las alturas de los subrboles izquierdo y derecho de un nodo cualquiera x tendremos que bal(x) 2 f1; 0; +1g. Por otro lado todo AVL es un rbol binario de bsqueda, por lo que el algoritmo de bsqueda en AVLs es idntico al de BSTs, y un recorrido en inorden de un AVL visitar todos sus elementos en orden creciente de claves.

{enero, febrero, marzo, abril, mayo, junio, julio, agosto, septiembre, octubre noviembre, diciembre}
marzo

febrero

octurbre

agosto

junio

mayo

septiembre

abril

enero

julio

noviembre

diciembre

Lema
La altura h(T ) de un AVL de tamao n es (log n). Demostracin. Puesto que el AVL es un rbol binario se cumple que h(T )  dlog2 (n + 1)e, es decir, h(T ) 2
(log n). Ahora vamos a demostrar que h(T ) 2 O(log n). Sea Nh el mnimo nmero de nodos necesario para construir un AVL de altura h. Claramente N0 = 0 y N1 = 1. El AVL ms desequilibrado posible de altura h > 1 se consigue poniendo un subrbol, digamos el izquierdo, de altura h 1 empleando el mnimo posible de nodos Nh1 y otro subrbol, el derecho, que tendr altura h 2 (pero no puede tener menos!) en el que pondremos tambin el mnimo posible de nodos, Nh2 . Por tanto, Nh = 1 + Nh1 + Nh2

Veamos algunos valores de la secuencia fNh gh0 :

0; 1; 2; 4; 7; 12; 20; 33; 54; 88; : : :


La similitud con la secuencia de los nmeros de Fibonacci llama rpidamente nuestra atencin y no es casual: puesto que F0 = 0, F1 = 1, Fn = Fn1 + Fn2 se cumple que Nh = Fh+1 1 para toda h  0. En efecto, N0 = F1 1 = 1 1 = 0 y

Nh = 1 + Nh1 + Nh2 = 1 + (Fh 1) + (Fh1 1) = Fh+1 1


El nmero n-simo de Fibonacci cumple:
  n 1  Fn = p + ;

donde  = (1 +

5)=2  1;61803 : : : es la razn urea.

Consideremos ahora un AVL de tamao n y altura h. Entonces n  Nh por denicin y

h+1 3 n  Fh+1 1  p
5 2
Luego,

Tomando logaritmos en base , y simplicando

3 5 (n + ) 2 

 h

h  log n + O(1) = 1;44 log2 n + O(1)


El lema queda demostrado.

Del lema anterior deducimos que el coste de cualquier bsqueda en un AVL de tamao n ser, incluso en caso peor, O(log n). El problema reside en cmo conseguir que se cumpla la condicin de equilibrio en cada uno de los nodos del AVL tras una insercin o un borrado. La idea es que ambos algoritmos actan igual que los correspondientes en BSTs, pero cada uno de los nodos en el camino que va de la raz al punto de insercin (o borrado) debe ser comprobado para vericar que contina cumpliendo la condicin de balance y si no es as hacer algo al efecto. En primer lugar nos damos cuenta de que ser necesario que cada nodo almacene informacin sobre su altura (o su balance), para evitar cmputos demasiado costosos.

template <typename Clave, typename Valor> class Diccionario { public: ... void buscar(const Clave& k, bool& esta, Valor& v) const throw(error); void inserta(const Clave& k, const Valor& v) throw(error); void elimina(const Clave& k) throw(); ... private: struct nodo_avl { Clave _k; Valor _v; int _alt; nodo_avl* _izq; nodo_avl* _der; // constructora de la clase nodo_avl nodo_avl(const Clave& k, const Valor& v, int alt = 1, nodo_avl* izq = NULL, nodo_avl* der = NULL); }; nodo_avl* raiz; ... static int altura(nodo_avl* p) throw(); static void actualizar_altura(nodo_avl* p) throw(); };

int max(int x, int y) { return x > y ? } template <typename Clave, typename Valor> static int Diccionario<Clave,Valor>::altura( nodo_avl* p) throw() { if (p == NULL) return 0; else return p -> _alt; } template <typename Clave, typename Valor> static void Diccionario<Clave,Valor>::actualizar_altura( nodo_avl* p) throw() { p -> _alt = 1 + max(altura(p -> _izq), altura(p -> _der)); } x : y;

Para reestablecer el balance en un nodo que ya no cumpla la condicin de balance se utilizan rotaciones. La gura muestra las rotaciones ms simples. Obsrvese que si el rbol de la izquierda es un BST (A < x < B < y < C ) entonces el de la derecha tambin lo es, y viceversa.

x y C A B C

Supongamos un subrbol de un AVL con raz y en el que estamos haciendo la insercin de una clave k tal que k < x < y y que nalizado el proceso de insercin recursiva se produce un desequilibrio en y . Si C tiene altura h entonces el subrbol enraizado en x ha de tener altura h altura h + 1. Si la altura de x fuera h entonces la insercin de la nueva clave k no podra provocar el desequilibrio en y , de manera que vamos a suponer que la altura de x es h + 1.

Razonando de manera parecida llegamos a la conclusin de que la altura de A tiene que ser h. As pues la altura de y , previa a la insercin era h + 2. Como consecuencia de la insercin vamos a suponer que la altura del subrbol A se incrementa, de manera que el nuevo subrbol A0 tiene altura h + 1. Si suponemos que x mantiene la condicin de AVL tras la insercin (pero y no) eso nos lleva a concluir que B tambin tena altura h. Tras la insercin el subrbol enraizado en x pasa a tener altura h + 2 de manera que bal(y) = +2!

Antes:
y

x C h+1 A h B h

Despues:
y

x C h+2 A h+1 B h

LL

x y h+1 A B h C h+1

Al aplicar la rotacin sobre el nodo y , se preserva la propiedad de BST y adems se reestablece la propiedad de AVL. Por hiptesis, A0 , B y C son AVLs. Tras la rotacin la altura de y es h + 1 y su balance 0, y la altura de x pasa a ser h + 2 y su balance 0. No slo hemos solventado el problema de desequilibrio en y : el subrbol donde hemos aplicado la insercin es un AVL y tiene la misma altura que el subrbol antes de hacer la insercin, de manera que ninguno de los antecesores de y en el AVL va a estar desbalanceado. A la rotacin que hemos empleado se le suele denominar LL (left-left).

Un anlisis anlogo revela que si la clave insertada k cumple x < y < k y se produce un desbalanceo en el nodo x, entonces podemos reestablecer el equilibrio aplicando una rotacin RR (right-right) sobre el nodo x. La rotacin RR es la simtrica de la rotacin LL.
RR
x y A h h+2 h B h+1 C h+1 A h B C h+1 y

Analicemos ahora el caso de un subrbol de un AVL con raz y en el que estamos haciendo la insercin de una clave k, pero esta vez x < k < y y que nalizado el proceso de insercin recursiva se produce un desequilibrio en y . Si C tiene altura h entonces el subrbol enraizado en x ha de tener altura h + 1, la altura de B ha de ser h (aqu razonamos igual que cuando hicimos el anlisis de la situacin LL). Como consecuencia de la insercin vamos a suponer que la altura del subrbol B se incrementa, de manera que el nuevo subrbol B 0 tiene altura h + 1. Si suponemos que x mantiene la condicin de AVL tras la insercin (pero y no) eso nos lleva a concluir que A tambin tena altura h. Tras la insercin el subrbol enraizado en x pasa a tener altura h + 2 de manera que bal(y) = +2!

Si aplicamos la rotacin simple LL sobre el nodo y , se preserva la propiedad de BST pero no se reestablece la propiedad de AVL. Por hiptesis, A, B 0 y C son AVLs. Pero tras la rotacin LL la altura de y es h + 2 y su balance +1, y la altura de x pasa a ser h + 3 y su balance 2!
LL no sirve
y x

x C h+1 A h B h+1

y A h h+1 B h+2

Vamos a tener que pensar alguna otra cosa y hacer un anlisis ms no.

Vamos a suponer que la raz del subrbol B 0 es z y que tiene subrboles B1 y B2 , ambos AVLs. Puesto que B 0 tiene altura h + 1 y es un AVL, al menos uno de los dos subrboles Bi tiene altura h y el otro Bj tiene altura h h 1. Si aplicamos una rotacin que lleva a z a la raz entonces x es la raz de su subrbol izquierdo y sus hijos son A y B1 , y y es la raz de su subrbol derecho y sus hijos son B2 y C .
LR
y z

x C

h+2

x < _ h+1
A B1 B2

y h+1 C h

z
h h+2 h+1 B1 B2 A

< _h

Ntese que antes de la rotacin el inorden era A < x < B1 < z < B2 < y < C , exactamente igual que tras la rotacin, esto, este nuevo tipo de rotacin tambin preserva la propiedad de BST.

Al aplicar la rotacin sobre el nodo y , se preserva la propiedad de BST y adems se reestablece la propiedad de AVL. Por hiptesis, A, B y C son AVLs. Tras la rotacin la altura de x es h + 1 y su balance 0 +1, la altura de y es h + 1 y su balance 0 1, y la altura de z pasa a ser h + 2 y su balance 0. Igual que con las rotaciones simples LL y RR, no slo arreglamos el problema de desequilibrio en y : el subrbol donde hemos aplicado la insercin es un AVL y tiene la misma altura que el subrbol antes de hacer la insercin, de manera que ninguno de los antecesores de y en el AVL va a estar desbalanceado. A esta nueva rotacin se le denomina rotacin doble LR (left-right).

Para la situacin en que una clave se inserta a la derecha y luego hacia la izquierda, el desequilibrio que se pudiera producir se corrige mediante una rotacin RL, que es completamente anloga a la LR, cambiando derecha por izquierda y viceversa.
RL
x y A h z

h+2

x < _ h+1

y h+1 B2 C h

z
C h+1 B1 B2 A h+2 B1

< _h

Las rotaciones dobles (LR y RL) se denominan as porque podemos descomponerlas como una secuencia de dos rotaciones simples. Por ejemplo la rotacin doble LR sobre el nodo y consiste en primero aplicar una rotacin RR sobre x y a continuacin una rotacin LL sobre y (su hijo izquierdo ha pasado a ser z y no x).
RR(x)
y y h z h h+2 C

x C h h+2

z
A h B1 B2

B2 B1 h

< _ h1

< _ h1

LL(y)

h+2

y h+1

B1

B2

De manera similar, una rotacin RL se compone de una rotacin LL seguida de una rotacin RR.
LL(y)
x y A h A h y C B1 B2 h+2 h+2 h B1

x
z

z < _h
B2

< _h

RR(x)

h+2

x < _h
A B1 h B2

y h+1 C h

Ejemplo de los meses del ao en un AVL:


enero 2 febrero
1

RR
0 0

febrero enero marzo


0

marzo +{enero, febrero, marzo}

febrero
+2

0 +1

enero

marzo junio +1 julio


0

abril

mayo

0 agosto

+ {abril, mayo, junio, julio, agosto}

LR
1

febrero
0

agosto
0

marzo +1 enero julio


0

abril

junio +1

mayo

Ejemplo de los meses del ao en un AVL:


2

febrero
0

agosto
0

marzo enero julio


0 0

1 2

abril

junio +1

mayo

septiembre octubre

+1

+ {septiembre, octubre}

RL

febrero
0

agosto
0

marzo 0 enero julio


0

abril

junio +1
0 mayo

octubre

septiembre

Ejemplo de los meses del ao en un AVL:


febrero
0 2

agosto
0

marzo 1 enero julio


0

abril

junio

+1 1

octubre

+1

mayo
0

septiembre noviembre

+ {noviembre}

RR

marzo 0

febrero

octubre

+1

0 agosto

junio +1

mayo

septiembre

abril

enero

julio

noviembre

marzo

+1

+1

febrero

octubre

+1

1 agosto

junio +1

mayo

septiembre

abril

+1

enero

julio

noviembre

diciembre

+ {diciembre}

De nuestro anlisis previo podemos establecer los siguientes hechos y sus consecuencias:
1

Una rotacin simple o doble slo involucra la modicacin de unos pocos apuntadores y actualizaciones de alturas, y su coste es por tanto (1), independiente de la talla del rbol. En una insercin en un AVL slo habr que efectuarse a lo sumo una rotacin (simple o doble) para reestablecer el equilibrio del AVL. Dicha rotacin se aplica, en su caso, en uno de los nodos en el camino entre la raz y el punto de insercin. El coste en caso peor de una insercin en un AVL es (log n).

template <typename Clave, typename Valor> class Diccionario { public: ... private: struct nodo_avl { ... }; nodo_avl* raiz; ... static nodo_avl* inserta_en_avl(nodo_avl* p, const Clave& k, const Valor& v) throw(error); static nodo_avl* rotacionLL(nodo_avl* p) throw(); static nodo_avl* rotacionLR(nodo_avl* p) throw(); static nodo_avl* rotacionRL(nodo_avl* p) throw(); static nodo_avl* rotacionRR(nodo_avl* p) throw(); ... };

template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_avl* Diccionario<Clave,Valor>::rotacionLL(nodo_avl* p) throw() { nodo_avl* q = p -> _izq; p -> _izq = q -> _der; q -> _der = p; actualiza_altura(p); actualiza_altura(q); return q; } template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_avl* Diccionario<Clave,Valor>::rotacionRR(nodo_avl* p) throw() { nodo_avl* q = p -> _der; p -> _der = q -> _izq; q -> _izq = p; actualiza_altura(p); actualiza_altura(q); return q; }

template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_avl* Diccionario<Clave,Valor>::rotacionLR(nodo_avl* p) throw() { p -> _izq = rotacionRR(p -> _izq); return rotacionLL(p); } template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_avl* Diccionario<Clave,Valor>::rotacionRL(nodo_avl* p) throw() { p -> _der = rotacionLL(p -> _der); return rotacionRR(p); }

template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_avl* Diccionario<Clave,Valor>::inserta_en_avl(nodo_avl* p, const Clave& k, const Valor& v) throw(error) { if (p == NULL) return new nodo_avl(k, v); if (k < p -> _k) { p -> _izq = inserta_en_avl(p -> _izq, k, v); // comprobamos si hay desequilibrio en p // y rotamos si es necesario if (altura(p -> _izq) - altura(p -> _der) == 2) { // p -> _izq no puede ser vaco if (k < p -> _izq -> _k) // caso LL p = rotacionLL(p); else // caso LR p = rotacionLR(p); } } else if (p -> _k < k) { // analogo al caso anterior ... p -> _der = inserta_en_avl(p -> _der, k, v); if (altura(p -> _der) - altura(p -> _izq) == 2) { if (p -> _der -> _k < k) // caso RR p = rotacionRR(p); else // caso RL p = rotacionRL(p); } } else // p -> _k == k p -> _v = v; actualiza_altura(p); return p; }

Si bien resulta posible escribir una versin iterativa del algoritmo de insercin, resulta complicada ya que una vez efectuada la insercin del nuevo nodo en la hoja que corresponde deberemos deshacer el camino hasta la raz para detectar si en alguno de los nodos del camino se produce un desequilibrio y entonces aplicar la rotacin correspondiente. Este deshacer el camino ocurre de manera natural con la recursividad, a la vuelta de cada llamada recursiva comprobamos si en el nodo desde el que se hace la llamada recursiva hay o no desequilibrio al terminar la insercin. Para una versin iterativa necesitaramos que cada nodo contuviese un apuntador explcito a su padre o bien tendremos que almacenar la secuencia de nodos visitados durante la bajada en una pila, y despus ir desapilando uno a uno para deshacer el camino.

El algoritmo de borrado en AVLs se fundamenta en las mismas ideas, si bien el anlisis de qu rotacin aplicar en cada caso es un poco ms complejo. Baste decir que el coste en caso peor de un borrado en un AVL es (log n). Los desequilibrios se solucionan aplicando rotaciones simples o dobles, pero a diferencia de lo que sucede con las inserciones, podemos tener que aplicar varias. No obstante, todas las rotaciones (de coste (1)) se aplican a lo sumo una vez sobre cada uno de los nodos del camino desde la raz hasta el punto de borrado, y como slo hay O(log n) de stos, el coste es logartmico en caso peor. El algoritmo de borrado en AVLs es relativamente ms complejo que el de insercin, y la versin iterativa an ms. En las siguientes transparencias se muestra el cdigo del algoritmo de borrado, en su versin recursiva, sin ms comentarios.

template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_avl* Diccionario<Clave,Valor>::elimina_en_avl(nodo_avl* p, const Clave& k) throw() { if (p == NULL) return p; if (k < p -> _k) { p -> _izq = elimina_en_avl(p -> _izq, k); // comprobamos si hay desequilibrio en p // y rotamos si es necesario if (altura(p -> _der) - altura(p -> _izq) == 2) { // p -> _der no puede ser vaco if (altura(p -> _der -> _izq) - altura(p -> _der -> _der) == 1) p = rotacionRL(p); else p = rotacionRR(p); } } else if (p -> _k < k) { // analogo al caso anterior ... p -> _der = elimina_en_avl(p -> _der, k); if (altura(p -> _izq) - altura(p -> _der) == 2) { if (altura(p -> _izq -> _der) - altura(p -> _der -> _izq) == 1) p = rotacionLR(p); else p = rotacionLL(p); } } else { // p -> _k == k nodo_avl* to_kill = p; p = juntar(p -> _izq, p -> _der); delete to_kill; } actualiza_altura(p); return p; }

template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_avl* Diccionario<Clave,Valor>::juntar(nodo_avl* t1, nodo_avl* t2) throw() { // // if if si uno vaco, (t1 == (t2 == de los dos subrboles es la implementacin es trivial NULL) return t2; NULL) return t1;

// t1 != NULL y tambin t2 != NULL nodo_alv* z; elimina_min(t2, z); z -> _izq = t1; z -> _der = t2; actualiza_altura(z); return z; }

template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_avl* Diccionario<Clave,Valor>::elimina_min( nodo_avl*& p, nod_avl*& z) throw() { if (p -> _izq != NULL) { elimina_min(p -> _izq, z); // comprobamos si hay desequilibrio en p // y rotamos si es necesario if (altura(p -> _der) - altura(p -> _izq) == 2) { // p -> _der no puede ser vaco if (altura(p -> _der -> _izq) - altura(p -> _der -> _der) == 1) p = rotacionRL(p); else p = rotacionRR(p); } } else { z = p; p = p -> _der; } actualiza_altura(p); }

Anlisis de Algoritmos Divide y vencers rboles binarios de bsqueda rboles balanceados (AVLs) Tablas de Hash Colas de prioridad Grafos y Recorridos

Tablas de dispersin (hash)

Una tabla de dispersin (ing: hash table) permite almacenar un conjunto de elementos (o pares clave-valor) mediante una funcin de dispersin h que va del conjunto de claves al conjunto de ndices o posiciones de la tabla (p.e. 0::M 1). Idealmente la funcin de dispersin h hara corresponder a cada uno de los n elementos (de hecho a sus claves) que queremos almacenar una posicin distinta. Obviamente, esto no ser posible en general y a claves distintas les corresponder la misma posicin de la tabla.

Pero si la funcin de dispersin dispersa ecazmente las claves, el esquema seguir siendo til, ya que la probabilidad de que el diccionario a representar mediante la tabla de hash contenga muchas claves con igual valor de h ser pequea. Dadas dos claves x e y distintas, se dice que x e y son sinnimos, o que colisionan, si h(x) = h(y ). El problema bsico de la implementacin de un diccionario mediante una tabla de hash consistir en la denicin de una estrategia de resolucin de colisiones.

template <typename T> class Hash { public: int operator()(const T& x) const throw(); }; template <typename Clave, typename Valor, template <typename> class HashFunct = Hash> class Diccionario { public: ... void busca(const Clave& k, bool& esta, Valor& v) const throw(error); void inserta(const Clave& k, const Valor& v) throw(error); void elimina(const Clave& k) throw(); ... private: struct nodo { Clave _k; Valor _v; ... }; nat _M; // tamao de la tabla nat _n; // nm. de elementos en la tabla double _alfa_max; // factor de carga // direccionamiento abierto nodo* _Thash; // tabla con los pares <clave,valor> // separate chaining // nodo** _Thash; // tabla de punteros a nodo static int hash(const Clave& k) throw() { HashFunct<Clave> h; return h(k) % _M; } };

Funciones de hash

Una buena funcin de hash debe tener las siguientes propiedades:


1 2

Debe ser fcil de calcular Debe dispersar de manera razonablemente uniforme el espacio de las claves, es decir, si dividimos el conjunto de todas las claves en grupos de sinnimos, todos los grupos deben contener aproximadamente el mismo nmero de claves. Debe enviar a posiciones distantes a claves que son similares.

La construccin de buenas funciones de hash no es fcil y requiere conocimientos muy slidos de varias ramas de las matemticas. Est fuertemente relacionada con la construccin de generadores de nmeros pseudoaleatorios. Como regla general da buenos resultados calcular un nmero entero positivo a partir de la representacin binaria de la clave (p.e. sumando los bytes que la componen) y tomar mdulo M , el tamao de la tabla. Se recomienda que M sea un nmero primo. La clase Hash<T> dene el operador () de manera que si h es un objeto de la clase Hash<T> y x es un objeto de la clase T, podemos aplicar h(x). Esto nos devolver un nmero entero. La operacin privada hash de la clase Diccionario calcular el mdulo mediante h(x) % _M de manera que obtengamos un ndice vlido de la tabla, entre 0 y _M - 1.

// especializacin del template para T = string template <> class Hash<string> { public: int operator()(const string& x) const throw() { int s = 0; for (int i = 0; i < x.length(); ++i) s = s * 37 + x[i]; return s; }; // especializacin del template para T = int template <> class Hash<int> { static long const MULT = 31415926; public: int operator()(const int& x) const throw() { long y = ((x * x * MULT) << 20) >> 4; return y; } };

Otras funciones de hash ms sosticadas emplean sumas ponderadas o transformaciones no lineales (p.e. elevan al cuadrado el nmero representado por los k bytes centrales de la clave).

Resolucin de colisiones

Existen dos grandes familias de estrategias de resolucin de colisiones. Por razones histricas, que no lgicas, se utilizan los trminos dispersin abierta (ing: open hashing) y direccionamiento abierto (ing: open addressing). Estudiaremos dos ejemplos representativos: tablas abiertas con sinnimos encadenados (ing: separate chaining) y sondeo lineal (ing: linear probing).

Sinnimos encadenados
En las tablas con sinnimos encadenados cada entrada de la tabla da acceso a una lista enlazada de sinnimos.
template <typename Clave, typename Valor, template <typename> class HashFunct = Hash> class Diccionario { ... private: struct nodo { Clave _k; Valor _v; nodo* _sig; // constructora de la clase nodo nodo(const Clave& k, const Valor& v, nodo* sig = NULL); }; nodo** _Thash; // tabla de punteros a nodo int _M; // tamao de la tabla int _n; // numero de elementos double _alfa_max; // factor de carga nodo* busca_sep_chain(const Clave& k) const throw(); void inserta_sep_chain(const Clave& k, const Valor& v) throw(error); void elimina_sep_chain(const Clave& k) throw(); };

M = 13

X = { 0, 4, 6, 10, 12, 13, 17, 19, 23, 25, 30}

h (x) = x mod M 13 0

0 1 2 3 4 5 6 7 8 9 10 11 12

30

17

19

23

10

25

12

Para la insercin, se accede a la lista correspondiente mediante la funcin de hash, y se recorre para determinar si ya exista o no un elemento con la clave dada. En el primer caso, se modica la informacin asociada; en el segundo se aade un nuevo nodo a la lista con el nuevo elemento. Puesto que estas listas de sinnimos contienen por lo general pocos elementos lo ms efectivo es efectuar las inserciones por el inicio (no hace falta usar fantasmas, cierre circular, etc.). Existen variantes en que las listas de sinnimos estn ordenadas o se sustituyen por rboles de bsqueda, pero no comportan una ventaja real en trminos prcticos (el coste asinttico terico es mejor, pero el criterio no es efectivo ya que el nmero de elementos involucrado es pequeo).

La bsqueda es tambin simple: se accede a la lista apropiada mediante la funcin de hash y se realiza un recorrido secuencial de la lista hasta que se encuentra un nodo con la clave dada o la lista se ha examinado sin xito.

template <typename Clave, typename Valor, template <typename> class HashFunct> void Diccionario<Clave,Valor,HashFunct>::inserta(const Clave& k, const Valor& v) throw(error) { if (_n / _M > _alfa_max) // la tabla est demasiado llena ... enviar un error // o redimensionar inserta_sep_chain(k, v); } template <typename Clave, typename Valor, template <typename> class HashFunct> void Diccionario<Clave,Valor,HashFunct>::inserta_sep_chain( const Clave& k, const Valor& v) throw(error) { int i = hash(k); nodo* p = _Thash[i]; // buscamos en la lista de sinnimos while (p != NULL and p -> _k != k) p = p -> _sig; // lo insertamos al principio // si no estaba if (p == NULL) { _Thash[i] = new nodo(k, v, _Thash[i]); ++_n; } else p -> _v = v; }

template <typename Clave, typename Valor, template <typename> class HashFunct> void Diccionario<Clave,Valor,HashFunct>::busca(const Clave& k, bool& esta, Valor& v) const throw(error) { nodo* p = buscar_sep_chain(k); if (p == NULL) esta = false; else { esta = true; v = p -> _v; } } template <typename Clave, typename Valor, template <typename> class HashFunct> Diccionario<Clave,Valor,HashFunct>::nodo* Diccionario<Clave,Valor,HashFunct>::busca_sep_chain(const Clave& k) const throw() { int i = hash(k); nodo* p = _Thash[i]; // buscamos en su lista de sinnimos while (p != NULL and p -> _k != k) p = p -> _sig; return p; }

template <typename Clave, typename Valor, template <typename> class HashFunct> void Diccionario<Clave,Valor, HashFunct>::elimina(const Clave& k) throw() { eliminar_sep_chain(k); } template <typename Clave, typename Valor, template <typename> class HashFunct> void Diccionario<Clave,Valor,HashFunct>::eliminar_sep_chain (const Clave& k) throw() { int i = hash(k); nodo* p = _Thash[i]; nodo* ant = NULL; // apuntara al anterior de p // buscamos en su lista de sinnimos while (p != NULL and p -> _k != k) { ant = p; p = p -> _sig; } // // // if si p != NULL, lo quitamos de la lista teniendo en cuenta que puede ser el primero (p != NULL) { --_n; if (ant != NULL) ant -> _sig = p -> _sig; else _Thash[i] = p -> _sig; delete p;

} }

Sea n el nmero de elementos almacenados en la tabla con encadenamiento de sinnimos. En promedio, cada lista tendr = n=M elementos y el coste de bsquedas (con y sin xito), de inserciones y borrados ser proporcional a . Si es un valor razonablemente pequeo entonces podemos considerar que el coste promedio de todas las operaciones sobre la tabla de hash es (1), ya que es constante.

Al valor se le denomina factor de carga (ing: load factor). Otra variantes de hash abierto almacenan los sinnimos en una zona particular de la propia tabla, la llamada zona de excedentes (ing: cellar). Las posiciones de esta zona no son direccionables, es decir, la funcin de hash nunca toma sus valores.

Direccionamiento abierto

En las estrategias de direccionamiento abierto los sinnimos se almacenan en la tabla de hash, en la zona de direccionamiento. Bsicamente, tanto en bsquedas como en inserciones se realiza una secuencia de sondeos que comienza en la posicin i0 = h(k) y a partir de ah contina en i1 ; i2 ; : : : hasta que encontramos una posicin ocupada por un elemento cuya clave es k (bsqueda con xito), una posicin libre (bsqueda sin xito, insercin) o hemos explorado la tabla en su totalidad. Las distintas estrategias varan segn la secuencia de sondeos que realizan. La ms sencilla de todas es el sondeo lineal: i1 = i0 + 1; i2 = i1 + 1; : : :, realizndose los incrementos mdulo M .

Sondeo lineal
template <typename Clave, typename Valor, template <typename> class HashFunct = Hash> class Diccionario { ... private: struct nodo { Clave _k; Valor _v; bool _libre; // constructora de la clase nodo nodo(const Clave& k, const Valor& v, bool libre = true); }; nodo* _Thash; // tabla de nodos int _M; // tamao de la tabla int _n; // numero de elementos double _alfa_max; // factor de carga

int busca_linear_probing(const Clave& k) const throw(); void inserta_linear_probing(const Clave& k, const Valor& v) throw(error); void elimina_linear_probing(const Clave& k) throw(); };

M = 13

X = { 0, 4, 6, 10, 12, 13, 17, 19, 23, 25, 30} (incremento 1) 0 13


ocupado ocupado libre libre

h (x) = x mod M 0

0 1 2 3 4 5 6 7 8 9 10 11 12

0 1 2 3

0 1 2 3 4 5 6 7 8 9 10 11 12

0 13 25

ocupado ocupado ocupado libre

4 6

4 5 6 7 8 9

4 17 6 19

ocupado ocupado ocupado ocupado libre libre

4 17 6 19 30

ocupado ocupado ocupado ocupado ocupado libre

10 12

10 11 12

10 23 12

ocupado ocupado ocupado

10 23 12

ocupado ocupado ocupado

+ {0, 4, 6, 10, 12}

+ {13, 17, 19, 23}

+ {25, 30}

// Solo se invoca si hay al menos un sitio // no ocupado en la tabla: _n < _M template <typename Clave, typename Valor, template <typename> class HashFunct> void Diccionario<Clave,Valor,HashFunct>::inserta_linear_probing( const Clave& k, const Valor& v) throw(error) { int i = hash(k); while (not _Thash[i]._libre and _Thash[i]._k != k) i = (i + 1) % _M; } _Thash[i]._k = k; _Thash[i]._v = v; if (_Thash[i]._libre) ++_n; _Thash[i]._libre = false; }

template <typename Clave, typename Valor, template <typename> class HashFunct> int Diccionario<Clave,Valor,HashFunct>::busca( const Clave& k, bool& esta, Valor& v) const throw(error) { int i = busca_linear_probing(k); if (not _Thash[i]._libre and _Thash[i]._k == k) { esta = true; v = _Thash[i]. _v; } else esta = false; } template <typename Clave, typename Valor, template <typename> class HashFunct> int Diccionario<Clave,Valor,HashFunct>::busca_linear_probing( const Clave& k) const throw() { int i = hash(k); int vistos = 0;

// // // //

para asegurarnoss una pasada, no ms; es inncesario si n < _M ya que entonces habr una posicin i tal que _libre == true

while (not _Thash[i]._libre and _Thash[i]._k != k and vistos < _M) { ++vistos; i = (i + 1) % _M; } return i; }

El borrado en tablas de direccionamiento abierto es algo ms complicado. No basta con marcar la posicin correspondiente como libre ya que una bsqueda posterior podra no hallar el elemento buscado aunque ste se encontrase en la tabla. Se ha de recorrer los elementos que siguen al que se borra en el mismo clster desplazando al lugar desocupado a todo elemento cuya posicin inicial de hash preceda o sea igual a la posicin desocupada. Para ello necesitamos la funcin displ(j; i) que nos da la distancia entre las posiciones j e i en el orden cclico: si j > i hay que dar la vuelta pasando por la posicin _M 1 y regresando a la posicin 0.
int displ(j, i, M) { if (i >= j) return i - j; else return M + (i - j); }

// asumimos para simplificar que _n < _M template <typename Clave, typename Valor, template <typename> class HashFunct> int Diccionario<Clave,Valor,HashFunct>::elimina_linear_probing( const Clave& k) const throw() { int i = busca_linear_probing(k); if (not _Thash[i]._libre) { // _Thash[i] es el elemento que se quiere eliminar int free = i; i = (i + 1) % _M; int d = 1; while (not _Thash[i]._libre) { int i_home = hash(_Thash[i]._k); if (displ(i_home, i, _M) >= d) { _Thash[free] = _Thash[i]; free = i; d = 0; } i = (i + 1) % _M; ++d; } _Thash[free]._libre = true; --_n; } }

El sondeo lineal ofrece algunas ventajas ya que los sinnimos tienden a encontrarse en posiciones consecutivas, lo que resulta ventajoso en implementaciones sobre memoria externa. Generalmente se usara junto a alguna tcnica de bucketing: cada posicin de la tabla es capaz de albergar b > 1 elementos. Por otra parte tiene algunas serias desventajas y su rendimiento es especialmente sensible al factor de carga = n=M .

Si es prximo a 1 (tabla llena o casi llena) el rendimiento ser muy pobre. Si < 1 el coste de las bsquedas con xito (y modicaciones) ser proporcional a

1 1 1+ 2 1


y el de bsquedas sin xito e inserciones ser proporcional a

1 1 1+ : 2 (1 )2

Un fenmeno indeseable que se acenta cuando es prximo a 1 es el apiamiento (ing: clustering). Muchos elementos no pueden ocupar su posicin preferida al estar ocupada por otro elemento que no tiene porqu ser un sinnimo (invasor). Grupos de sinnimos ms o menos dispersos acaban fundindose en grandes clusters y cuando buscamos una cierta clave tenemos que examinar no slo sus sinnimos sino un buen nmero de claves que no tienen relacin alguna con la clave buscada.

Redimensionamiento

Muchos lenguajes de programacin permiten crear tablas jando su tamao en tiempo de ejecucin. Entonces es posible utilizar la llamada tcnica de redimensionamiento (ing: resizing). Si el factor de carga es muy alto, superando un cierto valores umbral, se reclama a la memoria dinmica una tabla cuyo tamao es aproximadamente el doble del tamao de la tabla en curso y se reinserta toda la informacin de la tabla en curso sobre la nueva tabla.

Para ello se recorre secuencialmente la tabla en curso y cada uno de los elementos presentes se inserta por el procedimiento habitual en la nueva tabla, usando una funcin de hash distinta, obviamente. La operacin de redimensionamiento requiere tiempo proporcional al tamao de la tabla en curso (y por tanto (n)); pero hay que hacerla slo muy de vez en cuando. El redimensionamiento permite que el diccionario crezca sin lmites prejados, garantizando un buen rendimiento de todas las operaciones y sin desperdiciar demasiada memoria. La misma tcnica puede aplicarse a la inversa, para evitar que el factor de carga sea excesivamente bajo (desperdicio de memoria). Puede demostrarse que, aunque una operacin individual podra llegar tener coste (n), una secuencia de n operaciones de actualizacin tendr coste total (n).

Anlisis de Algoritmos Divide y vencers rboles binarios de bsqueda rboles balanceados (AVLs) Tablas de Hash Colas de prioridad Grafos y Recorridos

Una cola de prioridad (cat: cua de prioritat; ing: priority queue) es una coleccin de elementos donde cada elemento tiene asociado un valor susceptible de ordenacin denominado prioridad. Una cola de prioridad se caracteriza por admitir inserciones de nuevos elementos y la consulta y eliminacin del elemento de mnima (o mxima) prioridad.

template <typename Elem, typename Prio> class ColaPrioridad { public: ... // Aade el elemento x con prioridad p a la cola de // prioridad. void inserta(cons Elem& x, const Prio& p) throw(error) // Devuelve un elemento de mnima prioridad en la cola de // prioridad. Se lanza un error si la cola est vaca. Elem min() const throw(error); // Devuelve la mnima prioridad presente en la cola de // prioridad. Se lanza un error si la cola est vaca. Prio prio_min() const throw(error); // Elimina un elemento de mnima prioridad de la cola de // prioridad. Se lanza un error si la cola est vaca. void elim_min() throw(error); // Devuelve cierto si y slo si la cola est vaca. bool vacia() const throw(); };

// Tenemos dos arrays Peso y Simb con los pesos atmicos // y smbolos de n elementos qumicos, // p.e., Simb[i] = "C" y Peso[i] = 12.2. // Utilizamos una cola de prioridad para ordenar la // informacin de menor a mayor smbolo en orden alfabtico ColaPrioridad<double, string> P; for (int i = 0; i < n; ++i) P.inserta(Peso[i], Simb[i]); int i = 0; while(not P.vacia()) { Peso[i] = P.min(); Simb[i] = P.prio_min(); ++i; P.elim_min(); }

Se puede usar una cola de prioridad para hallar el k-simo elemento de un vector no ordenado. Se colocan los k primeros elementos del vector en una max-cola de prioridad y a continuacin se recorre el resto del vector, actualizando la cola de prioridad cada vez que el elemento es menor que el mayor de los elementos de la cola, eliminando al mximo e insertando el elemento en curso.

Muchas de las tcnicas empleadas para la implementacin de diccionarios puede usarse para implementar colas de prioridad (no las tablas de hash ni los tries) P.e., con rboles binarios de bsqueda equilibrados se puede conseguir coste O(log n) para inserciones y eliminaciones

Denicin
Un montculo (ing: heap) es un rbol binario tal que
1

todos las hojas (subrboles son vacos) se sitan en los dos ltimos niveles del rbol. en el antepenltimo nivel existe a lo sumo un nodo interno con un slo hijo, que ser su hijo izquierdo, y todos los nodos a su derecha en el mismo nivel son nodos internos sin hijos. el elemento (su prioridad) almacenado en un nodo cualquiera es mayor (menor) o igual que los elementos almacenados en sus hijos izquierdo y derecho.

Se dice que un montculo es un rbol binario quasi-completo debido a las propiedades 1-2. La propiedad 3 se denomina orden de montculo, y se habla de max-heaps o min-heaps segn que los elementos sean   que sus hijos.

Proposicin
1

El elemento mximo de un max-heap se encuentra en la raz. Un heap de n elementos tiene altura

h = dlog2 (n + 1)e:
La consulta del mximo es sencilla y eciente pues basta examinar la raz.

Cmo eliminar el mximo?


1

Ubicar al ltimo elemento del montculo (el del ltimo nivel ms a la derecha) en la raz, sustituyendo al mximo Reestablecer el invariante (orden de heap) hundiendo la raz.

El mtodo hundir intercambia un nodo dado con el mayor de sus dos hijos si el nodo es menor que alguno de ellos, y repete este paso hasta que el invariante de heap se ha reestablecido

Cmo aadir un nuevo elemento?


1

Colocar el nuevo elemento como ltimo elemento del montculo, justo a la derecha del ltimo o como primero de un nuevo nivel Reestablecer el orden de montculo otando el elemento recin aadido

En el mtodo flotar el nodo dado se compara con su nodo padre y se realiza el intercambio si ste es mayor que el padre, iterando este paso mientras sea necesario

Puesto que la altura del heap es (log n) el coste de inserciones y eliminaciones ser O(log n). Se puede implementar un heap mediante memoria dinmica, con apuntadores al hijo izquierdo y derecho y tambin al padre en cada nodo Pero es mucho ms fcil y eciente implementar los heaps mediante un vector. No se desperdicia demasiado espacio ya que el heap es quasi-completo; en caso necesario puede usarse el redimensionado

Reglas para representar un heap en vector:


1 2

A[1] contiene la raz. Si 2i  n entonces A[2i] contiene al hijo izquierdo del elemento en A[i] y si 2i + 1  n entonces A[2i + 1] contiene al hijo derecho de A[i] Si i  2 entonces A[i=2] contiene al padre de A[i]

Las reglas anteriores implican que los elementos del heap se ubican en posiciones consecutivas del vector, colocando la raz en la primera posicin y recorriendo el rbol por niveles, de izquierda a derecha.
template <typename Elem, typename Prio> class ColaPrioridad { public: ... private: // la componente 0 no se usa; el constructor de la clase // inserta un elemento ficticio vector<pair<Elem, Prio> > h; int nelems; void flotar(int j) throw(); void hundir(int j) throw(); };

template <typename Elem, typename Prio> bool ColaPrioridad<Elem,Prio>::vacia() const throw() { return nelems == 0; } template <typename Elem, typename Prio> Elem ColaPrioridad<Elem,Prio>::min() const throw(error) { if (nelems == 0) throw error(ColaVacia); return h[1].first; } template <typename Elem, typename Prio> Prio ColaPrioridad<Elem,Prio>::prio_min() const throw(error) { if (nelems == 0) throw error(ColaVacia); return h[1].second; }

template <typename Elem, typename Prio> void ColaPrioridad<Elem,Prio>::inserta(cons Elem& x, cons Prio& p) throw(error) { ++nelems; h.push_back(make_pair(x, p)); flotar(nelems); } template <typename Elem, typename Prio> void ColaPrioridad<Elem,Prio>::elim_min() const throw(error) { if (nelems == 0) throw error(ColaVacia); swap(h[1], h[nelems]); --nelems; h.pop_back(); hundir(1); }

// // // //

Versin recursiva. Hunde el elemento en la posicin j del heap hasta reestablecer el orden del heap; por hiptesis los subrboles del nodo j son heaps.

// Coste: O(log(n/j)) template <typename Elem, typename Prio> void ColaPrioridad<Elem,Prio>::hundir(int j) throw() { // si j no tiene hijo izquierdo, hemos terminado if (2 * j > nelems) return; int minhijo = 2 * j; if (minhijo < nelems and h[minhijo].second > h[minhijo + 1].second) ++minhijo; // // // if minhijo apunta al hijo de minima prioridad de j si la prioridad de j es mayor que la de su menor hijo intercambiar y seguir hundiendo (h[j].second > h[minhijo].second) { swap(h[j], h[minhijo]); hundir(minhijo);

} }

// // // //

Versin iterativa. Hunde el elemento en la posicin j del heap hasta reestablecer el orden del heap; por hiptesis los subrboles del nodo j son heaps.

// Coste: O(log(n/j)) template <typename Elem, typename Prio> void ColaPrioridad<Elem,Prio>::hundir(int j) throw() { bool fin = false; while (2 * j <= nelems and not fin) { int minhijo = 2 * j; if (minhijo < nelems and h[minhijo].second > h[minhijo + 1].second) ++minhijo; if (h[j].second > h[minhijo].second) { swap(h[j], h[minhijo]); j = minhijo; } else { fin = true; } } }

// Flota al nodo j hasta reestablecer el orden del heap; // todos los nodos excepto el j satisfacen la propiedad // de heap // Coste: O(log j) template <typename Elem, typename Prio> void ColaPrioridad<Elem,Prio>::flotar(int j) throw() { // si j es la raz, hemos terminado if (j == 1) return; int padre = j / 2; // si el padre tiene mayor prioridad // que j, intercambiar y seguir flotando if (h[j].second < h[padre].second) { swap(h[j], h[padre]); flotar(padre); } }

Heapsort

Heapsort (Williams, 1964) ordena un vector de n elementos construyendo un heap con los n elementos y extrayndolos, uno a uno del heap a continuacin. El propio vector que almacena a los n elementos se emplea para construir el heap, de modo que heapsort acta in-situ y slo requiere un espacio auxiliar de memoria constante. El coste de este algoritmo es (n log n) (incluso en caso mejor) si todos los elementos son diferentes. En la prctica su coste es superior al de quicksort, ya que el factor constante multiplicativo del trmino n log n es mayor.

// Ordena el vector v[1..n] // (v[0] no se usa) // de Elems de menor a mayor template <typename Elem> void heapsort(Elem v[], int n) { crea_max_heap(v, n); for (int i = n; i > 0; --i) { // saca el mayor elemento del heap swap(v[1], v[i]); // hunde el elemento de indice 1 // para reestablecer un max-heap en // el subvector v[1..i-1] hundir(v, i-1, 1); } }

// Da estructura de max-heap al // vector v[1..n] de Elems; aqu // cada elemento se identifica con su // prioridad template <typename Elem> void crea_max_heap(Elem v[], int n) { for (int i = n/2; i > 0; --i) hundir(v, n, i); }

Sea H (n) el coste en caso peor de heapsort y B (n) el coste de crear el heap inicial. El coste en caso peor de hundir(v; i 1; 1) es O(log i) y por lo tanto

H (n) = B (n) +

i =n X

i=1 0

O(log i)
X 1

= B (n) + O @

= B (n) + O(log(n!)) = B (n) + O(n log n)


Un anlisis simple de B (n) indica que B (n) = O(n log n) ya que hay (n) llamadas a hundir, cada una de las cuales tiene coste O(log n) Por tanto H (n) = O(n log n), de hecho H (n) = (n log n) en caso peor

1in

log2 iA

Podemos renar el anlisis de B (n):

B (n) =

nn=2 = O log (n=2)!   = O log(2e)n=2 = O(n)


Puesto que B (n) =
(n), podemos armar que B (n) = (n).

1ibn=2c

O(log(n=i))
!

Demostracin alternativa: Sea h = dlog2 (n + 1)e la altura del heap. En el nivel h 1 k hay como mucho

2h1k <

n+1
2k

nodos y cada uno de ellos habr de hundirse en caso peor hasta el nivel h 1; eso tiene coste O(k)

Por lo tanto,

B (n) =

0kh1
0

+1 O(k) n 2 k
X 1

= O @n
0

0kh1 2
X

kA
k

= O @n
ya que
X

k0 2

kA
k

= O(n);

En general, si jrj < 1,


X

k0 2

= 2:

k0

k rk =

(1 r)2

Aunque H (n) = (n log n), el anlisis detallado de B (n) es importante: utilizando un min-heap podemos hallar los k menores elementos en orden creciente de un vector (y en particular el k-simo) con coste:

S (n; k) = B (n) + k O(log n) = O(n + k log n):


y si k = O(n= log n) entonces S (n; k) = O(n).

Anlisis de Algoritmos Divide y vencers rboles binarios de bsqueda rboles balanceados (AVLs) Tablas de Hash Colas de prioridad Grafos y Recorridos

Denicin

Un grafo (no dirigido) es un par G = hV; E i donde V es un conjunto nito de vrtices (tambin llamados nodos) y E es un conjunto de aristas; cada arista e 2 E es un par no ordenado fu; vg donde u y v (u 6= v) son elementos de V . Un grafo dirigido o digrafo es un par G = hV; E i donde V es un conjunto nito de vrtices o nodos y E es un conjunto de arcos; cada arco e 2 E es un par (u; v ) donde u y v (u 6= v ) son elementos de V .

Denicin

Si en vez de un conjunto de arcos o aristas tenemos multiconjuntos de arcos o aristas, y se permiten bucles (esto es, arcos de la forma (u; u) o aristas fu; ug) entonces tenemos multigrafos (dirigidos y no dirigidos, respectivamente). Dado un arco e = (u; v ) se denomina origen a u y destino a v . Se dice que u es un predecesor de v y que v es un sucesor de u. Para una arista e = fu; vg se dice que u y v son sus extremos, que la arista es incidente a u y v , que u y v son adyacentes.

Denicin

Un camino P = v0 ; v1 ; v2 ; : : : ; vn de longitud n en un grafo G = hV; E i es una secuencia de vrtices de G tal que para toda i, 0  i < n fvi; vi+1g 2 E El vrtice v0 es el origen del camino P y vn es su destino. Un camino en un digrafo se dene de manera anloga: para cada i, (vi ; vi+1 ) es un arco del digrafo. Se dice que un camino es simple si no se repite ningn vrtice. Un camino en el que v0 = vn es un ciclo.

Denicin

Un grafo G = hV; E i es conexo si y slo si, para todo par de vrtices u y v , existe un camino que va de u a v en G. Para digrafos la denicin es idntica, slo que entonces se dice que el digrafo es fuertemente conexo.

Denicin

Dado un grafo G = hV; E i, el grafo H = hV 0 ; E 0 i se dice que es un subgrafo de G si y slo si V 0  V , E 0  E , y para toda arista e0 = (u0 ; v0 ) 2 E 0 se cumple que u0 y v0 pertenecen a V 0 . Si E 0 contiene todas las aristas de E que son incidentes a dos vrtices de V 0 , se dice que H es el subgrafo inducido por V 0 . La denicin de subgrafo de un grafo dirigido es completamente anloga.

Denicin

Una componente conexa C de un grafo G es un subgrafo inducido maximal conexo de G. Maximal quiere decir que si se aade cualquier vrtice v a V (C ) entonces el subgrafo inducido correspondiente no es conexoen particular, no existe camino entre v y los restantes vrtices de C . En el caso de digrafos, se habla de componentes fuertemente conexas: una componente fuertemente conexa del digrafo G es un subgrafo dirigido inducido maximal fuertemente conexo.

Denicin

Un grafo conexo y sin ciclos se denomina rbol (libre). Si G es un grafo conexo, un subgrafo T = hV; E 0 i (es decir que contiene los mismos vrtices que G) es un rbol de expansin si T es un rbol. Los rboles libres no tienen raz, y no existe orden entre los vrtices adyacentes a un vrtice dado del rbol.

Lema

Si G = hV; E i es un rbol entonces jE j = jV j 1.

A menudo trabajaremos con grafos o digrafos con etiquetas en las aristas o arcos: el etiquetado de un grafo o digrafo es una funcin  : E ! L entre el conjunto de aristas o arcos de G y el conjunto (eventualmente innito) de etiquetas L. Cuando las etiquetas son nmeros (enteros, racionales, reales), se dice que el grafo o digrafo es ponderado y a la etiqueta (e) de una arista o arco e se le suele denominar peso.

Implementacin

Los grafos se implementan habitualmente de una de las dos siguientes formas:


1

Mediante listas de adyacencia: tenemos una tabla T de listas enlazadas; la lista T [i] es la lista de sucesores del vrtice i

Mediante matrices de adyacencia: la componente A[i; j ] de la matriz nos dice si el arco entre los vrtices i y j existe o no; eventualmente, A[i; j ] puede contener tambin el peso del arco entre los vrtices i y j

Implementacin

La implementacin mediante matrices de adyacencia es muy costosa en espacio: si jV j = n, se requiere espacio (n2 ) para representar el grafo, independientemente del nmero de aristas/arcos que haya en el grafo. Slo es interesante si necesitamos poder decidir la existencia (o no) de una arista o arco con mxima eciencia. Por regla general, la implementacin que se usar es la de listas de adyacencia. El espacio que se requiere es (n + m), donde m = jE j y n = jV j; el espacio utilizado es por lo tanto lineal respecto al tamao del grafo.

typedef int vertex; typedef pair<vertex, vertex> edge; typedef list<edge>::iterator edge_iter; class Graph { // grafos no dirigidos con V = {0, ..., n-1} public: // crea un grafo vaco (sin vrtices y sin aristas) Graph(); // crea un grafo con n vrtices y sin aristas Graph(int n); // aade un vrtice al grafo; el nuevo vrtice tendr // identificador n, donde n era el nmero de vrtices // del grafo antes de aadir el vrtice void add_vertex(); // aade la arista (u,v) o e = (u,v) al grafo void add_edge(vertex u, vertex v); void add_edge(edge e); // consultoras del numero de vertices y de aristas int nr_vertices() const; int nr_edges() const; // devuelve lista de adyacentes a un vertice list<edge> adjacent(vertice u) const; ... private: int n, m; vector<list<edge> > T; ... }

Recorridos
Denicin

Dado un grafo conexo G = hV; E i, un recorrido del grafo es una secuencia que contiene todos y cada uno de los vrtices en V (G) exactamente una vez y tal que para cualquier vrtice v en la secuencia se cumple que, o bien v es el primer vrtice de la secuencia, o bien existe una arista e 2 E (G) que une al vrtice v con otro vrtice que le precede en la secuencia. El recorrido de un grafo es una secuencia de los recorridos de las componentes conexas de G. No hay ningn vrtice que aparezca en el recorrido sin que se haya completado el recorrido de las componentes conexas correspondientes a los vrtices que le preceden en el recorrido.

Recorridos

En el caso de los digrafos, un recorrido que comienza en un cierto vrtice v visitar todos y cada uno de los vrtices accesibles desde v exactamente una vez; la visita de un vrtice w 6= v implica que existe algn otro vrtice visitado previamente del cual w es sucesor. Los recorridos nos permiten visitar todos los vrtices de un grafo, con arreglo a la topologa del grafo, pues se utilizan las aristas o arcos del grafo para ir haciendo las sucesivas visitas.

Recorridos
Mediante un recorrido (o una combinacin de recorridos) podemos resolver ecientemente numerosos problemas sobre grafos. Por ejemplo Determinar si un grafo es conexo o no Hallar las componentes conexas de un grafo no dirigido Hallar las componentes fuertemente conexas de un grafo dirigido Determinar si un grafo contiene ciclos o no Decidir si un grafo es 2-coloreable, equivalentemente, si es bipartido Decidir si un grafo es biconexo o no (la eliminacin de vrtice no desconecta al grafo) Hallar el camino ms corto (con menor nmero de aristas/arcos) entre dos vrtices dados etc.

Recorridos

En el recorrido en profundidad (ing: Depth-First Search, DFS) de un grafo G, se visita un vrtice v y desde ste, recursivamente, se realiza el recorrido de cada uno de los vrtices adyacentes/sucesores de v no visitados. Cuando se visita por vez primera un vrtice v se dice que se ha abierto el vrtice; el vrtice se cierra cuaando se completa, recursivamente, el recorrido en profundidad de los vrtices adyacentes/sucesores.

Recorridos

La numeracin directa o numeracin DFS de un vrtice es el nmero de orden en el que se abre el vrtice; as, el vrtice en el que se inicia el recorrido tiene numeracin directa 1. La numeracin inversa de un vrtice es el nmero de orden en el que se cierra el vrtice. El primer vrtice del recorrido para el cual todos sus vecions/sucesores ya han sido visitados tiene numeracin inversa 1, y as sucesivamente.

. visitado, nd, ni, ndir , ninv son variables globales procedure DFS(G) for v V G do visitado v false nd v ni v

end if end for end procedure

[ ] := [ ] := 0; [ ] := 0 end for ndir := 0; ninv := 0 for v 2 V (G) do if :visitado[v ] then DFS-REC(G; v; v )

2 ( )

procedure DFS-REC(G, v , padre) PRE-VISIT v visitado v true

() [ ] := ndir := ndir + 1; nd[v ] := ndir for w 2 G:A DJACENT(v ) do if :visitado[w] then PRE-VISIT-EDGE(v; w) DFS-REC(G; w; v ) POST-VISIT-EDGE(v; w) else . si w 6= padre entonces hemos encontrado un ciclo
end if end for POST-VISIT v
ninv

end procedure

() := ninv + 1; ni[v] := ninv

// Macros para escribir recorridos en un grafo #define forall(v,G) for(vertex (v) = 0; (v) < (G).nr_vertices(); ++(v)) #define forall_adj(e, u, G) \ for(edge_iter (e) = (G).adjacent((u)).begin(); \ (e) != (G).adjacent((u))].end() ; ++(e)) #define target(eit) ((eit) -> second) #define source(eit) ((eit) -> first)

void DFS(const Graph& G) { vector<bool> visitado(G.nr_vertices(), false); vector<int> nd(G.nr_vertices(), 0); vector<int> ni(G.nr_vertices(), 0); int ndir = 0; int ninv = 0; forall(v, G) if (not visitado[v]) DFS(G, v, v, visitado, ndir, ninv, nd, ni); }

void DFS(const Graph& G, vertex v, vertex padre, vector<bool>& visitado, int& ndir, int& ninv, vector<int>& nd, vector<int>& ni) { PRE-VISIT(v) visitado[v] = true ++ndir; nd[v] = ndir; forall_adj(e, v, G) { vertex w = target(e); if (not visitado[w]) { PRE-VISIT-EDGE(v,w) DFS(G, w, v, visitado, ndir, ninv, nd, ni) POST-VISIT-EDGE(v,w) } else { // si w != padre entonces hemos encontrado un ciclo } } POST-VISIT(v) ++ninv; ni[v] = ninv; }

Recorridos

El recorrido en profundidad de una componente conexa induce un rbol de expansin, al que se denomina rbol del recorrido TDFS . Las aristas de la componente conexa se clasican entonces en aristas de rbol (tree edges) y aristas de retroceso (backward edges). stas ltimas unen el vrtice que se acaba de abrir con un vrtice visitado (abierto o cerrado) y por tanto cierran ciclos. El recorrido de todo el grafo da lugar a un bosque de expansin.

Para cada vrtice v , CC [v ] ser el nmero de la componente conexa en la que est v . T reeEdges[i] es el conjunto de aristas de rbol en la componente i, y BackEdges[i] el conjunto de aristas de retroceso. El nmero de componentes conexas viene dado por ncc.
procedure DFS(G) for v 2 V G do
visitado v CC v

end if end for end procedure

( ) [ ] := false [ ] := 0 end for ncc := 0 for v 2 V (G) do if :visitado[v ] then ncc := ncc + 1 T reeEdges[ncc] := ; BackEdges[ncc] := ; DFS(G; v; v )

procedure DFS(G, v , padre) visitado v true


CC v for w

2 G:A DJACENT v do if :visitado w then

end if end if end for end procedure

() [ ] T reeEdges[ncc] := T reeEdges[ncc] [ f(v; w )g DFS(G; w; v ) else if w 6= padre then BackEdges[ncc] := BackEdges[ncc] [ f(v; w )g

[ ] := [ ] := ncc

a e i g b c d h f l

j k

k h d i
e

tree edge backward edge

Ejemplos

Detectar ciclos o calcular las componentes conexas son ejemplos triviales de aplicacin del recorrido en profundidad. Otro algoritmo simple que se puede disear aplicando el DFS es determinar si un grafo es bipartido. Recordemos que un grafo G es bipartido si existe una particin hA; B i del conjunto de vrtices V (es decir, A [ B = V ; A \ B = ;) tal que toda arista de G tiene un extremo en A y el otro en B : Equivalentemente, G es bipartido si y slo si es 2-coloreable, y si y slo si no contiene ningn ciclo de longitud impar (porqu?)

Ejemplos: Bipartido

procedure E S B IPARTIDO(G) for v 2 V G do


color v

end if end for return esbip end procedure

( ) [ ] := 1 end for color := 0 esbip := true for v 2 V (G) while esbip do if color[v ] 6= 1 then esbip := E S B IPARTIDO(G; v; v; color )

Ejemplos: Bipartido

procedure E S B IPARTIDO(G, v , padre, c) esbip true


color v c for w G:A DJACENT v while esbip do if color v then esbip E S B IPARTIDO G; w; v;

end if end for return esbip end procedure

:= [ ] := 2 () [ ] = 1 := ( else esbip := c 6= color [w ]

1 c)

Ejemplos: Biconexo
Un grafo conexo G se dice que es biconexo si al eliminar uno cualquiera de sus vrtices sigue siendo conexo. Supongamos que efectuamos un DFS empezando en el vrtice r y que para cada vrtice u determinamos el nmero DFS ms bajo que es alcanzable siguiendo un camino desde u hasta uno de sus descendientes en el TDFS y a continuacin subimos con una arista de retroceso. Denominaremos mas_alto[u] a dicho nmero.
r x mas_alto[u] = nd[x]

Ejemplos: Biconexo
Un vrtice u es un punto de articulacin de G si su eliminacin desconecta el grafo. Si G no tiene puntos de articulacin entonces G es biconexo. Para ver si un vrtice es punto de articulacin o no debermos considerar varios casos:
1

u es una hoja del TDFS (no tiene descendientes): entonces


no es un punto de articulacin ya que nunca se desconectar el grafo

u = r es la raz del TDFS : es punto de articulacin si y slo si tiene ms de un descendiente en el TDFS , su eliminacin
desconectara los subrboles, pero si slo hay uno su eliminacin no desconecta a los dems vrtices.

Ejemplos: Biconexo

u no es hoja y no es la raz: entonces es punto de


articulacin si y slo si alguno de los vrtices adyacentes descendientes w de u cumple mas_alto[w]  nd[u], es decir, si algn descendiente no puede escapar del subrbol enraizado en u sin pasar por u.

Por otro lado para cada vrtice u, mas_alto[u] es el mnimo entre: 1) nd[u], 2) mas_alto[v ] para todo v descendiente directo de u en el TDFS y 3) nd[v ] para todo v adyacente a u mediante una arista de retroceso.

Ejemplos: Biconexo
procedure B ICONEXO(G) for v 2 V G do visitado v false

end if end for return es_biconexo end procedure

( ) [ ] := nd[v ] := 0 punto_art[v ] := false num_desc[v ] := 0 end for num_dir := 0 es_biconexo := true for v 2 V (G) do if :visitado[v ] then B ICONEXO(G; v; v; es_biconexo; nd; : : :)

Ejemplos: Biconexo
procedure B ICONEXO(G, v , padre, . . . ) num_dir num_dir nd v num_dir visitado v true mas_alto v nd v for w 2 G:A DJACENT v do if :visitado w then num_desc v num_desc v B ICONEXO G; w; v; : : : mas_alto v min mas_alto v ; mas_alto w punto_art v punto_art v _ mas_alto w  nd v else if padre 6 w then mas_alto v min mas_alto v ; nd w end if end if end for if v padre then . v es la raz del TDFS punto_art v num_desc v > end if

:= + 1; [ ] := [ ] := [ ] := [ ] () [ ] [ ] := [ ]+1 ( ) [ ] := ( [] [ ]) [ ] := [] [ ] [] = [ ] := ( [ ] [ ])

[ ] :=

[] 1

Anlisis del DFS

Supongamos que el trabajo que se realiza en cada visita de un vrtice (PRE- y POST-VISIT) y cada visita de una arista, sea del rbol o de retroceso, es (1). Entonces el coste del DFS es
X

v 2V

(1) + (grado(v )) = @
0

X

v 2V

1 + grado(v )
1

1 A

v 2V

1+

v 2V

grado(v )A = (n + m)

Recorridos

El recorrido en profundidad de un digrafo tiene propiedades algo distintas de las del DFS de un grafo no dirigido. Al lanzar un DFS desde un vrtice v de un digrafo G se visitarn los vrtices (no visitados) de la componente fuertemente conexa de v pero adems todos los otros vrtices accesibles desde v . El DFS de un digrafo induce sucesivos rboles de recorrido, enraizados en los vrtices desde los cuales se lanzan los recorridos recursivos. Los conceptos de numeracin DFS y de numeracin inversa son igual que en el caso de grafos no dirigidos.

Recorridos
El DFS de un digrafo tambin induce una clasicacin de los arcos, pero es algo diferente:
1

Arcos de retroceso: van del vrtice v en curso a un vrtice antecesor w en el rbol TDFS ; se cumple que nd[w] < nd[v ] y que nd[w] est abierto Arcos de avance (forward edges): van del vrtice v en curso a un vrtice descendiente pero previamente visitado w en el TDFS ; se cumple que nd[v] < nd[w]

Arcos de rbol: van del vrtice v en curso a un vrtice no visitado w

Arcos de cruce (cross edges): van del vrtice v en curso a un vrtice previamente visitado w en el mismo TDFS o en un rbol diferente; se cumple que nd[w] < nd[v ], pero el vrtice w ya est cerrado (ni[w] 6= 0)

Recorridos

a e i g b c d h f l

tree edge
j k m

k backward edge forward edge cross edge

Resto de la seccin sobre Grafos y Recorridos:

Parte II Algoritmos Voraces


8

Algoritmos Voraces Simples y Tcnicas Generales Caminos Mnimos: Algoritmo de Dijkstra rboles de Expansin Mnimos: Algoritmos de Kruskal y de Prim Particiones Cdigos de Huffman

10

11

12

Algoritmos Voraces Simples y Tcnicas Generales Caminos Mnimos: Algoritmo de Dijkstra rboles de Expansin Mnimos: Algoritmos de Kruskal y de Prim Particiones Cdigos de Huffman

10

11

12

Algoritmos Voraces Simples y Tcnicas Generales Caminos Mnimos: Algoritmo de Dijkstra rboles de Expansin Mnimos: Algoritmos de Kruskal y de Prim Particiones Cdigos de Huffman

10

11

12

Algoritmo de Dijkstra
Dado un grafo dirigido G = hV; E i etiquetado, el algoritmo de Dijkstra (1959) nos permite hallar los caminos mnimos desde un vrtice s 2 V dado a todos los restantes vrtices del grafo. Si el grafo G contuviera ciclos de peso negativo la nocin de camino mnimo no estara bien denida para todo par de vrtices, pero el algoritmo de Dijkstra puede fallar en presencia de arcos con peso negativo, incluso si no hay ciclos de peso negativo. Supondremos por lo tanto que todos los pesos ! : E ! R+ son positivos. Sea P (u; v ) el conjunto de caminos entre dos vrtices u y v de G. Dado un camino  = [u; : : : ; v] 2 P (u; v) su peso es la suma de los pesos de los n arcos que lo forman:

!() = !(u; v1 ) + !(v1 ; v2 ) + + !(vn1 ; v):

Sea (u; v ) = m nf! ( ) j  2 P (u; v )g y  (u; v ) un camino de P (u; v ) cuyo peso es mnimo. Si P (u; v ) = ; entonces tomamos (u; v ) = +1, por convenio. Consideraremos en primer lugar la versin del algoritmo de Dijkstra que calcula los pesos de los caminos mnimos desde un vrtice a todos los restantes, y ms tarde la versin que calcula adicionalmente los caminos propiamente dichos.

.G .s

DIJKSTRA G; s; D . Para todo u V , D u

= hV; E i es un grafo dirigido con pesos positivos 2V (


2

[ ] = (s; u) ) [ ] = (s; u) [ ] +1 =)

.G .s

DIJKSTRA G; s; D; cam . Para todo u V , D u . Para todo u V , D u <

= hV; E i es un grafo dirigido con pesos positivos 2V (


2 2
cam u

[ ] = (s; u)

El algoritmo de Dijkstra acta en una serie de etapas. En todo momento el conjunto de vrtices V se divide en dos partes: los vrtices vistos y los vrtices no vistos o candidatos. Al inicio de cada etapa, si u es un vrtice visto entonces D[u] = (s; u); si u es un candidato entonces D[u] es el peso del camino mnimo entre s y u que pasa exclusivamente por vrtices intermedios vistos. Este es el invariante del algoritmo de Dijkstra. En cada etapa un vrtice pasa de ser candidato a ser visto. Y cuando todos los vrtices son vistos entonces tenemos completamente resuelto el problema.

Qu vrtice candidato debe seleccionarse en cada etapa para pasar a ser visto? Intuitivamente, aqul cuya D sea mnima de entre todos los candidatos. Sea u dicho vrtice. Entonces D[u] no slo es el peso del camino mnimo entre s y u que slo pasa por vistos (segn el invariante), sino el peso del camino mnimo. En efecto, si el camino mnimo pasase por algn otro vrtice no visto x, tendramos que el peso de dicho camino es D[x] + (x; u) < D[u], pero como (x; u)  0 y D[u] es mnimo llegamos a una contradiccin.

Ahora debemos pensar cmo mantener el resto del invariante. Para los vrtices vistos D ya tiene el valor adecuado (includo u, el vrtice que pasa de candidatos a vistos, como acabamos de ver). Pero si v es un candidato, D[v ] tal vez haya de cambiar ya que tenemos un vrtice adicional visto, el vrtice u. Un sencillo razonamiento demuestra que si el camino mnimo entre s y v que pasa por vrtices vistos incluye a u entonces u es el inmediato antecesor de v en dicho camino.

De ah se sigue que el valor de D slo puede cambiar para los vrtices v sucesores de u. Esto ocurrir si y slo si

D[v] > D[u] + !(u; v):


Si v fuera sucesor de u pero ya estuviera visto la condicin anterior no puede ser cierta.

procedure DIJKSTRA(G, s, D) . D es el resultado; D u s; u para todo u 2 V G . cand es un subconjunto de vrtices de V G for v 2 V G do

[ ] = [ ]

end if end for end while end procedure

( ) [ ] := +1 end for D [s] := 0 cand := V (G) while cand 6= ; do u := el vrtice de cand con D mnima cand := cand fug for v 2 S UCESORES(G; u) do d := D [u] + P ESO(G; u; v ) if d < D[v ] then D [v ] := d
Dv

( )

( )

1 0 0 0 0 0 0

1 1 1 1 1 f1; 2; 3; 4; 5; 6g 3 6 1 1 1 f2; 3; 4; 5; 6g 3 6 7 4 1 f3; 4; 5; 6g 3 6 7 4 9 f3; 4; 6g 3 6 7 4 8 f4; 6g 3 6 7 4 8 f6g ;

CANDIDATOS

Para conseguir que el algoritmo compute los caminos mnimos propiamente dichos, empezamos observando que si el camino mnimo entre s y u pasa por x entonces la parte de ese camino que nos lleva de s a x ha de ser necesariamente el camino mnimo entre s y x. Por lo tanto, bastar con computar un rbol de caminos mnimos que implcitamente se representa mediante una tabla cam tal que:

? donde cam[v ] = ? indica que cam[v ] no est denido.

cam[v] = >u
> :

8 > >s <

si v = s, si (u; v ) es el ltimo arco de  (s; v ), si (s; v ) = +1,

Claramente, los nicos cambios que sern necesarios son:


1 2

inicializar cam[s] := s

incluir la actualizacin de cam en el bucle interno

... for v
d

end if end for ...

2 S UCESORES(G; u) do := D[u] + PESO(G; u; v) if d < D[v ] then D [v ] := d cam[v ] := u

Si queremos el camino completo de s a v podemos deshacer el recorrido a la inversa:

cam[u]; cam[cam[u]]; : : : ; cam[ [cam[u]] ]


hasta que lleguemos a s.

Sea n el nmero de vrtices de G y m el nmero de arcos. Si el grafo G se implementa mediante matriz de adyacencias, el coste del algoritmo de Dijkstra es
(n2 ) ya que se hacen n iteraciones del bucle principal y dentro de cada una de ellas se incurrira en un coste como mnimo
(n) para recorrer los sucesores del vrtice seleccionado. Descartaremos esta posibilidad, y supondremos que el grafo se implementa mediante listas de adyacencia que nos darn mejores resultados tanto en tiempo como en espacio.

Supongamos, para simplicar, que V (G) = f1; : : : ; ng y que implementamos el conjunto cand y el diccionario D mediante sencillas tablas indexadas de 1 a n (cand[i] = cierto si y slo si el vrtice i es un candidato). Entonces el coste del algoritmo de Dijkstra es (n2 + m) = (n2 ) puesto que se hacen n iteraciones del bucle principal, buscar el mnimo de la tabla D en cada iteracin tiene coste (n) y actualizar la tabla D tiene coste proporcional al nmero de sucesores del vrtice seleccionado (combinando la obtencin de los vrtices sucesores y la etiqueta de los arcos correspondientes).

Si V (G) es un conjunto arbitrario, podemos conseguir el mismo rendimiento utilizando tablas de hash para implementar cand y D. Y el problema sigue siendo la ineciencia en la seleccin del vrtice con D mnima. Para mejorar la eciencia podemos convertir cand en una cola de prioridad cuyos elementos son vrtices y donde la prioridad de cada vrtice es su correspondiente valor de D . cand es una cola de prioridad de mnimos, cuyos . elementos son vrtices de G y la prioridad de un . elemento u es D[u] ... . D[v] = +1 para todo v 6= s; D[s] = 0 for v 2 V (G) do cand:I NSERTA(v; D[v]) end for while :cand:ES _ VACIA() do u := cand:MIN(); cand:ELIM _ MIN() for v 2 S UCESORES(G; u) do ...

El problema es que dentro del bucle que recorre los sucesores de u se puede modicar el valor de D (la prioridad) de vrtices candidatos. Por ello deberemos dotar a la cola de prioridad de una operacin adicional que, dado un elemento, nos permita decrementar su prioridad (los valores de D se modican siempre a la baja). ... for v 2 S UCESORES(G; u) do d := D[u] + P ESO(G; u; v) if d < D[v ] then

D[v] := d cand:DECR _ PRIO(v; d)

end if end for ...

Si los costes de las operaciones sobre la cola de prioridad son O(log n) entonces el coste del bucle principal es

D(n) =

= (n log n) + O(log n)

v 2 V (g )

O(log n) (1 + # sucesores de v)
X

= (n log n) + O(m log n) = O((n + m) log n)


Por otra parte, el coste de las inicializaciones previas es O(n log n). Por lo tanto el coste que nos queda es O((n + m) log n). Puede demostrarse que, de hecho, el coste en caso peor del algoritmo de Dijkstra es ((m + n) log n). Pero en muchos casos el coste es menor, porque no hay que usar decr_prio para todos los sucesores del vrtice seleccionado y/o porque el coste de las operaciones sobre la cola de prioridad es frecuentemente menor que (log n).

v2V (g)

# sucesores de v

Al crear cand sabemos cuntos vrtices tiene el grafo y podemos crear dinmicamente una estructura de datos para ese tamao. Adicionalmente podemos evitar la redundancia de la tabla D, ya que para cada elemento de cand tenemos su prioridad, que es su valor de D. Necesitamos por lo tanto una clase con la funcionalidad combinada tpica de las colas de prioridad y de los diccionarios:
template <class Elem, class Prio> class ColaPrioDijkstra<Elem,Prio> { public: ColaPrioDijkstra(int n = 0); void inserta(const Elem& e, const Prio& prio); Elem min() const; void elim_min(); Prio prio(const Elem& e) const; bool contiene(const Elem& e) const; void decr_prio(const Elem& e, const Prio& nuevaprio); bool es_vacia() const; ... };

typedef vector<list<pair<int,double> > > grafo; typedef list<pair<int,double> >::const_iterator arco; ... void Dijkstra(const grafo& G, int s, ... ) { ColaPrioDijkstra<int,double> cand(G.size()); for (int v = 0; v < G.size(); ++v) cand.inserta(v, INFINITY); cand.decr_prio(s, 0); while(not cand.es_vacia()) { int u = cand.min(); double du = cand.prio(u); cand.elim_min(); for (arco e = G[u].begin(); e != G[u].end(); ++e) { // e apunta al par <v, peso(u,v)> // e -> first() == v // e -> second() == peso(u,v) int v = e -> first(); if (cand.contiene(v)) { double d = du + e -> second(); if (d < cand.prio(v)) cand.decr_prio(v, d); } } } }

Pueden conseguirse costes logartmicos o inferiores en todas las operaciones sobre la cola de prioridad utilizando un heap implementado en vector. Adems necesitaremos una tabla de hash que nos permita traducir vrtices a ndices. Y una tabla de ndices y otra de posiciones en el heap.
template <class Elem, class Prio> class ColaPrioDijkstra<Elem,Prio> { ... private: vector<Prio> prio; vector<Elem> elems; int nelems; // == elems.size() vector<int> index; vector<int> pos; hash_map<Elem,int> identif;

Los vectores prio y elems, junto al contador nelems representan al heap. Si elems[i] = ej entonces index[i] = j y pos[j ] = i, es decir, pos nos da la posicin en el heap del elemento ej e index el identicador del elemento en la posicin i del heap, siendo identif [ej ] == j .

La constructora de la clase crea las tablas prio, e, index y pos con n componentes y el map. Cada vez que se inserta un nuevo elemento se le asigna el ndice nelems + 1, se inserta en identif , se coloca en el heap en la posicin nelems + 1, y se le hace otar. La operacin flotar se encarga de mantener actualizadas las tablas pos e index. Para elim_min, se intercambia el ltimo nodo del heap con la raz
// intercambiamos el ultimo elemento del heap con la raz swap(prio[1],prio[nelems]); swap(elems[1], elems[nelems]); // actualizamos pos e index swap(pos[index[1]], pos[index[nelems]]); swap(index[1],index[nelems]);

y a continuacin se hunde la raz, cuidando de mantener tambin actualizadas las tablas pos e index.

La operacin decr_prio requiere averiguar el identicador j del elemento ej cuya prioridad se va a decrementar usando la tabla de hash, usar la tabla pos[j ] para obtener la posicin i del nodo que le corresponde en el heap, y una vez modicada la prioridad, otar el nodo. Es fcil comprobar que las operaciones de la clase, exceptuando la constructora, tienen coste (1) (es_vacia, min, prio, contiene) o O(log n) (inserta, elim_min, decr_prio).

Algoritmos Voraces Simples y Tcnicas Generales Caminos Mnimos: Algoritmo de Dijkstra rboles de Expansin Mnimos: Algoritmos de Kruskal y de Prim Particiones Cdigos de Huffman

10

11

12

rboles de Expansin Mnimos

Dado un grafo no dirigido y conexo G = hV; E i con pesos o costes en las aristas ! : E ! R, un rbol de expansin mnimo (MST: minimum spanning tree) T = hV; Ai es un subgrafo de G tal que tiene el mismo conjunto de vrtices (V (T ) = V (G)), es un rbol (es decir, es conexo y acclico) y su coste

!(T ) =

e2A

!(e)

es mnimo entre todos los posibles rboles de expansin de G.

3 2 8 2 9 8 9 4 6 3 10 7

3 2 8 2 9 8 9 4 6 3 10 7

Existen multitud de algoritmos para calcular un MST de un grafo. No obstante todos ellos siguen un esquema voraz:

;; Candidatas E while jAj 6 jV G j do Seleccionar una arista e 2 Candidatas que no crea un ciclo en T
A A

:=

:= = ( ) 1

Candidatas end while

:= A [ feg := Candidatas feg

Diremos que un conjunto de aristas A  E (G) es prometedor si y slo si:


1 2

A no contiene ciclos A es un subconjunto de las aristas de un MST del grafo G

Un corte del grafo G es una particin de su conjunto de vrtices en dos subconjuntos C y C 0 no vacos y disjuntos:

V (G) = C [ C 0 ;

C \ C0 = ;

Una arista e respeta un corte hC; C 0 i si sus dos extremos estn ambos en C o ambos en C 0 , en caso contrario se dice que e cruza el corte.

Teorema

Sea A un conjunto prometedor de aristas que respetan un cierto corte hC; C 0 i del grafo G. Sea e 2 E (G) la arista de mnimo peso que cruza el corte hC; C 0 i. Entonces

A [ feg
es prometedor

El teorema anterior nos da la receta para disear algoritmos de clculo de un MST: una vez que hayamos denido cul es el corte que corresponde a cada iteracin, la seleccin de la arista consistir en localizar la arista e de mnimo peso que cruza el corte. Por denicin, como A respeta el corte y e lo cruza, e no puede crear un ciclo en A. Ms an: la correccin de los algoritmos que se fundamentan en esta idea queda automticamente establecida.

Demostracin: Sea A0 el conjunto de aristas de un MST T 0 tal que A  A0 . Puesto que A respeta el corte, al menos una arista de las que cruzan el corte tendra que pertenecer a A0 , en caso contrario A0 no sera conexo. Supongamos que una de dichas aristas es e0 . Si e0 es la arista de mnimo peso que cruza el corte, como A [ fe0 g es prometedor, hemos demostrado el teorema. Pero que ocurre si e0 no es la arista de mnimo peso que cruza el corte?

Demostracin (cont.): El coste del MST T 0 incluir el coste de las aristas de A, el coste ! (e0 ) y el coste de otras aristas. Si agregsemos a T 0 la arista e de mnimo peso que cruza el corte, crearamos un ciclo, porque T 0 es un rbol. Sea e0 la arista de T 0 que cruza el corte forma parte de dicho ciclo. De manera que

T = T 0 [ feg fe0 g
tambin es un rbol de expansin. Y su coste es menor o igual que el de T 0 puesto que slo cambiamos el coste de e0 por el de e, que es el mnimo. Como T 0 es MST, llegamos a una contradiccin a menos que e y e0 tengan el mismo coste, y por tanto T y T 0 tendran el mismo coste. El teorema queda demostrado, ya que A [ feg es un subconjunto de las aristas de T , que es un MST.

podemos sustituir e por e para obtener un nuevo MST

Algoritmo de Prim

En el algoritmo de Prim, se mantiene un conjunto de vrtices Vistos  V (G), y en cada etapa del algoritmo se selecciona la arista de mnimo peso que une a un vrtice u de Vistos con un vrtice v no visto (i.e., en V (G) Vistos). Dicha arista no puede crear un ciclo y se aade al conjunto A. Asimismo el vrtice v pasa a ser de Vistos. La correccin del algoritmo es inmediata; el conjunto A respeta el corte hVistos; V (G) Vistosi y seleccionamos la arista de mnimo peso que cruza el corte para agregarla a A.

procedure P RIM(G) A ;; Vistos fsg Candidatas ; for v 2 G:A DYACENTES s do Candidatas Candidatas [ f s; v end for while jAj 6 jV G j do Seleccionar la arista e u; v de Candidatas de mnimo peso

:=

:= := () := = ( ) 1

( )g

=( ) ()

Sea u el vrtice en Vistos: Vistos Vistos [ fv g for w 2 G:A DYACENTES v do if w 62 Vistos then Candidatas Candidatas [ f v; w end if end for end while return A end procedure

:= A [ feg :=

:=

)g

3 2 8 2 9 8 9 4 6 3 10 7

3 2 8 2 9 8 9 4 6 3 10 7

3 2 8 2 9 8 9 4 6 3 10 7

3 2 8 2 9 8 9 4 6 3 10 7

3 2 8 2 9 8 9 4 6 3 10 7

3 2 8 2 9 8 9 4 6 3 10 7

3 2 8 2 9 8 9 4 6 3 10 7

El coste del algoritmo de Prim depender de cmo implementemos la seleccin de la arista candidata. El bucle principal hace n 1 iteraciones, pues en cada iteracin aadimos una arista al rbol A, o equivalentemente, en cada iteracin vemos un vrtice nuevo y terminaremos cuando todos estn vistos. En el interior del bucle principal se hacen dos tareas costosas: seleccionar la arista de mnimo peso en Candidatas y aadir ciertas aristas adyacentes al vrtice recin visto al conjunto de Candidatas.

El coste de la segunda tarea es proporcional al grado del vrtice v y en total aportar al coste
X

v2V (G)

(grado(v )) = (

v2V (G)

grado(v )) = (m)

Pero si usamos una implementacin ingenua para el conjunto de Candidatas, el coste de seleccionar una arista es O(m) y el coste total del algoritmo de Prim es O(n m).

Para conseguir mejorar sensiblemente el coste del algoritmo el conjunto de Candidatas debe implementarse como un min-heap, siendo la prioridad de cada arista su coste. Entonces la seleccin (y eliminacin) de la arista de coste mnimo en cada iteracin pasa a ser O(log m). Por otro lado, el coste de ir aadiendo aristas a Candidatas lo tenemos que recalcular:
X 0

v2V (G)

(grado(v )(1 + log m)) = @

= (m log m)

v2V (G)

grado(v )(1 + log m)A

En total: ((n + m) log m)

Algoritmo de Kruskal

En el algoritmo de Kruskal se mantiene en todo momento un conjunto A de aristas que es un bosque (un conjunto de rboles). En cada paso se toma una arista e de mnimo peso entre todas las posibles; si dicha arista cierra un ciclo se descarta, si no cierra un ciclo, e se agrega a A. Cuando la arista e = (u; v ) escogida no cierra un ciclo, se dene como corte el que forman los vrtices en la misma componente conexa de u (mediante aristas de A) por un lado, y los restantes vrtices del grafo por otro. Claramente A respeta el corte y e es la arista de mnimo peso que respeta ese corte.

procedure K RUSKAL(G hV; E i) Ordenar E por peso creciente


A

while jAj 6

end if end while return A end procedure

:= ; = jV (G)j 1 do e = (u; v ) := S IGUIENTE(E ) if e no cierra un ciclo en A then A := A [ feg

3 2 8 2 9 8 4 4 6 3 10 7

3 2 8 2 9 8 4 4 6 3 10 7

3 2 8 2 9 8 4 4 6 3 10 7

3 2 8 2 9 8 4 4 6 3 10 7

3 2 8 2 9 8 4 4 6 3 10 7

3 2 8 2 9 8 4 4 6 3 10 7

3 2 8 2 9 8 4 4 6 3 10 7

3 2 8 2 9 8 4 4 6 3 10 7

3 2 8 2 9 8 4 4 6 3 10 7

Para determinar si una cierta arista e cierra un ciclo en A de manera eciente usaremos una estructura de datos astuta denominada PARTICION (tambin se les conoce como MFS ETS y U NION -F IND). Inicialmente creamos una PARTICION en la que cada vrtice del grafo constituye un bloque por s solo. Cada vez que agreguemos una arista e = (u; v ) al conjunto A, los bloques a los que pertenecen u y v se fusionan en un solo bloque (merge, union). Cada bloque de vrtices es pues una componente conexa de A; si existe un camino entre los vrtices w y w0 usando las aristas de A, w y w0 pertenecern a un mismo bloque de la PARTICIN.

El test que determina si un nueva arista cierra un ciclo entonces consiste simplemente en ver si los vrtices u y v pertenecen a un mismo bloque de la PARTICIN o no. Si ya estn conectados, la nueva arista cerrara un ciclo; en caso contrario, la nueva arista no cierra un ciclo y los vrtices pasan a estar conectados. Habitualmente la clase PARTICIN proporciona una operacin F IND que dado un elemento u nos devuelve el representante del bloque en el que est u. Dos elementos estn en el mismo bloque si y slo si los representantes de sus bloques son idnticos.

procedure K RUSKAL(G hV; E i) Ordenar E por peso creciente . Se crea una particin inicial, cada vrtice en un bloque distinto P:M AKE _U NION F IND V
A

end if end while return A end procedure

( ) := ; while jAj = 6 jV (G)j 1 do e = (u; v ) := S IGUIENTE(E ) if P:F IND(u) 6= P:F IND(v ) then A := A [ feg P:U NION(u; v )

Para calcular el coste del algoritmo de Kruskal tenemos por un lado el coste de la ordenacin de las aristas (m log m) = (m log n), siendo m = jE (G)j. La creacin de la particin inicial tiene coste (n), siendo n = jV (G)j. Por otro lado, el bucle siguiente hace al menos n 1 iteraciones (tantas como aristas tiene el MST hallado) pero puede llegar a hacer m iteraciones en caso peor. El coste del cuerpo del bucle vendr dominado por el coste de las llamadas a las operaciones F IND y U NION. Si el bucle principal realiza N iteraciones, n 1  N  m, se harn 2N llamadas a F IND. Por otro lado, se hacen siempre exactamente n 1 llamadas a U NION.

Es muy fcil obtener implementaciones de la PARTICIN que garanticen que el coste de F IND y U NION es O(log n), lo que nos llevara a un coste

(m log n) + O(m log n) = (m log n)


para el algoritmo.

Se puede mejorar el rendimiento notablemente (aunque no en caso peor) de la siguiente forma:


1

En vez de ordenar las aristas por peso, se crea un min-heap con coste (m) Se usa una versin de U NION -F IND que garantiza que O(m) F INDs + O(n) U NIONs tienen coste O((m + n) (m; n). La funcin (m; n), llamada funcin inversa de Ackermann crece de manera extremadamente lenta, a los efectos prcticos, por muy grandes que sean m y n, (m; n)  4. El bucle principal seguir teniendo coste O(m log n) pero en la mayora de casos ser ms cercano a O(n log n) porque no se suelen hacer muchas ms del mnimo de n 1 iteraciones.

El coste del algoritmo vendr dominado por el coste de extraer aristas durante las iteraciones del bucle principal y en caso peor es (m log n); en promedio ser cercano a (n log n), lo cual es muy ventajoso, sobre todo si m  n.

Algoritmos Voraces Simples y Tcnicas Generales Caminos Mnimos: Algoritmo de Dijkstra rboles de Expansin Mnimos: Algoritmos de Kruskal y de Prim Particiones Cdigos de Huffman

10

11

12

Particiones

Una particin de un conjunto no vaco A es una coleccin de subconjuntos no vacos = fA1 ; : : : ; Ak g tal que
1 2

Si i 6= j entonces Ai \ Aj

A=

1ik

Ai.

= ;.

Ais. Las particiones y las relaciones de equivalencia estn estrechamente ligadas. Recordemos que  es una relacin de equivalencia en A si y slo si  es reexiva: para todo a 2 A, a  a.  es transitiva: si a  b y b  c, entonces a  c, para cualesquiera a, b y c en A.  es simtrica: a  b si y slo si b  a, para cualesquiera a y b en A.
1 2 3

Se suele denominar bloque de la particin a cada uno de los

Dada una particin de A, sta induce una relacin de equivalencia  denida por

x  y () x e y pertenecen a un mismo bloque Ai 2


Y a la inversa, dada una relacin de equivalencia  en A, sta induce una particin = fAx gx2A , donde

Ax = fy 2 A j y  xg:

Al subconjunto de elementos equivalentes a x se le denomina clase de equivalencia de x. Cada uno de los bloques de la particin inducida por una relacin  es por lo tanto una clase de equivalencia. Ntese que si x  y entonces Ax = Ay . Un elemento cualquiera de la clase Ax se denomina representante de la clase. En muchos algoritmos, especialmente en algoritmos sobre grafos, es importante poder representar particiones de un conjunto nito de manera eciente.

Vamos a suponer que el conjunto soporte sobre el que se dene la particin es f1; : : : ; ng; sin excesiva dicultad, empleando alguna de las tcnicas vistas en temas precedentes podemos representar de manera eciente una biyeccin entre un conjunto nito A de cardinalidad n y el conjunto f1; : : : ; ng y con ello una particin sobre el conjunto soporte A en caso necesario. Adicionalmente, supondremos que el conjunto soporte es esttico, es decir, ni se aaden ni se eliminan elementos. No obstante, con poca dicultad extra podemos obtener representaciones ecientes para particiones de conjuntos dinmicos.

Generalmente el tipo de operaciones que una clase Particion debe soportar son las siguientes: 1) dados dos elementos i y j , determinar si pertenecen al mismo bloque o no; 2) dados dos elementos i y j fusionar los bloques a los que pertenecen (si procede) en un slo bloque, devolviendo la particin resultante. Frecuentemente el primer tipo de operacin se realiza mediante una operacin FIND que dado un elemento i, devuelve un representante de la clase a la que pertenece i. Si dos elementos i y j tienen el mismo representante, entonces han de estar en el mismo bloque.

El segundo tipo de operacin se llama MERGE o UNION. De ah que las particiones se les llame a menudo estructuras union-nd o mfsets (abreviacin de merge-nd sets). La operacin MAKE crea una particin de f1; : : : ; ng consistente en n bloques cada uno de los cuales contiene un elemento.

En muchas aplicaciones antes de hacer cualquier UNION se habr determinado previamente si los elementos i y j cuyos bloques habran de unirse, estn o no en el mismo bloque; para ello se habr preguntado si P:FIND(i) = P:FIND(j ) o no. Por esta razn es habitual que en una clase Particion la operacin UNION slo acte sobre elementos que son representantes de sus respectivas clases.

procedure F (: : :) ...

ri P:FIND i rj P:FIND j if ri rj then P:UNION ri; rj

:= := 6 =

... end if end procedure

() () ( )

Puesto que la operacin MAKE recibe como parmetro el nmero de elementos n del conjunto soporte, podremos utilizar implementaciones en vector, reclamando un vector con el nmero apropiado de componentes a la memoria dinmica si nuestro lenguaje de programacin lo soporta. Tambin ser posible utilizar este tipo de implementacin si el valor de n est acotado y dicha cota no es irrazonablemente grande. En cualquier caso podremos modicar sin demasiado esfuerzo las implementaciones que se vern a continuacin para que sean completamente dinmicas y funcionen correctamente en aquellos casos en que no se puedan crear vectores cuyo tamao se ja en tiempo de ejecucin o si el conjunto soporte ha de soportar inserciones y borrados.

Quick-nd consiste representar la particin mediante un vector P y almacenar en la componente i de P el representante de la clase a la que pertenece i. De este modo la operacin FIND tiene coste constante, ya que basta examinar P [i]. Sin embargo, la operacin UNION tendr coste (n) ya que cada uno de los elementos de la clase en la que est j (los ks tales que P [k] = P [j ]) han de cambiar de representante (P [k] := P [i]). O bien los elementos del mismo bloque que i han de pasar al bloque de j .

Con algunas modicaciones puede evitarse el recorrido completo del vector P y restringirlo a los elementos del bloque de j (o del bloque de i); pero an as el coste de una UNION sigue siendo lineal en n en el caso peor, ya que cualquiera de los dos bloques pude contener una fraccin considerable del total de los elementos.

Aunque resulta un tanto forzado, conviene contemplar la representacin quick-nd como un bosque de rboles; cada rbol representa a un bloque de la particin en un momento dado. Los rboles estn representados mediante apuntadores al padre, siendo la raz de cada rbol el representante del bloque correspondiente. Puesto que la raz de un rbol no tiene padre, las races se apuntan a s mismas. Del invariante de la representacin de quick-nd se sigue que todos los rboles tienen altura 1 (si slo contienen un elemento) o altura 2 (todos los elementos de un bloque excepto el representante estn en el segundo nivel, apuntando a la raz).

Otra estrategia, quick-union, explota la correspondenicia entre bloques y rboles del siguiente modo: para unir los bloques de i y j se localizan al representante de i, digamos u, y se coloca a u como hijo de j . Alternativamente, podemos localizar ambos representantes y hacer que uno de ellos sea hijo del otro.

procedure MAKE(n) for i to n do


P i

end for end procedure procedure UNION(i; j )


u FIND i v FIND j P u v .

:= 1 [ ] := i

end procedure procedure FIND(i) while P i 6 i do


i

:= ( ) := ( ) [ ] :=

1in

end while return i end procedure

[]= := P [i]

Aunque en general los rboles resultantes de una secuencia de uniones sern relativamente equilibrados y el coste de las operaciones ser bajo, en caso peor podemos crear rboles poco equilibrados de modo que tanto una UNION como un FIND tengan coste proporcional al nmero de elementos involucrados. Por ejemplo, si realizamos una secuencia de UNION es de tal modo que la clase en la que est j slo contenga a j , obtendremos rboles equivalentes a listas.

Los comentarios previos sugieren posibles soluciones al problema. En la unin por peso el rbol con menos elementos es que el que se aade como hijo del que tiene ms elementos. En la unin por rango el rbol de menor altura es el que se pone como hijo del de mayor altura. Tanto una como otra estrategia son fciles de implementar, pero requieren, en principio, que se almacene informacin auxiliar sobre el tamao o la altura de los rboles.

Se puede evitar el uso de espacio auxiliar observando que slo se necesita esta informacin de tamao o altura para las races y que el espacio correspondiente a sus apuntadores es esencialmente intil, ya que slo se precisara un bit que indique que i es una raz o no. Por ejemplo, podemos adoptar el convenio de que si P [i] < 0 entonces i es una raz y P [i] es el tamao del rbol.

procedure UNION(i; j )
u v

end if . u es el rbol pequeo


P v P u .

:= FIND(i) := FIND(j ) if P [u] > P [v ] then . u es el rbol grande u :=: v [ ] := P [v] + P [u] [ ] := v [] 0 := P [i]

end procedure

procedure FIND(i) while P i > do


i

1in

end while return i end procedure

El rendimiento O(log n) de estas operaciones es consecuencia directa del siguiente lema. Dado un bloque de tamao k en una Particion con unin por peso, la altura del rbol correspondiente es  log2 k.

Lema

Demostracin: Si k = 0, el lema es obviamente cierto. Supongamos que es cierto para todos los tamaos hasta k y demostraremos entonces que es cierto para k + 1. Sea t el rbol correspondiente a un bloque de tamao k + 1. Dicho bloque es el resultado de la unin de dos bloques de tamaos r y s, r  s  k. El rbol t tiene altura h(t)  m axflog2 r + 1; log2 sg, aplicando la hiptesis de induccin, y por la denicin de unin por peso. Supongamos que log2 r + 1  log2 s. Entonces

h(t)  m axflog2 r + 1; log2 sg = log2 s < log2 (k + 1):

Demostracin (cont.): Por otro lado, si log2 r + 1 > log2 s, y teniendo en cuenta que k + 1 = r + s  2r,

h(t)  m axflog2 r + 1; log2 sg = log2 r + 1 = log2 (2r)  log2 (k + 1):

Todava puede conseguirse un mejor rendimiento empleando una heurstica de compresin de caminos. La idea es reducir la distancia a la raz de los elementos en el camino de i hasta la raz durante una operacin FIND(i). Mientras se asciende desde i hasta la raz se disminuye la altura del rbol. Subsiguientes FINDs que afecten a i o alguno de los elementos que eran antecesores suyos sern ms rpidos. La compresin de caminos acorta caminos de manera que poco a poco los rboles adoptan la forma que tendran con quick-nd, pero no teniendo que encargarse de ello la operacin UNION ni compactndose todo un rbol de una sola vez.

1: Compresin por mitades: se modica el apuntador de cada elemento en el camino desde i hasta FIND (i), excepto a FIND (i) y el hijo de ste, para que apunte a su abuelo.
procedure FIND(i)
::: j i k P j while P k > do P j P k j k k P j

end while return k end procedure

:= ; := [ ] [] 0 [ ] := [ ] := := [ ]

2: Compresin total : se modica el apuntador de cada elemento en el camino desde i hasta FIND (i), para que apunte a la raz.
procedure FIND(i)
::: j i

while P j >
j k

:=

end while

[ ] 0 do := P [j ] [ ] 0 do := [ ] [ ] := :=

while P k >

:= i

tmp P k P k j k tmp

end while return k end procedure

Se ha demostrado que una secuencia de m UNIONes y n FINDs usando una de estas dos tcnicas tiene coste O((m + n) (m; n)), donde (m; n) es la denominada funcin inversa de Ackermann. Puesto que (m; n)  4 para cualesquiera valores de m y n concebibles en la prctica, el coste puede considerarse O(m + n). Aunque el coste de una UNION o un FIND no es constante, el coste amortizado s lo es ya que el coste de las m + n operaciones es O(m + n) en caso peor.

Algoritmos Voraces Simples y Tcnicas Generales Caminos Mnimos: Algoritmo de Dijkstra rboles de Expansin Mnimos: Algoritmos de Kruskal y de Prim Particiones Cdigos de Huffman

10

11

12

Parte III Programacin Dinmica


13

Introduccin a la Programacin Dinmica Distancia de edicin Algoritmo de Floyd Construccin de BSTs ptimos

14

15

16

13

Introduccin a la Programacin Dinmica Distancia de edicin Algoritmo de Floyd Construccin de BSTs ptimos

14

15

16

Introduccin
La programacin dinmica (PD) es un esquema de resolucin de problemas de optimizacin que se fundamenta en dos ingredientes fundamentales:
1

El principio de optimalidad (Bellman & Dreyfus, 1962) Un problema satisface el principio de optimalidad si cualquier subsolucin (solucin parcial) de la solucin ptima de una instancia es solucin ptima de la correspondiente subinstancia. La memoizacin: las soluciones de los subproblemas se almacenan en una tabla para evitar hacer ms de una vez la misma llamada recursiva; de hecho, con la memoizacin se elimina la recursividad y se concluye con un algoritmo iterativo

Son muchos los problemas que admiten una solucin mediante PD. El esquema es conceptualmente recursivo, pero por lo general se trabaja con versiones iterativas, organizando cuidadosamente el orden de resolucin de los subproblemas y utilizando la memoizacin, es decir, almacenando resultados intermedios en memoria con el n de optimizar su rendimiento.

Algunos problemas que se resuelven mediante este esquema incluyen: Clculo de la distancia de edicin entre cadenas. Clculo de los caminos mnimos entre todos los pares de vrtices de un grafo (algoritmo de Floyd). Clculo del BST ponderado esttico ptimo. El problema de la mochila entera. El problema del viajante de comercio. Clculo de la derivacin incontextual ms verosmil. Clculo de los caminos mnimos desde un vrtice a los restantes, con pesos eventualmente negativos (algoritmo de Bellman-Ford). Clculo de la secuencia de mezclas ptima. Clculo de la clausura transitiva de un grafo (algoritmo de Warshall). Clculo de la cadena de multiplicacin de matrices ptima.

Pasos para desarrollar una solucin de programacin dinmica:


1

Establecer una recurrencia para el valor (coste, benecio) de una solucin ptima de la instancia dada en trminos de los valores a soluciones ptimas de subinstancia, aplicando el principio de optimalidad Disear la estructura de datos apropiada para almacenar soluciones intermedias (memoizacin) y determinar el orden en que dichas soluciones intermedias deben obtenerse (cmo rellenar la tabla).

Implementar el algoritmo iterativo, calculando su coste en tiempo y espacio Detectar posibles optimizaciones en tiempo y/o espacio Disear el algoritmo que reconstruye una solucin ptima, utilizando la tabla de memoizacin o alguna estructura de datos auxiliar que permite recordar cules son las subinstancias que proporcionan las soluciones parciales con las que se construye la solucin ptima.

4 5

13

Introduccin a la Programacin Dinmica Distancia de edicin Algoritmo de Floyd Construccin de BSTs ptimos

14

15

16

Distancia de edicin

Dados dos strings x = x1 xm e y = y1 yn , la distancia de edicin entre ambos, dist(x; y ), es el mnimo nmero de operaciones de edicin (insercin de un carcter, borrado de un carcter, sustitucin de un carcter por otro) necesarias para convertir x en y . Por ejemplo, dist(BARCO; ARCOS) = 2, pues hay que borrar la B inicial y agregar una S al nal para pasar de un string al otro. Otro ejemplo: dist(PALAS; ATRAS) = 3, ya que hay que borrar la P inicial, sustituir la L por T (o por R) e insertar una R (o una T).

Empezaremos estableciendo la recurrencia para la distancia de edicin. Sea i;j la distancia de edicin entre el prejo x1 xi de longitud i de x y el prejo y1 yj de longitud j de y , 0  i  m, 0  j  n. Entonces la distancia buscada es dist(x; y ) = m;n . La base de recursin es simple:

0;j = j; i;0 = i;

0  j  n; 0  i  m;

puesto que hay que insertar j caracteres para convertir la cadena vaca en y1 yj , y anlogamente, hay que borrar i caracteres para convertir x1 xi en la cadena vaca.

Para el caso general, la distancia de edicin ser uno de las tres siguientes posibilidades:

usar el mnimo de operaciones para convertir x1 xi1 en y1 yj y luego borrar xi , usar el mnimo de operaciones para convertir x1 xi en y1 yj 1 y luego insertar yj , usar el mnimo de operaciones para convertir x1 xi1 en y1 yj 1 y sustituir, si es necesario, xi por yj .

Poniendo todo junto, hay que tomar la opcin que minimiza la distancia:

i;j = m n(i1;j + 1; i;j 1 + 1; i1;j 1 + s(xi ; yj ));


donde s(xi ; yj ) = 0 si xi

= yj y s(xi ; yj ) = 1 si xi 6= yj .

Usaremos una matriz o tabla bidimensional D con m + 1 las por n + 1 columnas de manera que D[i; j ] = i;j . La base de la recursin nos permite rellenar la la 0 y la columna 0 de la tabla D. Por otro lado en la recurrencia obtenida en el paso anterior observamos que el valor i;j depende del valor en la columna inmediatamente anterior (i; j 1) y de dos de los valores de la la previa (i 1; j ) e (i 1; j 1). Esto signica que la matriz D debe rellenarse por las de arriba (i = 1) a abajo (i = m) y, para cada la, por columnas, de izquierda (j = 1) a derecha (j = n).

.m

procedure E DIT D IST(x, y ) jxj, n jyj, D: matriz for j to n do

= [0::m; 0::n] de enteros := 0 D [0; j ] := j end for for i := 1 to m do D [i; 0] := i end for for i := 1 to m do for j := 1 to n do . s(a; b) retorna 0 si a = b, y 1 si a 6= b
D i; j

[ ] := m nD[i 1; j ] + 1; D[i; j 1] + 1; D [i 1; j 1] + s(x[i]; y [j ]) ]

end for end for return D m; n end procedure

El coste del algoritmo viene naturalmente dominado por el coste del bucle principal, en el que se hacen m n iteraciones, cada una de las cuales tiene coste (1). El coste en espacio y en tiempo del algoritmo es (m n). Para jar ideas, si m = c n entonces el tamao de la entrada (= suma de las longitudes de los strings) es m + n = (n), y el coste del algoritmo en tiempo y espacio es (n2 ), es decir, cuadrtico respecto al tamao de la entrada.

Resulta evidente que no necesitamos tener una matriz D completa: para hallar la entrada (i; j ) nos basta tener las entradas previas de la la i y la la i 1. Podemos entonces usar un par de vectores D[0::n] y Dprev [0::n] de manera que mantengamos el siguiente invariante: en la iteracin que ha de calcular i;j se cumple que Dprev[k] = i1;k para toda k y D[k] = i;k para toda k < j. El coste del algoritmo en tiempo sigue siendo (m n), pero el coste en espacio lo hemos reducido a (n). Si n > m convendr intercambiar x e y al inicio: la distancia de edicin es la misma.

.m

procedure E DIT D IST(x, y ) jxj, n jyj, D; Dprev: vectores for j to n do

= Dprev [j ] := j end for for i := 1 to m do D [0] := i for j := 1 to n do := 0


Dj

[0::n] de enteros

[ ] := m nDprev[j ] + 1; D[j 1] + 1; Dprev [j 1] + s(x[i]; y [j ])




end for

.D i; , Dprev i1; D Dprev . Copia Dprev en D

end for return D n end procedure

:=

[]

13

Introduccin a la Programacin Dinmica Distancia de edicin Algoritmo de Floyd Construccin de BSTs ptimos

14

15

16

13

Introduccin a la Programacin Dinmica Distancia de edicin Algoritmo de Floyd Construccin de BSTs ptimos

14

15

16

Parte IV Flujos sobre Redes y Programacin Lineal


17

Flujos sobre Redes Programacin Lineal

18

17

Flujos sobre Redes Programacin Lineal

18

Flujos sobre Redes


Denicin
1 2

Una red s-t es un grafo dirigido G = hV; E i donde Cada arco e tiene una capacidad ce El vrtice s 2 V , denominado fuente (ing: source) es el nico vrtice de G con grado de entrada 0. El vrtice t 2 V , denominado sumidero (ing: sink) es el nico vrtice de G con grado de salida 0. nodo interno 20 fuente capacidad

> 0.

u
32

12

s
15

t
18

sumidero

Denicin
Un ujo f sobre una red G es una funcin f que:
1

: E (G) ! R tal

Para todo arco e 2 E (G),

0  f (e)  ce
2

(condiciones de capacidad)

Para todo vrtice v


X

2 V (G) n fs; tg,


f (e) = 0
(conservacin del ujo)

e=(v;w)

f (e)

e=(w;v)

Un ejemplo de ujo

u
18/20 12/12 6/32

s
12/15

t
18/18

v
Las etiquetas x=y sobre los arcos denotan el ujo acarreado por el arco (x) y su capacidad (y ); empleamos un trazo ms grueso para indicar que un arco lleva ujo, y muy grueso para indicar que est saturado.

Si denimos

f out (v) = f (v) =


in

e=(v;w)
X

f (e) f (e)

ujo saliente ujo entrante

e=(w;v)

f out (v) f in (v) = 0 para todo v 2 V (G) n fs; tg.

las condiciones de conservacin del ujo las podemos reescribir

Salvo que el ujo f sea nulo (f (e) = 0 para todo e), la fuente s emite ujo (f out (s) > 0 y f in (s) = 0) y el sumidero t lo absorbe (f in (t) > 0 y f out (t) = 0). Si para un arco e se cumple que f (e) = ce arco est saturado.

> 0 se dice que el

Dado un conjunto de vrtices A,

f out (A) = f (A) =


in

v 2A
X

f out (v) f in (v)

v 2A

El valor v (f ) de un ujo f es el ujo saliente de s, que coincide con el ujo entrante en t

v(f ) = f out (s) = f in (t)

El problema a resolver se puede entonces formular entonces en los siguientes trminos: Dada G, una red s-t, hallar un ujo f mximo, es decir, un ujo sobre G cuyo valor v (f ) es mximo. Obs: una red G puede admitir varios ujos mximos, todos de idntico valor. Un cota trivial al mximo ujo alcanzable:

v(f ) = f out (s) =

e=(s;v)

f (e)

e=(s;v)

ce

Un corte s-t de una red G es una particin hA; B i del conjunto de vrtices V (G) tal que:

Denicin

A [ B = V (G) A\B =; s2Ayt2B

Denicin

Dado hA; B i, un corte s-t de G, su capacidad es

c(A; B ) =

e=(u;v) u2A;v2B

ce

Lema

Sea hA; B i un corte s-t de una red G, y f un ujo cualquiera sobre la red. Entonces

f out (A) f in (A) = v(f )


Demostracin: Puesto que f out (s) = v (f ) y f in (s) = 0

f out (A) = f out (s) +


= v (f ) +

X X

v2Anfsg
X

f out (v)

f in (A) = f in (s) +
=
X

v2Anfsg

f out (v) f in (v)

v2Anfsg

f in (v)

v2Anfsg

Demostracin (cont.): Luego

f out (A) f in (A) = v(f ) +


= v (f ) + = v (f )

(f (v ) f in (v )) v2Anfsg
out

v2Anfsg
X

f out (v)

v2Anfsg

f in (v)

puesto que f out (v ) f in (v ) = 0 para todo v

62 fs; tg.

Se puede demostrar de manera parecida que para cualquier corte hA; B i y cualquier ujo f

f in (B ) f out (B ) = f out (A) f in (A) = v(f )

Teorema

Dados un corte s-t cualquiera hA; B i y un ujo f cualquiera

v(f )  c(A; B )
El valor de un ujo no puede exceder la capacidad de ningn corte de la red La cota trivial v (f )  e=(s;v) ce es un caso particular del teorema, tomando A = fsg y B = V (G) n fsg.
P

Demostracin: Comenzaremos demostrando un resultado intermedio importante:

f out (A) f in (A) =

e sale de A

f (e)

e entra en A

f (e)

Por el lema demostrado anteriormente

v(f ) = f out (A) f in (A)


Tenemos

f out (A) = f in (A) = f out (A) f in (A) =

v 2A
X

f out (v) = f in (v) =


0 @ X

X X

v2A e=(v;w)

f (e)

v 2A
X

v2A e=(w;v)

f (e)
1

v2A e=(v;w)

f (e)

e=(w;v)

f (e)A

Demostracin (cont.): Consideraremos tres casos:


1

Arcos que salen de v hacia un vrtice fuera de A, es decir, de la forma (v; w) con w 2 B : contribuyen el trmino
X

e=(v;w) w 2B
2

f (e)

Arcos que llegan a v desde un vrtice fuera de A (entran en A), es decir, de la forma (w; v ) siendo w 2 B : contribuyen el trmino

e=(w;v) w 2B

f (e)

Demostracin (cont.):
3

Arcos internos a A, de la forma (x; y ) con x; y 2 A: cuando sumamos sobre todos los vrtices v de A contribuyen 0
0 XB X B @ 1

v2A e=(v;w) w 2A

f (e)

e=(w;v) w 2A

f (e)C A f (e)
X

e=(v;w) v2A;w2A

e=(w;v) v2A;w2A

f (e) = 0

Demostracin (cont.): Recapitulando

v(f ) = f out (A) f in (A) =


=

v2A e=(v;w) w 2B
X

f (e)

e sale de A
X X

f (e)

v2A e=(w;v) w 2B
X

f (e)

e entra en A
X

f (e)

Como todos los ujos son positivos o cero,

v(f ) = f out (A) f in (A) =

e sale de A

f (e) 

e sale de A

f (e)

e sale de A

ce = c(A; B )

e entra en A

f (e)

El algoritmo de Ford-Fulkerson y el teorema maxow-mincut


El algoritmo de Ford-Fulkerson (1954) calcula un ujo mximo a travs de una serie de etapas sucesivas. Se parte de un ujo nulo y cada etapa enva ujo adicional de s a t, incrementando el valor del ujo en curso. Para ello se busca, en cada etapa, un camino de s a t en el que se pueda incrementar el ujo que atraviesa los arcos del camino. El valor del ujo se va acercando progresivamente al valor mximo. El algoritmo naliza cuando no podemos encontrar un camino que nos lleve de s a t en el que podamos incrementar el ujo. De ahora en adelante asumiremos que todas las capacidades ce de la red son enteros positivos, y nuestro anlisis de la correccin del algoritmo y de su tiempo de ejecucin se basarn en esta hiptesis.

El algoritmo de Ford-Fulkerson va calculando ujos sucesivos f0 , f1 , f2 , . . . de manera que v(fi ) < v(fi+1 ), con f0 el ujo nulo; entre dos ujos sucesivos fi y fi+1 slo son diferentes los ujos enviados a travs de los arcos de un cierto camino, todos los restantes arcos llevan el mismo ujo en fi y en fi+1 . A medida que evoluciona el algoritmo podemos pensar en sucesivas redes G0 = G, G1 , . . . donde el ujo asignado a los arcos e de Gi les deja una capacidad remanente o residual c0e = ce fi (e). Este procedimiento nos puede llevar a un callejn sin salida si elegimos mal el camino que nos lleva de s a t, pues podemos saturar arcos vitales para llegar de s a t bloqueando otros posibles ujos de mayor valor. Por ello debemos considerar la posibilidad de retornar el ujo o parte de l que hayamos enviado a travs de un arco.

u
0/20 0/12 0/32 0/15 0/18

u
12/20 12/12 0/32 0/15 0/18

u
12/20 12/12 0/32

s
15/15

t
15/18

u
15/20 12/12 3/32

s
15/15

t
18/18

Denicin

Dada una red G = hV; E i y un ujo f sobre G, la red residual (o grafo residual) Gf = hVf ; Ef i se dene como sigue:
1 2

Vf = V Si e = (u; v ) 2 E y f (e) < ce entonces e = (u; v ) 2 Ef y ce = ce f (e) (arcos de avance; ing: forward edges) Si e = (u; v ) 2 E y f (e) > 0 entonces e0 = (v; u) 2 Ef y ce0 = f (e) (arcos de retroceso; ing: backward edges)

Observacin: en el grafo residual todas las capacidades residuales c son nmeros enteros positivos si los ujos lo son

u
12/20 12/12 0/32

8 12

u
12 32

s
15/15

s
15

t
3 15

15/18

Red con ujo

Red residual

Los arcos de avance de la red residual se muestran con lnea slida y etiquetados por la capacidad residual (ce = ce f (e)); los arcos de retroceso se muestran con lnea discontinua y etiquetados por su capacidad residual (c(v;u) = f ((u; v )))

Caminos de aumentacin Dada una red G y un ujo f , un camino de aumentacin (ing: augmenting path) es un camino simple entre s y t en el grafo residual Gf .
procedure B OTTLENECK(P ) return la capacidad residual mnima de un arco de P end procedure procedure P USH F LOW(P , f ) b B OTTLENECK P for e u; v 2 P do if e es un arco de avance en Gf then

:=

end if end for return f end procedure

( ) =( ) f (e) := f (e) + b else . e es un arco de retroceso e0 := (v; u) f (e0 ) := f (e0 ) b

Observacin: Si para todos los arcos e el ujo f (e) es un nmeros entero positivo, lo mismo ocurre con f 0 = P USH F LOW(P; f ), es decir, f 0 (e) es un nmero entero positivo tambin. Sea f 0 = P USH F LOW(P; f ). Entonces f 0 es un ujo vlido sobre la red G y v (f 0 ) = v (f ) + B OTTLENECK(P ) > v (f )

Proposicin

Demostracin: Sea b = B OTTLENECK(P ).


1

Condiciones de capacidad: Slo se modica el ujo en los arcos de P . Supongamos que e es un arco de avance. Entonces f 0 (e) = f (e) + b. Pero ce = ce f (e)  b pues b es la capacidad residual mnima, luego ce  b + f (e) = f 0 (e). Como f (e)  0 y b > 0 tenemos que f 0 (e) > 0. Supongamos ahora que e = (u; v ) es un arco de retroceso. Entonces f 0 ((v; u)) = f ((v; u)) b. La capacidad residual de e es ce = f ((v; u)) y por tanto c(u;v)  f ((v; u)) > f 0 ((v; u)) = f ((v; u)) b  0, ya que b  ce .

Demostracin (cont.):
2

Conservacin del ujo: Consideremos cualquier vrtice que no est en el camino P . Como el ujo f 0 es igual al ujo f en los arcos que entran y que salen de v , la conservacin del ujo se cumple para dichos vrtices. Para los vrtices v 62 fs; tg que forman parte del camino P tenemos que considerar cuatro posibles casos (FF,FR,RF,RR), segn que el camino P llegue al vrtice por un arco de avance o uno de retroceso, y que el camino salga de v por un arco de avance o uno de retroceso.

Demostracin (cont.): Por ejemplo, supongamos el caso FR. Los arcos e1 = (u; v ) de avance y e2 = (v; w) de retroceso forman parte del camino P . Ningn arco de salida de v en el grafo original cambia, luego f out (v) = f 0 out (v). Pero los arcos de entrada (u; v) y (w; v) s cambian su ujo. No obstante como f 0 ((u; v )) = f ((u; v )) + b y f 0 ((w; v)) = f ((w; v)) b, f in (v) = f 0 in (v), de lo cual deducimos que el ujo se conserva en v . Los otros tres casos (FF,RF,RR) se demuestran razonando de manera anloga. Demostrado que f 0 es vlido y puesto que P necesariamente debe comenzar con un arco de avance que sale de s (Gf no puede tener arcos de retroceso que salgan de s!),

v(f 0 ) = f 0 out (s) = f out (s) + b = v(f ) + b

El algoritmo de Ford-Fulkerson (FF)


Require: G una red s-t Ensure: f

= FORD-F ULKERSON(G) es un ujo mximo sobre G

procedure F ORD -F ULKERSON(G) for e 2 E G do


f e

end while return f end procedure

( ) ( ) := 0 end for Gres := G while existen caminos de aumentacin entre s y t en Gres do P := un camino de aumentacin f := P USH F LOW(P; f ) ACTUALIZAR R ESIDUAL(Gres; P; f )

u
0/20 0/12 0/32 0/15 0/18 20

u
12 32 15 18

v
Red

v
Red residual

u
18/20 0/12 18/32 0/15 2 18

u
12 18 15 14 18

t
18/18

v
Red

v
Red residual

u
18/20 0/12 18/32 0/15 2 18

u
12 18 15 14 18

t
18/18

v
Red

v
Red residual

u
18/20 12/12 6/32 2 18

u
12 6 12 3 26 18

s
12/15

t
18/18

v
Red

v
Red residual

Terminacin y tiempo de ejecucin de FF


En cada iteracin del bucle principal del algoritmo el valor del ujo f aumenta al menos en una unidad. Por tanto el nmero de iteraciones del algoritmo est acotado superiormente por

C=

e=(s;v)

ce ;

y la terminacin del algoritmo est garantizada. Cada iteracin puede implementarse con coste O(m + n) ya que el grafo residual nunca tiene ms de 2m arcos (m = jE (G)j) y un camino de aumentacin contiene a lo sumo n = jV (G)j vrtices. El coste total del algoritmo es O((m + n) C ). En caso peor, este coste puede llegar a ser muy elevado; es posible construir redes en las que C sea muy grande, y en cada iteracin slo aumente una unidad el valor del ujo f .

Correccin

El algoritmo de Ford-Fulkerson se termina cuando el grafo residual Gf no contiene un camino de s a t. Porqu eso nos garantiza que el ujo f es entonces mximo? Sea fF F el ujo hallado por el algoritmo. En el correspondiente grafo residual Gf no hay ningn camino de s a t. Denamos dos conjuntos de vrtices:
FF

AF F = fv 2 Gf j v es accesible desde sg BF F = fv 2 Gf j v no es accesible desde sg


FF FF

El par hAF F ; BF F i es un corte s-t del grafo G: todo vrtice de G pertenece a AF F o a BF F , s pertenece a AF F (porque siempre podemos acceder a s desde s!) y t pertenece a BF F , puesto que Gf no podemos acceder a t desde s. Ya hemos visto antes que
FF

v(fF F ) = f out (AF F ) f in (AF F ) X X = f (e)


e sale de A
FF

e entra A

f (e)
FF

Sea e un arco que sale de AF F (sale de un cierto v 2 AF F y llega a un vrtice w 2 BF F ). No puede existir un arco e = (v; w) en Gf , porque sino tendramos la posibilidad de acceder a w desde s, lo cual es una contradiccin. La nica forma de que (v; w) no sea un arco en Gf es que el ujo sature e, dejando una capacidad residual nula (y por tanto e no aparece en el residual). As que para todo arco e que sale de AF F , f (e) = ce .
FF FF

Sea e = (w; v ) un arco que entra en AF F (sale de un cierto w 2 BF F y llega a un cierto v 2 AF F ). Razonando como antes, no puede existir un arco de retroceso e0 = (v; w) en el grafo residual Gf , porque sino w sera accessible desde s y por denicin w no es accessible. Para que no exista el arco de retroceso e0 = (v; w) en Gf tiene que ocurrir que el arco e = (w; v) del grafo original no lleve ujo, porque si f (e) > 0 entonces e0 = (v; w) estara en el residual y su capacidad ce0 = f (e). Para todo arco e que entra en AF F se tiene que cumplir que f (e) = 0.
FF FF

Por lo tanto

v(fF F ) =
= =

e sale de A
X

f (e)
FF

e sale de A
X

ce 0

e entra A

f (e)
FF

FF

e sale de A

ce = c(AF F ; BF F )
FF

Por un teorema que hemos visto anteriormente ningn ujo puede tener un valor que exceda la capacidad de ningn corte s-t. Como v(fF F ) coincide con c(AF F ; BF F ) la nica conclusin posible es que v (fF F ) es mximo y que c(AF F ; BF F ) es mnima. Acabamos de demostrar un importante teorema.

Teorema (Maxow-mincut)
Para toda red s-t con capacidades enteras, existe un ujo mximo f cuyo valor coincide con la capacidad de un corte hA; B i de capacidad mnima.

v(f ) = c(A ; B )
El algoritmo de Ford-Fulkerson nos retorna un ujo fF F de valor mximo; el corte hAF F ; BF F i tiene capacidad mnima.

17

Flujos sobre Redes Programacin Lineal

18

Parte V Estructuras de Datos Avanzadas


19

Tries Skip Lists Estructuras de Datos Multidimensionales Estructuras de Datos Mtricas

20

21

22

19

Tries Skip Lists Estructuras de Datos Multidimensionales Estructuras de Datos Mtricas

20

21

22

Tries

Las claves que identican a los elementos de una coleccin estn formadas por una secuencia de smbolos (p.e. caracteres, dgitos, bits), siendo est descomposicin ms o menos natural, y dicha circunstancia puede aprovecharse ventajosamente para implementar las operaciones tpicas de un diccionario de manera notablemente eciente. Adicionalmente, es frecuente que necesitemos ofertar operaciones del TAD basadas en esta descomposicin de las claves en smbolos: por ejemplo, podemos querer una operacin que dada una coleccin de palabras C y una palabra p nos devuelva la lista de todas las palabras de C que contienen a la palabra p como subcadena.

Consideremos un alfabeto nito = f1 ; : : : ; m g de cardinalidad m  2. Mediante denotaremos, como es habitual en muchos contextos, el conjunto de las secuencias o cadenas formadas por smbolos de . Dadas dos secuencias u y v denotaremos u v la secuencia resultante de concatenar u y v . Para la secuencia de longitud 0, es decir, la secuencia vaca usaremos la notacin .

Dado un conjunto nito de secuencias X  de idntica longitud, el trie T correspondiente a X es un rbol m-ario denido recursivamente de la siguiente manera:
1

Denicin

Si jX j  2, sea Ti el trie correspondiente a Xi = fy j x = i y 2 X ^ i 2 g. Entonces T es un rbol m-ario constituido por una raz  y los m subrboles T1 ; T2 ; : : : ; Tm .

Si X contiene un slo elemento o ninguno entonces T es un rbol consistente en un nico nodo que contiene al nico elemento de X o est vaco.

Si las aristas del trie T correspondiente a un conjunto X se etiquetan mediante los smbolos de de tal modo que la arista que une la raz con su primer subrbol se etiqueta 1 , la que une la raz con su subrbol se etiqueta 2 , etc. entonces las etiquetas del camino que nos llevan desde la raz hasta una hoja no vaca que contiene a x constituyen el prejo ms corto que distingue unvocamente a x; es decir, ningn otro elemento de X empieza con el mismo prejo.

Lema

Lema

Sea p la etiqueta correspondiente a un camino que va de la raz de un trie T hasta un cierto nodo (interno u hoja) de T . Entonces el subrbol enraizado en dicho nodo contiene todos los elementos de X que tienen en comn al prejo p (y no ms elementos).

Lema

Dado un conjunto X  de secuencias de igual longitud, su trie correspondiente es nico. En particular T no depende del orden en que se presenten los elementos de X .

La altura de un trie T es igual a la longitud mnima de prejo necesaria para distinguir cualesquiera dos elementos del conjunto al que corresponde el trie. En particular, si ` es la longitud de las secuencias en X , la altura de T ser  `.

Lema

La denicin de tries impone que todas las secuencias sean de igual longitud, lo cual es muy restrictivo. Pero si no exigimos esta condicin entonces tenemos un problema que habremos de afrontar: si un elemento x es prejo propio de otro elemento y, cmo podremos distinguirlo? Cmo diferenciamos las situaciones en la que x e y pertenecen ambos a X de las situaciones en la que slo y est en X ? Una solucin habitual consiste en ampliar con un smbolo especial (p.e. ]) de n de secuencia, y marcar cada una de las secuencias en X con dicho smbolo. Ello garantiza que ninguna de las secuencias (marcadas) es prejo propio de otra. El precio a pagar es que hay que trabajar con un alfabeto de m + 1 smbolos.

X = {ahora, alto, amigo, amo, asi, bar, barco, bota, ...}


a b c

...
a

...
a

h l m

... ...
bota
a r

ahora alto

asi

...

...

... ...
c

amigo

amo

...

barco

bar

Las tcnicas de implementacin de los tries son las convencionales para rboles. Si se utiliza un vector de apuntadores por nodo, los smbolos de suelen poderse utilizar directamente como ndices (eventualmente, se habr de utilizar una funcin ind : ! f1; : : : ; mg). Las hojas que contienen los elementos de X pueden almacenar exclusivamente los sujos restantes, ya que el prejo est mplicitamente codicado en el camino desde la raz a la hoja. En el caso en que se utilice la representacin primognito-siguiente hermano, cada nodo almacena un smbolo y sendos apuntadores al primognito y al siguiente hermano. Puesto que suele haber denido un orden sobre la lista de hijos de cada nodo suele ordenarse de acuerdo a quel.

Aunque es ms costoso en espacio emplear nodos del trie para representar las palabras completas, resulta ventajoso evitar la necesidad de nodos de distinto tipo, apuntadores a nodos de diferentes tipos, o representaciones poco ecientes para los nodos (p.e. unions).

// La clase Clave debe soportar las siguientes // operaciones: // x.length() devuelve la longitud >= 0 de una clave x template <typename Clave> int length() throw(); // x[i] devuelve el i-simo smbolo de x, // lanza un error si i < 0 o i >= x.length() template <typename Simbolo, typename Clave> Simbolo operator[](const Clave& x, int i) throw(error); template <typename Simbolo, typename Clave,typename Valor> class DiccDigital { public: ... private: struct nodo_trie { Simbolo _c; nodo_trie* _primg; nodo_trie* _sigher; Valor _v; }; nodo_trie* raiz; ... };

<typename Simbolo, typename Clave, typename Valor> void DiccDigital<Simbolo,Clave,Valor>::busca( const Clave& k, bool& esta, Valor& v) const throw(error) { template nodo_trie* p = busca_en_trie(raiz, k, 0); if (p == NULL) esta = false; else { esta = true; v = p -> _v; } } // Pre: p es la raz del subrbol conteniendo // las claves cuyos i-1 primeros smbolos // coinciden con los i-1 smbolos iniciales de k // Coste: {\color{red} $\Theta(\texttt{longitud}(k)$} template <typename Simbolo, typename Clave, typename Valor> DiccDigital<Simbolo,Clave,Valor>::nodo_trie* DiccDigital<Simbolo,Clave,Valor>::busca_en_trie( nodo_trie* p, const Clave& k, int i) const throw() { (p == NULL) return NULL; (i == k.length()) return p; (p -> _c > k[i]) return NULL; (p -> _c < k[i]) return busca_en_trie(p -> _sigher, k, i); // p -> _c == k[i] return busca_en_trie(p -> _primg, k, i+1); if if if if }

Una solucin que intenta combinar eciencia en el acceso a los subrboles y en espacio consiste en implementar cada nodo del trie como un BST. La estructura resultante se denomina rbol ternario de bsqueda ya que cada uno de sus nodos contiene tres apuntadores: dos apuntadores al hijo izquierdo y derecho, respectivamente, en el BST, y un apuntador a la riz del subrbol al que da acceso el nodo.

template <typename Simbolo, typename Clave, typename Valor> class DiccDigital { public: ... void inserta(const Clave& k, const Valor& v) throw(error); ... private: struct nodo_tst { Simbolo _c; nodo_tst* _izq nodo_tst* _cen; nodo_tst* _der; Valor _v; }; nodo_tst* raiz; ... static nodo_tst* inserta_en_tst(nodo_tst* t, int i, const Clave& k, const Valor& v) throw(error); ... };

template <typename Simbolo, typename Clave, typename Valor> void DiccDigital<Simbolo,Clave,Valor>::inserta( const Clave& k, const Valor& v) throw(error) { // Simbolo() es un simbolo nulo, // p.e. si Simbolo==char entonces // Simbolo() == \0 k[k.length()] = Simbolo(); // aadir centinela // al final de la clave raiz = inserta_en_tst(raiz, 0, k, v); }

template <typename Simbolo, typename Clave, typename Valor> DiccDigital<Simbolo,Clave,Valor>::nodo_tst* DiccDigital<Simbolo,Clave,Valor>::inserta_en_tst( nodo_tst* t, int i, const Clave& k, const Valor& v) throw(error) { if (t == NULL) { t = new nodo_tst; t -> _izq = t -> _der = t -> cen = NULL; t -> _c = k[i]; if (i < k.length() - 1) { t -> _cen = inserta_en_tst(t -> _cen, } else { // i == k.length() - 1; k[i] == t -> _v = v; } } else { if (t -> _c == k[i]) t -> _cen = inserta_en_tst(t -> _cen, if (k[i] < t -> _c) t -> _izq = inserta_en_tst(t -> _izq, if (t -> _c < k[i]) t -> _der = inserta_en_tst(t -> _der, } return t; }

i + 1, k, v); Simbolo()

i + 1, k, v); i, k, v); i, k, v);

La descomposicin digital de las claves puede emplearse adems de para la bsqueda para la ordenacin. Los algoritmos de ordenacin basados en estos principios se denominan de ordenacin digital (ing: radix sort). Consideremos un vector de n elementos cada uno de los cuales es una secuencia de ` bits. Dado un elemento x, bit(x; i) denotar su i-simo bit. Si ordenamos el vector con relacin al bit de mayor peso, cada bloque resultante de acuerdo al bit de siguiente peso, y as sucesivamente, habremos ordenado el vector en su totalidad.

// Llamada inicial: // radix_sort(A, 0, A.size()-1, {\color{red} $\ell-1$}); template <typename Elem, typename Symb> void radix_sort(vector<Elem>& A, int i, int j, int r) { if (i < j and r >= 0) { int k; radix_split(A, i, j, r, k); radix_sort(A, i, k, r - 1); radix_sort(A, k + 1, j, r - 1); } }

// bit(x, r) devuelve el bit r-simo de x // r == 0 => bit de menor peso // r == {\color{red}$\ell-1$} => bit de mayor peso template <typename Elem, typename Symb> void radix_split(vector<Elem>& A, int i, int j, int r, int& k) { int u = i; int v = j; while (u < v + 1) { while (u < v + 1 and bit(A[u],r) == 0) ++u; while (u < v + 1 and bit(A[v],r) == 1) --v; if (u < v + 1) swap(A[u], A[v]); } k = v; }

Cada etapa de radix sort tiene un coste no recursivo lineal; puesto que el nmero de etapas es ` el coste del algoritmo es (n `). Otra forma de deducir el coste consiste en considerar el coste asociado a cada elemento de A: un elemento cualquiera de A es examinado (y eventualmente intercambiado con otro) a lo sumo ` veces, de ah que el coste total sea (n `).

19

Tries Skip Lists Estructuras de Datos Multidimensionales Estructuras de Datos Mtricas

20

21

22

Skip lists

Una skip list es una estructura de datos muy sencilla que permite la implementacin de un TAD diccionario con poco esfuerzo de manera eciente. Las skip lists consiguen tener rendimiento (log n) en promedio en bsquedas y actualizaciones gracias al uso de aleatorizacin. Los costes promedio no dependen de que la entrada sea aleatoria o no: slo de las decisiones aleatorias tomadas por los algoritmos. Las skip lists fueron inventadas por W. Pugh en 1989.

Una skip list S que representa a un conjunto de elementos X consiste en un cierto nmero de listas enlazadas no vacas, ordenadas por las claves de los elementos que contienen y numeradas de 1 en adelante, de modo que se satisface
1 2

Si x pertenece a la lista i entonces, con probabilidad q , x pertenece tambin a la lista i + 1.

Todos los elementos de X pertenecen a la lista 1.

Dado un elemento x, su nivel es el nmero de listas en las que est includo. De la denicin anterior se desprende que el nivel de cada elemento es una variable aleatoria independiente y P[nivel(x) = i] = p q

i 1 ;

p = 1 q:

Para implementar una skip list, cada elemento se almacenar en un nodo, que asimismo contendr tantos apuntadores como correspondan al nivel del elemento. Cada uno de dichos apuntadores apunta al sucesor de x en la correspondiente lista. Adicionalmente, usaremos un nodo cticio de cabecera con apuntadores a los primeros elementos de cada lista. El nivel de la skip list es el mximo nivel de entre sus elementos y ste ser el nmero de apuntadores en el header.

template <typename Clave, typename Valor> class Diccionario { public: ... private: struct nodo_skip_list { Clave _k; Valor _v; int _alt; nodo_skip_list** _sig; nodo_skip_list(Clave k, Valor v, int alt) : _k(k), _v(v), _alt(alt), _sig(new nodo_skip_list*[alt]) { } }; nodo_skip_list* _header; int _nivel; double _p; // p.e., _p = 0.5 ... };

template <typename Clave, typename Valor> void Diccionario<Clave,Valor>::busca(const Clave& k, bool& esta, Valor& v) const throw(error) { nodo_skip_list* p = buscar_en_skip_list(_header, _nivel-1, k); if (p == NULL) esta = false; else { esta = true; v = p -> _v; } } template <typename Clave, typename Valor> Diccionario<Clave,Valor>::nodo_skip_list* Diccionario<Clave,Valor>::buscar_en_skip_list( nodo_skip_list* p, int l, const Clave& k) const throw() { while (l >= 0) if (p -> _sig[l] == NULL or k <= p ->_sig[l] -> _k) --l; else p = p -> _sig[l]; if (p -> _sig[0] == NULL or p -> _sig[0] -> _k != k) // k no est return NULL; else // k est, modificamos el valor asociado return p -> _sig[0]; }

Para realizar la insercin de un nuevo elemento se procede en cuatro fases: 1: Se busca en la skip list la clave k dada. Se emplea un bucle de bsqueda ligeramente distinto, para que anote en un vector de apuntadores los ltimos nodos examinados en cada nivel. Dichos nodos son los potenciales predecesores del nuevo nodo. Basta anotar cal es el ltimo nodo visitado en el nivel ` antes de bajar de nivel. 2: Si la clave k ya existe se modica el valor asociado y se termina.

template <typename Clave, typename Valor> void Diccionario<Clave,Valor>::inserta_en_skip_list(...) { nodo_skip_list** pred = new nodo_skip_list*[l + 1]; while (l >= 0) if (p -> _sig[l] == NULL or k <= p ->_sig[l] -> _k) { pred[l] = p; // <====== anotar el predecesor --l; } else { p = p -> _sig[l]; } if (p -> _sig[0] == NULL or p -> _sig[0] -> _k != k) // k no est; aadimos un nuevo nodo ... else // k est, modificamos el valor asociado p -> _sig[0] -> _v = v; }

3: En caso contrario, se crea un nuevo nodo con la clave y valor dados, utilizando procedimiento aleatorio para decidir el nivel r del elemento

4: Se enlaza el nuevo nodo en las r primeras listas

template <typename Clave, typename Valor> class Diccionario { public: ... private: ... util::Random _rng; // generador de nmeros aleatorios // asociado a la skip list }; template <typename Clave, typename Valor> void Diccionario<Clave,Valor>::inserta_en_skip_list(...) { ... // aadir nuevo nodo // generar aleatoriamente su altura int alt = 1; while (_rng() > _p) ++alt; nodo_skip_list* nn = new nodo_skip_list(k, v, alt); if (alt > _nivel) { // aadir nuevos niveles al header } // enlazar el nuevo nodo en las listas // enlazadas pertinentes for (int i = alt - 1; i >= 0; --i) { nn -> _sig[i] = pred[i] -> _sig[i]; pred[i] -> _sig[i] = nn; } }

... if (alt > _nivel) { // aadir nuevos niveles al _header y a pred // (desde i = _nivel hasta i = alt - 1) // nuevo header y nueva tabla pred nodo_skip_list** _new_header = new nodo_skip_list*[alt]; nodo_skip_list** new_pred = new nodo_skip_list*[alt]; // copiamos for (int i = _nivel - 1; i >= 0; --i) { _new_header -> _sig[i] = _header -> _sig[i]; new_pred -> _sig[i] = pred -> _sig[i]; } // los niveles desde _nivel a alt - 1 estn vacos for (int i = alt - 1; i >= _nivel; --i) { _new_header -> _sig[i] = NULL; new_pred -> _sig[i] = NULL; } // eliminamos el header y la tabla pred antiguas delete[] _header; delete[] pred; // actualizamos _header = _new_header; pred = new_pred; _nivel = alt; } ...

19

Tries Skip Lists Estructuras de Datos Multidimensionales Estructuras de Datos Mtricas

20

21

22

19

Tries Skip Lists Estructuras de Datos Multidimensionales Estructuras de Datos Mtricas

20

21

22

Potrebbero piacerti anche