Aplicaciones.

## ☀**5.APLICACIONES**

Principalmente se enfoca en algoritmos eficientes
en optimización discreta, combinatoria, continua, on-line, en teoría
de grafos, aplicaciones internet y análisis de ficheros de texto y también
para la evaluación de consultas particulares de bases de datos muy grandes
en genómica, recuperación de imágenes, búsquedas en la web y geometría
elemental. Así mismo se utiliza en la complejidad en computación científica
(cotas superiores e inferiores de complejidad en el contexto numérico,
seminumérico y simbólico).

Se
utiliza en los siguientes campos:

Teoría
general de grafos, optimización lineal y no-lineal, local y global, algoritmos
on-line, Knowledge Management, microeconomía y algorítmica de publicidad
on-line, complejidad de Kolmogorov y azar, algoritmos para problemas de
palabras (stringology) y se puede aplicar en genómica, álgebra lineal
numérica, sampling, teoría de números efectiva, criptografía, constraint data
bases, geometría algebraica, semi-algebraica y diofántica efectiva
(Computer Algebra), teoría algebraica de la complejidad.

El
siguiente ejemplo nos ayuda a resolver problemas de logística en alguna empresa
o centro de distribución, pues se pueden calcular los tiempos de ejecución en
el mejor, peor o caso medio.

Supongamos
que disponemos de la siguiente definición de tipo:

Consideramos entonces los procedimientos y
funciones siguientes:

a)
Calcular sus tiempos de ejecución en el mejor, peor, y caso medio.

b) Dar
cotas asintóticas O, Ω y Θ para las funciones anteriores.

**Solución:**

Procedimiento
Algoritmo 1

Para
obtener el tiempo de ejecución, calcularemos primero el número de operaciones
elementales (OE) que se realizan:

– En la
línea (1) se ejecutan 3 OE (una asignación, una resta y una comparación) en
cada una de las iteraciones del bucle más otras 3 al final, cuando se efectúa
la salida del FOR.

– Igual
ocurre con la línea (2), también con 3 OE (una asignación, una suma y una
comparación) por iteración, más otras 3 al final del bucle.

– En la
línea (3) se efectúa una condición, con un total de 4 OE (una diferencia, dos
accesos a un vector, y una comparación).

– Las
líneas (4) a (6) sólo se ejecutan si se cumple la condición de la línea (3), y
realizan un total de 9 OE: 3, 4 y 2 respectivamente.

Con esto:

En el
caso mejor para el algoritmo la condición de la línea (3) será siempre falsa, y
no se ejecutarán nunca las líneas (4), (5) y (6). Así, el bucle más interno
realizará (n–i) iteraciones, cada una de ellas con 4 OE (línea 3), más las 3 OE
de la línea (2). Por tanto, el bucle más interno realiza un total de:

OE,
siendo el 3 adicional por la condición de salida del bucle. A su vez, el bucle
externo repetirá esas 7(n–i)+3 OE en cada iteración, lo que hace que el número
de OE que se realizan en el algoritmo sea:

• En el
caso peor, la condición de la línea (3) será siempre verdadera, y las líneas
(4), (5) y (6) se ejecutarán en todas las iteraciones. Por tanto, el bucle más
interno realiza:

OE. El
bucle externo realiza aquí el mismo número de iteraciones que en el caso
anterior, por lo que el número de OE en este caso es:

• En el
caso medio, la condición de la línea (3) será verdadera con probabilidad 1/2.
Así, las líneas (4), (5) y (6) se ejecutarán en la mitad de las iteraciones del
bucle más interno, y por tanto realiza:

OE. El
bucle externo realiza aquí el mismo número de iteraciones que en el caso
anterior, por lo que el número de OE en este caso es:

Como los
tiempos de ejecución en los tres casos son polinomios de grado 2, la
complejidad del algoritmo es cuadrática, independientemente de qué caso se
trate.

Obsérvese
cómo hemos analizado el tiempo de ejecución del algoritmo sólo en función de su
código y no respecto a lo que hace, puesto que en muchos casos esto nos llevaría
a conclusiones erróneas. Debe ser a posteriori cuando se analice el objetivo
para el que fue diseñado el algoritmo.

En el
caso que nos ocupa, un examen más detallado del código del procedimiento nos
muestra que el algoritmo está diseñado para ordenar de forma creciente el
vector que se le pasa como parámetro, siguiendo el método de la Burbuja. Lo que
acabamos de ver es que sus casos mejor, peor y medio se producen
respectivamente cuando el vector está inicialmente ordenado de forma creciente,
decreciente y aleatoria.

Función
Algoritmo 2 (Guerequeta & Vallecillo, 1998)

Para
calcular el tiempo de ejecución, calcularemos primero el número de operaciones
elementales (OE) que se realizan:

– En la
línea (1) se ejecutan 2 OE (dos asignaciones).

– En la línea
(2) se efectúa la condición del bucle, que supone 1 OE (la comparación).

– Las
líneas (3) a (6) componen el cuerpo del bucle, y contabilizan 3, 2+1, 2+2 y 2
OE respectivamente. Es importante hacer notar que el bucle también puede
finalizar si se verifica la condición de la línea (4).

– Por
último, la línea (9) supone 1 OE. A ella se llega cuando la condición del bucle
WHILE deja de ser cierta.

Con esto:

• En el
caso mejor se efectuarán solamente la líneas (1), (2), (3) y (4). En
consecuencia, T(n) = 2+1+3+3 = 9.

• En el
caso peor se efectúa la línea (1), y después se repite el bucle hasta que su
condición sea falsa, acabando la función al ejecutarse la línea (9). Cada
iteración del bucle está compuesta por las líneas (2) a (8), junto con una
ejecución adicional de la línea (2) que es la que ocasiona la salida del bucle.
En cada iteración se reducen a la mitad los elementos a considerar, por lo que
el bucle se repite logn veces. Por tanto,

• En el
caso medio, necesitamos calcular el número medio de veces que se repite el
bucle, y para esto veamos cuántas veces puede repetirse, y qué probabilidad
tiene cada una de suceder.

Por un
lado, el bucle puede repetirse desde una vez hasta logn veces, puesto que en
cada iteración se divide por dos el número de elementos considerados. Si se
repitiese una sola vez, es que el elemento ocuparía la posición n/2, lo que
ocurre con una probabilidad 1/(n+1). Si el bucle se repitiese dos veces es que
el elemento ocuparía alguna de las posiciones n/4 ó 3n/4, lo cual ocurre con
probabilidad 1/(n+1)+1/(n+1)=2/(n+1). En general, si se repitiese i veces es
que el elemento ocuparía alguna de las posiciones nk/2i , con k impar y 1≤k. Es
decir, el bucle se repite i veces con probabilidad 2i–1/(n+1). Por tanto, el
número medio de veces que se repite el ciclo vendrá dado por la expresión:

Con esto,
la función ejecuta la línea (1) y después el bucle se repite ese número medio
de veces, saliendo por la instrucción RETURN en la línea (4). Por consiguiente,

En el
caso mejor el tiempo de ejecución es una constante. Para los casos peor y
medio, la complejidad resultante es de orden Θ(logn) puesto que:

Es una
constante finita y distinta de cero en ambos casos (10 y 8 respectivamente).

Función
Euclides

En este
caso el análisis del tiempo de ejecución y la complejidad de la función sigue
un proceso distinto al estudiado en los casos anteriores.

Lo
primero es resaltar algunas características del algoritmo, siguiendo una línea
de razonamiento similar a la de [BRA97]:

[1] Para
cualquier par de enteros no negativos m y n tales que n≥m, se verifica que n
MOD m < n/2. Veámoslo:

a) Si m
> n/2 entonces 1≤ n/m < 2 y por tanto n DIV m = 1, lo que implica que n
MOD m = n – m(n DIV m) = n – m < n – n/2 = n/2.

b) Por
otro lado, si m ≤ n/2 entonces n MOD m < m ≤ n/2.

[2]
Podemos suponer sin pérdida de generalidad que n ≥ m. Si no, la primera
iteración del bucle intercambia n con m ya que n MOD m = n cuando n < m.
Además, la condición n ≥ m se conserva siempre (es decir, es un invariante del
bucle) pues n MOD m nunca es mayor que m.

[3] El cuerpo
del bucle efectúa 4 OE, con lo cual el tiempo del algoritmo es del orden exacto
del número de iteraciones que realiza el bucle. Por consiguiente, para
determinar la complejidad del algoritmo es suficiente acotar este número.

[4] Una
propiedad curiosa de este algoritmo es que no se produce un avance notable con
cada iteración del bucle, sino que esto ocurre cada dos iteraciones.
Consideremos lo que les ocurre a m y n cuando el ciclo se repite dos veces,
suponiendo que no acaba antes. Sean m0 y n0 los valores originales de los
parámetros, que podemos suponer n0 ≥ m0 por [2]. Después de la primera
iteración, m vale n0 MOD m0. Después de la segunda iteración, n toma ese valor,
y por tanto ya es menor que n0/2 (por [1]). En consecuencia, n vale menos de la
mitad de lo que valía tras dos iteraciones del bucle. Como se sigue manteniendo
que n ≥ m, el mismo razonamiento se puede repetir para las siguientes dos
iteraciones, y así sucesivamente.

El hecho
de que n valga menos de la mitad cada dos iteraciones del bucle es el que nos
permite intuir que el bucle se va a repetir del orden de 2logn veces. Vamos a
demostrar esto formalmente.

Para
ello, vamos a tratar el bucle como si fuera un algoritmo recursivo. Sea T(l) el
número máximo de veces que se repite el bucle para valores iniciales m y n
cuando m ≤ n ≤ l. En este caso l representa el tamaño de la entrada.

– Si n ≤
2 el bucle no se repite (si m = 0) o se hace una sola vez (si m es 1 ó 2).

– Si n
> 2 y m=1 o bien m divide a n, el bucle se repite una sola vez.

– En otro
caso (n > 2 y m no divide a n) el bucle se ejecuta dos veces, y por lo visto
en [4], n vale a lo sumo la mitad de lo que valía inicialmente. En consecuencia
n ≤ (l/2), y además m se sigue manteniendo por debajo de n.

Esto nos
lleva a la ecuación en recurrencia T(l) ≤ 2 + T(l/2) si l > 2, T(l) ≤ 1 si
l≤ 2, lo que implica que el algoritmo de Euclides es de complejidad logarítmica
respecto al tamaño de la entrada (l).

Nos
preguntaremos la razón de usar T(l) para acotar el número de iteraciones que
realiza el algoritmo en vez de definir T directamente como una función de n, el
mayor de los dos operandos, lo cual sería mucho más intuitivo.

El
problema es que si definimos T(n) como el número de iteraciones que realiza el
algoritmo para los valores m ≤ n, no podríamos concluir que T(n) ≤ 2 + T(n/2)
del hecho de que n valga la mitad de su valor tras cada dos iteraciones del
bucle.

Por
ejemplo, para Euclides (8,13), obtenemos que T(13) = 5 en el peor caso,
mientras que T(13/2) = T(6) = 2. Esto ocurre porque tras dos iteraciones del
bucle n no vale 6, sino 5 (y m = 3), y con esto sí es cierto que T(13) ≤ 2 +
T(5) ya que T(5) = 3.

La raíz
de este problema es que esta nueva definición más intuitiva de T no lleva a una
función monótona no decreciente (T(5) > T(6)) y por tanto la existencia de
algún n’ ≤ n/2 tal que T(n) ≤ 2 + T(n’) no implica necesariamente que T(n) ≤ 2
+ T(n/2).

En vez de
esto, solamente podríamos afirmar que T(n)≤2+max{T(n’)|n’≤n/2}, que es una
ecuación en recurrencia bastante difícil de resolver. Esa es la razón de que
escogiésemos nuestra función T de forma que fuera no decreciente y que
expresara una cota superior del número de iteraciones.

Para
acabar, es interesante hacer notar una característica curiosa de este
algoritmo: se demuestra que su caso peor ocurre cuando m y n son dos términos
consecutivos de la sucesión de Fibonacci.

T(l)∈Θ(logl) como se deduce de la ecuación en
recurrencia que define el tiempo de ejecución del algoritmo.

Procedimiento
Misterio

En este
caso son tres bucles anidados los que se ejecutan, independientemente de los
valores de la entrada, es decir, no existe peor, medio o mejor caso, sino un
único caso.

Para
calcular el tiempo de ejecución, veamos el número de operaciones elementales
(OE) que se realizan:

– En la
línea (1) se ejecuta 1 OE (una asignación).

– En la
línea (2) se ejecutarán 3 OE (una asignación, una resta y una comparación) en
cada una de las iteraciones del bucle más otras 3 al final, cuando se efectúa
la salida del FOR.

– Igual
ocurre con la línea (3), también con 3 OE (una asignación, una suma y una
comparación) por iteración, más otras 3 al final del bucle.

– Y
también en la línea (4), esta vez con 2 OE (asignación y comparación) más las 2
adicionales de terminación del bucle.

– Por
último, la línea (5) supone 2 OE (un incremento y una asignación).

Con esto,
el bucle interno se ejecutará j veces, el medio (n–i) veces, y el bucle
exterior (n–1) veces, lo que conlleva un tiempo de ejecución de:

Como el
tiempo de ejecución es un polinomio de grado 3, la complejidad del algoritmo es
de orden Θ(n 3).

# Bibliografía

Guerequeta, R., & Vallecillo, A.
(1998). *Técnicas de Diseño de Algoritmos.* Malaga, España: Servicio de
Publicaciones de la Universidad de Málaga.

Huelva, U. d. (18 de 12 de 2017). *Educación*
Holística*. Obtenido de La Verdad mas allá de las Formas:*
https://www.educacion-holistica.org/notepad/documentos/Informatica/Algoritmia/Complejidad%20de%20Algoritmos%202.pdf