Está en la página 1de 10

Complejidad: Álgebra de órdenes e intuición.

Apunte complementario.

Algoritmos y Estructuras de Datos 2


Primer cuatrimestre - 2005

1. Introducción
A la hora de escribir un algoritmo no sólo nos interesa garantizar que éste realiza lo esperado (correctitud)
sino que también resulta importante poder estimar la complejidad del mismo. El análisis de complejidad se
ocupa de analizar y estudiar dos aspectos fundamentales de los algoritmos: el tiempo que demora en realizar lo
que pretende y cuánta memoria necesita a tal efecto. En general nos ocuparemos principalmente de la cuestión
temporal relegando la espacial, no obstante las técnicas que veremos pueden ser aplicadas en ambos tipos de
análisis. De la misma manera, los análisis que en general realizaremos serán de carácter asintótico. La realización
de un análisis con esta caracterı́stica tiene implı́cita dos preferencias: no interesa el detalle preciso de la cantidad
de operaciones que un algoritmo realiza, ası́ como tampoco cómo se comporta para algunas entradas particulares
de cierto tamaño. En el primer caso, la experiencia indica que como primer aproximación resulta prescindible el
detalle fino de la cantidad de operaciones; además su cálculo es considerablemente más dificultoso y la expresión
resultante puede llegar a ser complicada. Con la notación ası́ntotica se obtiene una caracterización precisa y
concisa de un algoritmo, que se basa en el estudio de los rasgos fundamentales de un algoritmo sin entrar en
detalles que pueden hacer perder el foco de esos puntos principales.
Este tipo de estudio sobre un algoritmo resulta fundamental para poder comprenderlo cabalmente; si a un
algoritmo no le podemos calcular su complejidad difı́cilmente podamos entender qué es lo que realiza y cómo
lo hace. También resulta fundamental porque nos da herramientas con las cuales poder comparar distintos
algoritmos que realizan una misma tarea y de acuerdo a algún criterio elegir entre ellos.
Como primer paso para ello hemos de precisar cuales son los parámetros en función de los cuales nos interesa
expresar dicha complejidad: el largo de una lista, la cantidad de posiciones de un arreglo, etc.. Si bien para
cada algoritmo se pueden establecer párametros mejores que otros, en principio cualquier elección es buena en
la medida en que establezcamos precisamente la correspondencia existente entre los parámetros establecidos y
la entrada del algoritmo en particular.
También es importante que la expresión que demos para la complejidad de un algoritmo refleje todos los
parámetros que pueden hacer que el tiempo de ejecución se vea alterado por cambios en la entrada. Por otro
lado, preferimos que nuestras expresiones de complejidad sólo estén escritas en función de estos parámetros y
que no incluyan valores que no dependan de la entrada. Más aún, también preferimos dejar en la expresión sólo
los términos más significativos que en ella aparecen; haciendo uso de las propiedades conocidas de O() que luego
repasaremos.
Por último, debemos tener claro bajo que circunstancias el orden establecido es válido y, luego, indicarlo. A
lo largo de la materia los órdenes a calcular serán mayoritariamente del peor caso. Ante una omisión se suele dar
por sentado de que se habla del peor caso; ası́ lo haremos en el presente apunte. El análisis del caso promedio suele
ser más dificultoso, de hecho surge una dificultad inmediata respecto a cuál es el caso promedio. Eso depende del
contexto en el cual se vaya a ejecutar el algoritmo en cuestión. A los efectos de realizar un análisis general lo más
razonable posible, lo que suele hacerse es considerar que todos los posibles inputs tienen igual probabilidad de
ser recibidos como entrada. Esto implica tomar una distribución uniforme para la entrada, a pesar de lo sencilla
que es esta distribución el análisis que suele ser necesario realizar resulta dificultoso.

1
2. Primeros cálculos
2.1. La matriz
Comencemos recordando una propiedad que hemos visto en Álgebra 1; para ello escribamos un procediento
bastante sencillo que dada una matriz de n×n nos dice cuántos elementos por debajo de la diagonal son distintos
de cero:
cantCerosPorEncimaDeLaDiagonal(M )
1 cantidadDeCeros ← 0
2 for f il ← 1 to n
3 do for col ← 1 to f il − 1
4 do if M(f il,col) = 0
5 then cantidadDeCeros ← cantidadDeCeros + 1
6 return cantidadDeCeros

Expresemos la complejidad en función de la cantidad de filas (o columnas, ya que es cuadrada) de la matriz.


De todos los parámetros que afectan al tiempo de ejecución de este procedimiento él único que depende de la
entrada es la cantidad de iteraciones que se realizarán. El costo de una comparación, una suma, una asignación
u obtener el valor de una posición de la matriz, no dependen de la entrada de nuestro algoritmo particular, los
consideraremos de tiempo constante en nuestro análisis1 . Entonces lo que nos interesa es contar la cantidad de
iteraciones que se realizan:
n
X
f il − 1 (1)
f il=1

Al recorrer la fila f il se visitan las primeras f il − 1 columnas; luego en la primer fila no se visitan columas,
en la segunda se visita una columna y ası́ sucesivamente.
Entonces, si recordamos de las clases de Álgebra que:
X 1
(a + bj) = a(m + 1) + bm(m + 1) (2)
2
0≤j≤m

Vemos que en nuestro caso a = −1, b = 1 y ademas, tenemos que omitir el primer término de la sumatoria.
Por lo tanto, la expresı́ón a partir de (2) es
1
(n − 1)n (3)
2
Que, como dijimos, nos interesa escribir solamenente como O(n2 ). Ahora bien, ¿cuál serı́a la complejidad de un
algoritmo que quisiera contar los ceros sobre toda la matriz?

2.2. La secuencia
Ahora tomemos un segundo ejemplo, donde dado un número n se devuelve una secuencia con los n primeros
números naturales
generarSecuenciaNaturales(n)
1 res ←<>
2 i←0
3 while long(res) < n
4 do res ← res ◦ i
5 i←i+1
6 return res

1 En ciertos casos, puede resultar importante distinguir, por ejemplo, el costo de la suma del de la multiplicación.

2
¿Qué podemos decir, entonces, de la complejidad de este ejemplo? Como se ve, el ciclo se realizará n veces ya
que en cada paso se agrega un elemento y la secuencia comienza estando vacı́a. ¿Quiere decir que el algoritmo
es O(n)? No, no podemos afirmarlo (sólo podemos dar por cierto es que el algoritmo es Ω(n).) ya que no hemos
analizado las operaciones que en el algoritmo se realizan. Entonces, lo que necesitamos es conocer los costos
de las operaciones de secuencia involucradas. Para precisarlos supongamos una implementación de la secuencia
consistente en nodos simplemente encadenados y donde se tiene un puntero al primero de los nodos de la lista
encadenada2 . Entonces es razonable asumir:
<> es O(1)3 .
◦ y long son O(l)4 .
El =, asignación por referencia, es O(1).
Entonces la cuenta que podemos plantear para el algoritmo teniendo en cuenta el costo de las operaciones
que no son constantes es
n−1
X
T◦ (l) + Tlong (l) + Tlong (n) (4)
l=0

Ya que se realizan n iteraciones y n + 1 evaluaciones de la guarda del ciclo; teniendo en cuenta los costos
asumidos (y las propiedades de O() que permiten reemplazar T (n) = O(f (n)) por f (n) directamente) puede
transformarse en
n−1
X
n+ l (5)
l=0

lo que resulta, nuevamente en virtud de la identidad (2), O(n2 ).


El mismo resultado se podrı́a haber conseguido un poco más efecientemente si en lugar de preguntar la
longitud en cada iteración lleváramos un contador para guardar dicho valor5 . De todas maneras, la complejidad
de esta variante no cambiarı́a ya que todavı́a en cada iteración se usa ◦.
Para poder lograr el mismo resultado con una complejidad O(n) basta implementar el siguiente algoritmo

generarSecuenciaNaturalesQuick(n)
1 aux ←<>
2 i←0
3 while i < n
4 do aux ← i • aux
5 i←i+1
6 res ←<>
7 while ¬vacia?(aux)
8 do res ← prim(aux) • res
9 aux ← f in(aux)
10 return res

Donde las funciones nuevas que usamos de secuencia tienen O(1) de complejidad, lo cual es factible para
la implementación propuesta. Entonces, pasamos a tener dos ciclos de O(n), los cuales al combiarlos dan un
algoritmo de la misma complejidad. Resulta más eficiente, entonces, el caso donde tenemos dos ciclos que el
primero donde habı́a uno solo.
2 Otra alternativa hubiera sido no asumir implementación de la secuencia y expresar la complejidad del algoritmo en función de

esos costos desconocidos.


3 ¿Podrı́a ser de otra manera?
4 l es la longitud de la secuencia parámetro.
5 De hecho i puede hacer las veces de éste contador.

3
3. Algunas notaciones
Antes de seguir con más ejemplos, hagamos un alto y realicemos algunos comentarios sobre la notación de
O(). En general decimos , por ejemplo, “el orden del algoritmo A es O(n). . . ”. Esa frase no es del todo correcta
ya que O(), Ω() y Θ() están definidas para funciones matemáticas, tenemos que tener presente que una frase
más correcta serı́a “el orden de la complejidad del algoritmo A es O(n). . . ”, donde la complejidad es la función
matemática que para su algoritmo asociado da una medida de la cantidad de operaciones que éste efectúa.
Al realizar las manipulaciones de términos habituales a la hora de calcular complejidades, suelen surgir
expresiones que podrı́an prestarse a la confusión:

n = O(n) (6)
2n = O(n) (7)

pero no sucede que n = 2n, claro está.


Lo que tenemos que tener siempre en cuenta es que estamos trabajando con “igualdades en un solo sentido”,
la parte derecha de una ecuación no aporta más información que la izquierda.
Más precisamente, podemos pensar que las fórmulas que involucran la notación O(f (n)) han de ser consider-
adas como conjuntos de funciones de n. El sı́mbolo O(f (n)) se refiere al conjunto de todas las funciones g tales
que existe una constante M con |g(n)| ≤ M |f (n)| para todo n grande.
Entonces si S y T son conjuntos de funciones S + T se refiere al conjunto {g + h|g ∈ S ∧ h ∈ T }, de manera
análoga podemos definir S + c,S − T , log S, etc..
Más allá de cómo lo escribamos o lo querramos ver, lo importante es tener en claro qué es lo que está suce-
diendo.
Listemos una serie de identidades que pueden resultarnos útiles

f (n) = O(f (n)) (8)


c · O(f (n)) = O(f (n)) (9)
O(f (n)) + O(f (n)) = O(f (n)) (10)
O(O(f (n))) = O(f (n)) (11)
O(f (n))O(g(n)) = O(f (n)g(n)) (12)
O(f (n)g(n)) = f (n)O(g(n)) (13)

También podemos apuntar que si P (n) es un polinomio de grado menor o igual a m entonces P (n) = O(nm ).
En virtud de las consideraciones anteriores, al trabajar con las expresiones usualmente también ignoraremos
constantes que no nos interesan dejando las expresiones solamente en función de aquellos parámetros relevantes.

4. Más ejemplos
Es recomendable en primer instancia realizar un análisis previo de un algoritmo sin tratar de usar directamente
el aparatejo matemático con que disponemos.

4.1. Los sı́mbolos


Supongamos que tenemos una serie de n sı́mbolos en un arreglo llamado simb (de n posiciones), donde
eventualmente hay sı́mbolos repetidos y donde cada forma de los sı́mbolos tiene asociado un natural (entre 1 y
cantF ormas).
Lo que desea es imprimir cada uno de los sı́mbolos comenzando por el primero la cantidad de veces que el
sı́mbolo aparece y mostrando agrupados los sı́mbolos de igual tipo que pudieran venir en simb

4
imprimirSimbolos(simb)
1 for i ← 1 to cantF ormas
2 do repet[i] = 0
3 for i ← 1 to n
4 do repet[nat(simb[i])] = repet[nat(simb[i])] + 1
5 for i ← 1 to cantF ormas
6 do for j ← 1 to repet[i]
7 do imprimirSimboloDeF orma(i)
8

Entonces, la pregunta de rigor: ¿Cuál es la complejidad del algortimo? Veamos, que es lo que se está hacien-
do. . .
Como dijimos al plantear el problema, se tienen n sı́mbolos cada uno de los cuales es de alguno de los
cantF ormas tipos distintos. Los dos primeros ciclos se realizan cantF ormas y n veces respectivamente. Entonces
nos falta ver el tercer ciclo, con su bucle interno.
Ahora bien, la llamada a imprimirSimbolo se realiza por cada sı́mbolo que se imprime, luego se llama
solamente n veces por más que se encuentre dentro de un par de ciclos anidados. De todas maneras, ¿Podemos
decir, entonces, que el último grupo de ciclos es O(n)? No, no tan rápido; pensemos que sucederı́a si n = 0 (por
lo que ∀ i repet[i] = 0), podemos ver es que, de todas maneras, se iterará cantF ormas veces.
Plateando con cuidado la sumatoria para estos dos últimos ciclos vemos que en cada iteración del ciclo
exterior se evalúa si hay que entrar o no en el ciclo iterno y, eventualmente, se itera
cantF
X ormas
(repet[i] + 1) (14)
i=1

donde es relevante incorporar el término +1 correspondiente a la evaluación de la guarda que se realiza


indefectiblemente en cada iteración del ciclo externo.
Entonces la complejidad es O(cantF ormas + n). Cuando encontramos expresiones que involucran una suma
resulta pertinente considerar en qué situaciones alguno de los dos términos es mayor que el otro. Si tomanos
n ≥ cantF ormas el algoritmo es O(n).

4.2. La altura del árbol


Ahora, analicemos un algortimo para calcular la altura de un árbol

altura(a)
1 if nil?(a)
2 then return 0
3 else d ← altura(der(a))
4 i ← altura(izq(a))
5 return max(d, i) + 1

Para estimar la complejidad de este algoritmo veremos que tenemos herramientas más rigurosas pero en el
espı́ritu del análisis anterior pensemos que es lo que está sucediendo. En cada paso descendemos por cada hijo
una única vez hasta encontrarnos que llegamos a una hoja. En ningún caso volvemos en el camino andado, por
lo cual visitamos cada nodo una única vez; entonces el algoritmo es O(n) siendo n la cantidad de nodos del árbol
recibido como parámetro.
Por otro lado, digamos que no podrı́amos esperar resolverlo con una complejidad menor:¿Si no visitamos
algún nodo, como podemos garantizar que el subárbol que lo tiene como raı́z no es aquel con la rama más larga?

4.3. Recorrer un árbol completo


Ahoras supongamos que tenemos un árbol binario donde, además de la operaciones habituales, contamos con
las operaciones hermanoDerecha, hermanoIzquierda y padre (con las precondiciones pertinentes y para cada una
de ellas con una función asociada que indica si se pueden aplicar). Entonces escribamos un algoritmo que, dado

5
un árbol completo con estas caracterı́sticas, parándose en cada nodo descienda por la derecha hasta llegar a una
hoja. Un pseudocódigo serı́a

recorrerArbolCompleto(a)
1 if ¬nil?(a)
2 then
3 h←a
4 while ¬nil?(der(h))
5 do h ← der(h)
6 sentido ← IZQ
7 visiteRaiz ← f alse
8 while ¬visiteRaiz
9 do b ← h
10 while ¬nil?(der(b))
11 do imprimir(b)
12 b ← der(b)
13 if tieneP adre?(h)
14 then if sentido = IZQ
15 then if tieneHmnoIzq?(h)
16 then h ← hermanoIzquierda(h)
17 else h ← padre(h)
18 sentido ← DER
19 else if tieneHmnoDer?(h)
20 then h ← hermanoDerecha(h)
21 else h ← padre(h)
22 sentido = IZQ
23 else visiteRaiz ← true

Entonces, ¿cúantas veces se realiza la impresión? Veamos, intentemos expresar dicha cantidad en función de
la cantidad de nodos del árbol (n). Lo que hace el algoritmo es pararse en cada nodo y descender hasta una
hoja, claramente en cada descenso se recorre a lo sumo la altura de nuestro árbol (que, dado que es completo,
es lg(n + 1)). Entonces podemos decir que se imprime O(n · lg(n + 1)) veces, lo que es O(n · lg n).
Lo que hemos encontrado es una cota superior a la cantidad de impresiones; tratemos de afinar la cuenta
para hallar una cota más justa, ¿Se podrá? Como dijimos, parándonos en cada nodo bajamos hasta una hoja,
pero no siempre recorremos la altura; de hecho sólo lo hacemos cuándo empezamos en la raı́z. Por otro lado,
cuando empezamos en las hojas de hecho nada imprimimos. Notemos, por lo tanto, que a medida que vamos
subiendo en cada nivel cada vez recorremos más al bajar pero también se reduce la cantidad de nodos a partir
de los cuáles se comienza el descenso.
Para precisar lo enunciado en el párrafo precedente, recordemos que en el nivel i (la raı́z está en el nivel
0) de un árbol completo hay 2i nodos. Por otro lado, nuestro algoritmo para cada nodo del nivel i imprime
altura(a) − i veces. Ahora estamos en condiciones de plantear la siguiente sumatoria
altura(a)
X
2i (altura(a) − i) (15)
i=0

Ahora hagamos el siguiente reemplazo en la sumatoria, llamemos j = altura(a) − i. Reemplazando


altura(a)
X
2altura(a)−j (j) (16)
j=0

Lo cual puede ser reescrito como


altura(a)
X j
2altura(a) (17)
j=0
2j

6
Ahora bien altura(a) = lg(n + 1), entonces tenemos
altura(a)
X j
(n + 1) (18)
j=0
2j

¿Pero la sumatoria, cuánto vale? Notemos que tenemos una del tipo aritmético-geométrica, cuya razón es
menor a uno (tenemos 1/2) entonces la serie infinita converge a un cierto número c constante. Entonces, la nueva
cota para la cantidad de impresiones es O(n)6 . De la misma manera en que nos planteamos antes: ¿Podrı́amos
encontrar una mejor cota asintótica?
¿Es esta la complejidad del algoritmo? No, no podemos asegurar esto ya que no conocemos la complejidad de
las operaciones utilizadas. De todas formas, la cuenta realizada consituye la base del análisis de este algoritmo,
lo único que falta, como dijimos, es tener en cuenta cuánto cuestan las operaciones de árboles que utilizamos
además de indicar que el ciclo previo que se sitúa en la hoja de la extrema derecha cuesta lg n (lo cual no afecta
a la cota hallada).

4.4. La mano de póker


Por último planteemos un algoritmo para realizar una mano de un póker ficticio. Supongamos que tenemos
j jugadores por cada uno de los cuales se reparten c cartas. Luego de que los jugadores piensan su jugada cada
uno tiene la posibilidad de cambiar la cantidad de cartas que desee a menos que el primer jugador decida que
existe un lı́mite de l cartas a pedir. Una vez realizada la nueva distribución se calcula el (o los) ganadores

manoDeCartas(mazo, jugadores, j, c)
1 for jj ← 1 to j
2 do cartas[jj] =<>
3 for cc ← 1 to c
4 do for jj ← 1 to j
5 do cartas[jj] = sacarCarta(mazo) • cartas[jj]
6 hayLimite ← hayLimite(jugadores[0], cartas[0])
7 if hayLimite
8 then
9 limite ← limite(jugadores[0], cartas[0])
10 for jj ← 1 to j
11 do if hayLimite
12 then cartas[jj] ← sacarCartas(jugardores[jj], cartas[jj], limite)
13 else cartas[jj] ← sacarCartas(jugardores[jj], cartas[jj])
14 despues ← long(cartas[jj])
15 for n ← despues to c
16 do cartas[jj] = sacarCarta(mazo) • cartas[jj]
17 puntosGanadores ← calcularP untaje(cartas[jj])
18 ganadores ← 0• <>
19 for jj ← 2 to j
20 do ptje ← calcularP untaje(cartas[jj])
21 if ptje > puntosGanadores
22 then ganadores ← jj• <>
23 if ptje = puntosGanadores
24 then ganadores ← jj • ganadores
25 return ganadores

Supongamos la misma implementación de las secuencias, arreglos con operaciones de tiempo constante y los
siguientes órdenes

sacarCarta es O(1).
6 Notar que esta cota no es en algún caso promedio sino que es válida en el peor caso; la encontrada anteriormente no era tan

buena.

7
hayLimite, limite y sacarCartas (sin lı́mite) son O(car).
calcularPuntaje es O(car2 ).
sacarCartas (con limite) son O(car · lim).

donde car es la cantidad de cartas recibidas y lim el lı́mite impuesto. Queda como ejericio (para quien quiera,
obviamente) calcular la complejidad de este algoritmo.

5. Un caso promedio. . . ¿simple?


Hasta ahora nos ocupamos de considerar los peores casos de ejecución y trabajar con notación ası́ntotica,
tal como haremos fundamentalmente en la materia, en función de las consideraciones realizadas. Sin embargo,
alejémonos de esas premisas por un momento y tomemos un algoritmo sencillo (dado un arreglo de n posiciones
se desea hallar el máximo en él) para ver qué tipo de análisis surge al considerar casos promedios y dejar de lado
la notación asintótica.
maximo(D)
1 max ← D[0]
2 for j ← 2 to n
3 do aux ← D[j]
4 if aux > max
5 then max ← aux
6 return max

Haciendo el tipo de análisis que hasta ahora realizábamos el algoritmo es O(n) temporalmente hablando;
como siempre se recorre todo el arreglo el algoritmo también es Ω(n) (por lo tanto Θ(n)).
Ahora bien, si nos ponemos más detallistas podrı́a interesarnos la cantidad de veces que se ejecuta cada paso
del algoritmo. Una vez se ejecuta el primer paso, n veces la evaluación de la guarda del ciclo y n − 1 veces tanto
la obtención de un nuevo elemento dentro del ciclo como la comparación del nuevo valor con max. Pero:¿cuántas
veces se ejecuta el reemplazo de max por el nuevo valor encontrado? Para completar el análisis estudiemos esa
cantidad, llamemósla A.
Veamos algunos casos, en el peor de los casos A = n − 1, eso sucede cuando el arreglo está ordenado creciente-
mente. En cada paso encontramos un valor que es mayor a todos los que habı́amos visto antes. Recı́procamente,
en el mejor de los casos A = 0, cuando el arreglo está ordenada decrecientemente, el primer elemento es el
mayor de todos. Lo que no queda tan claro es: ¿cuál es el valor promedio de A? Claramente cae entre 0 y n − 1.
¿Es 21 n? ¿Es 13 n? Resulta crucial para responder está pregunta definir precisamente qué queremos significar por
promedio. Para ello asumiremos ciertas caracterı́sticas del arreglo:
Todos sus elementos son distintos.
Cada una de las n! permutaciones de los n valores de la secuencia es igualmente probable.
Notemos que la performance del algoritmo no depende de los valores en sı́ sino más bien del orden entre ellos.
A los efectos del análisis, entonces, podemos decir que el arreglo está formado por los números 1 . . . n en algún
orden.
La probabilidad de que A tenga el valor k es

pnk = (número de permutaciones de n objetos para las cuales A = k)/n! (19)

Recordemos cómo se definı́a el promedio (media):


X
An = kpnk (20)
k

También cómo se definı́a la varianza, el promedio de (A − An )2 :


X X
Vn = (k − An )2 pnk = k 2 pnk − A2n (21)
k k

8

Por último, la desviación estándar (σn ) es Vn .
El significado de σn puede enterderse notando que, para todo r ≥ 1, la probabilidad que A no caiga dentro
de rσn de su valor medio es menos que 1/r2 . Por ejemplo, |A − An | > 2σn con probabilidad < 1/4.
Podemos determinar el comportamiento de A determinando las probalidades pnk . Por la ecuación (19),
queremos contar el número de permutaciones que tienen A = k.
Consideremos las permutaciones x1 x2 . . . xn en {1, 2, . . . , n}. Si xn = n, el valor de A es uno más que el valor
obtenido en x1 . . . xn−1 ; si xn 6= n, el valor de A es exactamente el mismo que el de x1 . . . xn−1 . Entonces:
1
pnk = (]perm de n − 1 con A = k − 1 + (]perm de n − 1 con A = k)n − 1) (22)
n!
Donde el factor n-1 da cuenta de las n-1 posiciones en la permutación original donde se puede encontrar n (todas
las posiciones excepto la última). Luego dividiendo cada término por n! y escribiéndolo como n(n − 1)! en virtud
de (19)
1 n−1
pnk = p(n−1)(k−1) + p(n−1)k (23)
n n
Esta ecuación determinará pnk si proveemos los valores iniciales:

p1k = δ0k (24)


Donde δij es la Delta de Kronecker, que vale uno si i = j y cero en caso contrario. Por otro lado para k < 0

pnk = 0 (25)
Ahora introducimos la siguiente función (este tipo de funciones se denominan generadoras o generatrices):
X
Gn z = pn0 + pn1 z + . . . = pnk z k (26)
k

Como sabemos que A ≤ n − 1 resulta que pnk = 0 para valores grandes de k. Entonces Gn (z) resulta ser un
polinomio.
Por (24) resulta que G1 (z) = 1 y por (23)
z n−1 z+n−1
Gn (z) = Gn−1 (z) + Gn−1 (z) = Gn−1 (z) (27)
n n n
Luego, utilizando esta misma expresión pero sobre Gn−1 (z) y ası́ sucesivamente
1
Gn (z) = (z + n − 1)(z + n − 2) . . . (z + 1) (28)
n!
Entonces,  
1 z+n
Gn (z) = (29)
z+n n
Tomemos directamente el siguiente resultado
1 X h n i k−1
Gn (z) = z (30)
n! k
k

donde aparecen los números de Stirling de primer tipo


hni X
= k1 k2 . . . kn−m (31)
m
0<k1 <k2 <...<kn−m <n

Por lo que, entonces


hni
pnk = /k! (32)
k
Entonces, con una expresión para los coeficientes lo que tenemos que hacer es usarlos en las ecuaciones
(20) y (21). Pero resulta que esto es complicado. De hecho, es poco habitual que se tengan determinadas
explı́citamente la probabilidades. En la mayorı́a de los casos lo que se conoce es la función generadores Gn (z).

9
El hecho importante es que se pueden determinar el promedio y la varianza fácilmente a partir de la función
misma.
Para lograr esto tomemos una función generadora cuyos coeficientes representen probabilidades

G(z) = p0 + p1 z + p2 z 2 + . . . (33)
Entonces queremos calcular
X
mean(G) = kpk (34)
k
y también
X
var(G) = k 2 pk − (mean(G))2 (35)
k
Ahora notemos que G(1) = 1 ya que es la suma de todas las probabilidades.
Si derivamos
X
G0 (z) = kpk z k−1 (36)
entonces

mean(G) = G0 (1) (37)


Derivando nuevamente se puede obtener

var(G) = G00 (1) + G0 (1) − G0 (1)2 (38)


Para el caso que nos interesaba, queremos calcular G0n (1) = An . Por la ecuación (27) tenemos
1 z+n−1 0
G0n (z) = Gn−1 (z) + Gn−1 (z) (39)
n n
evaluando
1
G0n (1) = + G0n−1 (1) (40)
n
pero a partir de la condición inicial G0 (1) = 0

An = G0n (1) = Hn − 1 (41)


Donde Hn son los números armónicos definidos por
X 1
Hn = (42)
k
1≤k≤n

Para n grande resulta que An es aproximadamente ln n.


Ya tenemos calculado el promedio, conformémonos con sólamente decir, por último, que

var(A) = Hn − Hn(2) (43)


donde
X 1
Hn(i) = (44)
ki
1≤k≤n

Este desarrollo fue extraido de §1.2.10 de [Knu73].

Referencias
[Knu73] Donald E. Knuth. The art of computer programming, volume Fundamental Algorithms of Computer
Science and information processing. Addison-Wesley, second edition, 1973.

10

También podría gustarte