Está en la página 1de 8

Algoritmos y Complejidad Unidad 1: Introducción

Unidad 1:
Introducción

1.1. Conceptos preliminares: Problemas, Algoritmos,


Programas.
Un problema computacional consiste en una caracterización de un conjunto de datos
de entrada junto con una especificación de la salida deseada en base a cada entrada.

Un algoritmo es una secuencia bien determinada de acciones elementales que


transforma los datos de entrada en datos de salida con el objetivo de resolver un problema
computacional.

• Para cada algoritmo es necesario aclarar cuáles son las operaciones elementales y
cómo están representados los datos de entrada y de salida.

• En general se representarán los algoritmos por medio de un pseudo código


informal.

Un programa consiste en la especificación de un algoritmo por medio de un lenguaje


de programación, de forma que pueda ser ejecutado por una computadora.

1.2. Instancias de un problema. Tamaño de una instancia.


Un problema computacional tiene una o más instancias, valores particulares para los
datos de entrada, sobre las cuales se puede ejecutar un algoritmo para resolver el problema.

F.C.A.D – U.N.E.R. 1
Algoritmos y Complejidad Unidad 1: Introducción

Ejemplo: el problema computacional multiplicar dos números enteros tiene infinitas


instancias, como por ejemplo multiplicar 8312 por 725, multiplicar 759 por 10000,
multiplicar –6482 por 1, etc.

Un problema computacional abarca a otro problema computacional si las instancias


del segundo pueden ser resueltas como instancias del primero en forma directa.

Ejemplo: el problema computacional multiplicar un entero por 359 es abarcado por el


problema multiplicar dos números enteros.

El tamaño de una instancia corresponde formalmente al número de bits necesarios


para representar la instancia en una computadora, usando algún esquema de codificación
precisamente definido y razonablemente compacto. Menos formalmente, se usará la
palabra tamaño para significar cualquier entero que mida de alguna manera el número de
componentes de una instancia. Por ejemplo, cuando hablamos de ordenamiento, por lo
general mediremos el tamaño de la instancia por el número de elementos a ser ordenados,
ignorando el hecho de que cada elemento podría utilizar más de un bit de computadora.
Cuando se habla de grafos, el tamaño de una instancia se mide en general por el número de
nodos o arcos (o ambos) involucrados. En una multiplicación de matrices, el tamaño se
refiere al número de filas y columnas de las matrices a multiplicar.

1.3. Problemas de la Algoritmia: correctitud, eficiencia.


Un algoritmo debe funcionar correctamente sobre todas las instancias del problema
que dice resolver. Para mostrar que un algoritmo es incorrecto, sólo se necesita encontrar
una instancia del problema para la cual el algoritmo es incapaz de encontrar una respuesta
correcta. Es decir que un algoritmo puede ser rechazado sobre las bases de un único
resultado erróneo. Pero puede ser muy difícil probar la correctitud de un algoritmo. Para
que esto sea posible, cuando se especifica un problema es importante especificar su
dominio de definición, es decir, el conjunto de instancias a considerar. En el ejemplo visto
anteriormente, el algoritmo de multiplicación de enteros no funcionará para números
reales, al menos si no se introducen algunas modificaciones. Esto no significa que el
algoritmo no sirve, sólo que los números reales no están en el dominio de definición que se
eligió.

F.C.A.D – U.N.E.R. 2
Algoritmos y Complejidad Unidad 1: Introducción

Cuando tenemos un problema a resolver, puede haber varios algoritmos disponibles, y


obviamente nos interesa elegir el mejor. ¿Por qué? Básicamente porque un buen algoritmo
implementado en una computadora lenta puede ejecutarse mucho mejor que un mal
algoritmo implementado en una computadora rápida. Entonces surge la pregunta de cómo
decidir cuál de todos los algoritmos es preferible. Si el problema es simple, no nos
preocupamos demasiado y elegimos el más fácil de programar, o uno cuyo programa ya
existe. Sin embargo, si tenemos muchas instancias a resolver, o si el problema es difícil,
debemos elegir con más cuidado.

El enfoque empírico (o a posteriori) para elegir un algoritmo consiste en programar las


diferentes técnicas y probarlas con diferentes instancias con la ayuda de una computadora.
El enfoque teórico (o a priori), que es el que se privilegia en este curso, consiste en
determinar matemáticamente la cantidad de recursos que necesita cada algoritmo como
una función del tamaño de las instancias consideradas, con lo cual se podrá acotar (por
arriba o por debajo) su tiempo de ejecución. Los recursos que nos interesan son el tiempo
de computación y el espacio de almacenamiento, aunque el primero es generalmente el
más crítico. Por lo tanto, de aquí en más, cuando hablemos de eficiencia de un algoritmo
estaremos hablando simplemente de cuán rápido corre, salvo que se haga explícito lo
contrario.

La ventaja del enfoque teórico es que no depende de la computadora que se usa, ni del
lenguaje de programación, ni siquiera de la habilidad del programador. Ahorra el tiempo
que se perdería tanto programando un algoritmo ineficiente como probándolo en una
máquina. Más importante aún, nos permite estudiar la eficiencia de un algoritmo
trabajando con instancias de cualquier tamaño. Este no es el caso con el enfoque empírico,
donde las consideraciones prácticas pueden forzarnos a probar nuestros algoritmos sólo
con un pequeño número de instancias de moderado tamaño elegidas arbitrariamente. Dado
que es frecuente que un algoritmo que recién se descubre comience a trabajar mejor que su
predecesor sólo cuando ambos se usan sobre instancias grandes, este último punto es
particularmente importante.

También se puede analizar un algoritmo usando un enfoque híbrido, donde la forma de


la función que describe la eficiencia del algoritmo se determina teóricamente, y luego se
determinan empíricamente los parámetros numéricos requeridos para un programa y una
máquina en particular, usualmente mediante algún tipo de regresión.

F.C.A.D – U.N.E.R. 3
Algoritmos y Complejidad Unidad 1: Introducción

1.4. Tiempo y espacio de un algoritmo, aproximación.


Si queremos medir la cantidad de almacenamiento que usa un algoritmo como una
función del tamaño de las instancias, tenemos una unidad natural disponible que es el bit.
Sin tener en cuenta la máquina que se usa, la noción de un bit de almacenamiento está bien
definida. Si por el contrario, como se da más frecuentemente, queremos medir la eficiencia
de un algoritmo en términos del tiempo que le lleva llegar a una respuesta, la elección no
es tan obvia. Para resolver este problema debemos tener en cuenta el principio de
invarianza.

El principio de invarianza establece que dos implementaciones diferentes del mismo


algoritmo no diferirán en eficiencia más que en alguna constante multiplicativa.
Supongamos por ejemplo que esa constante sea 5. Entonces sabemos que si la primera
implementación toma 1 segundo en resolver instancias de un tamaño particular, entonces
la segunda implementación (quizás en una máquina diferente, o escrita en otro lenguaje de
programación) no tomará más de 5 segundos en resolver las mismas instancias. Este
principio no es algo que podamos probar: simplemente establece un hecho que puede
confirmarse con la observación. Además, tiene una aplicación muy amplia. El principio
permanece verdadero cualquiera sea la computadora usada para implementar un algoritmo,
independientemente del lenguaje de programación y del compilador empleado, e incluso
de la habilidad del programador.

Volviendo al tema de la unidad a usar para expresar la eficiencia de un algoritmo, el


principio de invarianza nos permite decidir que no hay tal unidad. Decimos que un
algoritmo toma un tiempo en el orden de t(n), para una función t dada, si existe una
constante positiva c y una implementación del algoritmo capaz de resolver cualquier
instancia de tamaño n en no más de ct(n) segundos. El uso de segundos en esta definición
es obviamente arbitraria: sólo necesitamos cambiar la constante para que la eficiencia
quede expresada en años o en microsegundos.

Dado el algoritmo A, definimos el tiempo de ejecución t (n) de A como la cantidad de


A

pasos, operaciones o acciones elementales que debe realizar un algoritmo al ser ejecutado
en una instancia de tamaño n. Este tiempo depende de diversos factores, como los datos de
entrada que se le suministra, la calidad del código generado por el compilador para crear el
programa objeto, la naturaleza y rapidez de las instrucciones máquina del procesador
concreto que ejecuta el programa, y la complejidad intrínseca del algoritmo.

F.C.A.D – U.N.E.R. 4
Algoritmos y Complejidad Unidad 1: Introducción

El espacio e (n) de A es la cantidad de datos elementales que el algoritmo necesita al


A

ser ejecutado en una instancia de tamaño n, sin contar la representación de la entrada.

Estas definiciones son ambiguas en dos sentidos:

• No está claramente especificado cuáles son las operaciones o los datos elementales.

• Dado que puede haber varias instancias de tamaño n, no está claro cuál de ellas es
la que se tiene en cuenta para determinar la cantidad de recursos necesaria.

1.5. Tipos de análisis: peor caso, caso promedio, análisis


probabilístico, mejor caso.
Para resolver el problema presentado anteriormente, se definen distintos tipos de
análisis:

• Análisis en el peor caso: se considera el máximo entre las cantidades de recursos


insumidas por todas las instancias de tamaño n.

• Análisis caso promedio: se considera el promedio de las cantidades de recursos


insumidas por todas las instancias de tamaño n.

• Análisis probabilístico: se considera la cantidad de recursos de cada instancia de


tamaño n pesada por su probabilidad de ser ejecutada.

• Análisis en el mejor caso: se considera el mínimo entre las cantidades de recursos


insumidas por todas las instancias de tamaño n.

En general concentraremos nuestro análisis en el peor caso, porque:

• Constituye una cota superior al total de los recursos insumidos por el algoritmo.
Conocerla nos asegura que no se superará esa cantidad.

• Para muchos algoritmos, el peor caso es el que ocurre más seguido.

• Debido al uso de la notación asintótica, el caso promedio o probabilístico es


muchas veces el mismo que el peor caso.

F.C.A.D – U.N.E.R. 5
Algoritmos y Complejidad Unidad 1: Introducción

• No se necesita conocer la distribución de probabilidades para todas las instancias


de un mismo tamaño, como sería necesario en el análisis probabilístico.

Se considerará entonces que un algoritmo es más eficiente que otro para resolver un
determinado problema si su tiempo de ejecución (o espacio) en el peor caso tiene un
crecimiento menor.

1.6. Ejemplos simples de Análisis de Algoritmos.


Problema: multiplicación de dos números enteros

Algoritmos: a la americana, a la inglesa, a la rusa

A la Americana A la Inglesa A la Rusa


9 8 1 9 8 1 981 1234 1234
× 1 2 3 4 × 1 2 3 4 490 2468
3 9 2 4 9 8 1 245 4936 4936
2 9 4 3 1 9 6 2 122 9872
1 9 6 2 2 9 4 3 61 19744 19744
9 8 1 3 9 2 4 30 39488
1 2 1 0 5 5 4 1 2 1 0 5 5 4 15 78976 78976
7 157952 157952
3 315904 315904
1 631808 631808
Algoritmo de multiplicación a la rusa: 1210554

function multRusa(m,n)
prod  0
repeat
if m es impar then
prod  prod + n
end if
mm÷2
nn+n
until m = 1
return prod

Problema: ordenar un arreglo A de n elementos enteros

Algoritmos: inserción, selección

F.C.A.D – U.N.E.R. 6
Algoritmos y Complejidad Unidad 1: Introducción

El algoritmo de ordenamiento por inserción examina sucesivamente cada elemento del


arreglo desde el segundo al último y lo inserta en el lugar apropiado entre sus
predecesores. El ordenamiento por selección trabaja extrayendo el menor elemento del
arreglo y llevándolo al principio; luego extrae el segundo menor elemento y lo lleva al
segundo lugar, y así sucesivamente.

Ordenamiento por inserción Ordenamiento por selección


procedure insercion(A[1..n]) procedure seleccion(A[1..n])
for i  2 to n do for i  1 to n – 1 do
x  A[i] minj  i
ji–1 minx  A[i]
while j > 0 and x < A[j] do for j  i + 1 to n do
A[j+1]  A[j] if A[j] < minx then
jj–1 minj  j
end while minx  A[j]
A[j+1]  x end if
end for end for
A[minj]  A[i]
A[i]  minx
end for

Supongamos que U y V son dos arreglos de n elementos, donde U ya está ordenado en


forma ascendente y V está en orden descendente. Ambos algoritmos toman más tiempo
con V que con U. De hecho, V es el peor caso posible para ambos algoritmos. Sin embargo,
el tiempo requerido por el algoritmo de ordenamiento por selección no es muy sensible al
ordenamiento inicial del arreglo: el control “if A[j] < minx” se ejecuta exactamente el
mismo número de veces en cualquier caso. La variación en el tiempo de ejecución se debe
solamente al número de veces que se ejecuta la asignación correspondiente a la parte then
de ese control. La práctica demostró que el tiempo requerido para ordenar un número dado
de elementos varió en un máximo de un 15% cualquiera fuese el orden inicial de los
elementos a ordenar. El tiempo requerido por el ordenamiento por selección es cuadrático,
independientemente del orden inicial de los elementos.

La situación es diferente si comparamos los tiempos utilizados por el algoritmo de


ordenamiento por inserción sobre los mismos dos arreglos. Debido a que la condición que
controla el ciclo while es siempre falsa al comienzo, el ordenamiento de U es muy rápido y
toma tiempo lineal. Por el contrario, el ordenamiento de V toma un tiempo cuadrático
porque el ciclo while se ejecuta i − 1 veces por cada valor de i. Por lo tanto, la variación de
tiempo entre estas dos instancias es considerable. Más aún, esta variación aumenta con el

F.C.A.D – U.N.E.R. 7
Algoritmos y Complejidad Unidad 1: Introducción

número de elementos a ordenar. En la práctica se detectó que toma menos de 4 décimas de


segundo ordenar un arreglo de 5000 elementos ya ordenados en forma ascendente,
mientras que le llevó tres minutos y medio (es decir, unas mil veces más) ordenar el mismo
arreglo si inicialmente estaba en orden descendente.

Problema: calcular el n–ésimo número de la serie de Fibonacci

Algoritmos: recursivo, iterativo (2)

Algoritmo Recursivo Segundo Algoritmo Iterativo


function Fib1(n) function Fib3(n)
if n < 2 then i1
return n j0
else k0
return (Fib1(n – 1) + Fib1(n – 2)) h1
end if while n > 0 do
if n es impar then
t  jh
Primer Algoritmo Iterativo j  ih + jk + t
function Fib2(n) i  ik + t
i1 end if
j0 t  h2
for k  1 to n do h  2kh + t
ji+j k  k2 + t
ij–i nn÷2
end for end while
return j return i

Si bien el primer algoritmo surge directamente de la definición de la serie de


Fibonacci, es muy ineficiente porque calcula muchas veces los mismos valores. Con el
primer algoritmo iterativo, el tiempo de ejecución es del orden de n si se asume la adición
como una operación elemental, porque evita la repetición innecesaria de cálculos onerosos.
El tercer algoritmo introduce una mejora igualmente significativa, al reducir el tiempo de
ejecución al orden del logaritmo de n. El siguiente es un cuadro comparativo de los
desempeños de los diferentes algoritmos dependiendo del valor de n.

n 10 20 30 50 100 10.000 106 108


Fib1 8 mseg 1 seg 2 min 21 días 109 años
Fib2 1/6 mseg 1/3 mseg 1/2 mseg 3/4 mseg 1 ½ mseg 150 mseg 15 seg 25 min
Fib3 1/3 mseg 2/5 mseg 1/2 mseg 1/2 mseg 1/2 mseg 1 mseg 1 ½ mseg 2 mseg

F.C.A.D – U.N.E.R. 8

También podría gustarte