Apuntes sobre Análisis de Algoritmos

Anuncio
Programación 3
Apuntes de Teórico
Análisis de Algoritmos
Instituto de Computación,
Facultad de Ingeniería,
Universidad de la República
28 de agosto de 2015
Índice
1. Introducción
1
2. Conceptos básicos
2.1. Contar las operaciones teniendo en cuenta el costo . . . . . . . . . . . . . . . . .
2.2. Contar las operaciones sin tener en cuenta el costo . . . . . . . . . . . . . . . . .
2.3. Complejidad de un algoritmo . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
2
2
3
3. Definiciones
3
4. Ejemplo: Find
5
5. Comportamiento asintótico
5.1. Noción informal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2. Notación asintótica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
6
7
iii
1.
Introducción
En esta sección se desarrollará el tema de Análisis de Algoritmos. Un algoritmo es un método
para resolver un problema que en particular puede utilizar una computadora para llegar a la
resolución del mismo. El punto de partida es en la realidad un problema determinado, el cual se
estudia y eventualmente se desarrollan posibles formas de solucionarlo.
Entonces, dado un problema particular (y una arquitectura subyacente donde resolverlo),
es necesario contar con un método preciso que lo resuelva (algoritmo). Es por tanto de interés
desarrollar formas de análisis y medidas que nos permitan realizar comparaciones entre distintos
algoritmos que resuelven un mismo problema y/o estimar qué tan bueno es un algoritmo particular. En particular interesa estudiar los aspectos relativos a la eficiencia de un algoritmo, es
decir, aquellos aspectos que determinan un uso eficiente de los recursos.
En general la medida de eficiencia de un algoritmo está basada en el tiempo que demora en
ejecutarse y la memoria ocupada o que utiliza. En este curso se hará énfasis en el Tiempo de
Ejecución de un algoritmo.
El Análisis de Algoritmos consiste en calcular de antemano cuánto tiempo puede llevar la
ejecución de un algoritmo que resuelva un problema dado y ya que no es práctico buscar
el tiempo exacto, será necesario definir una medida representativa del mismo pero que se obtenga del propio algoritmo. De esta manera se consigue desacoplar la eficiencia temporal de un
algoritmo de otros aspectos arquitectónicos y de contexto que requieren de un análisis cuya dependencia en estas características lo convierten en un enfoque difícilmente generalizable y poco
práctico. El objetivo será entonces conocer la cantidad de operaciones que un algoritmo realiza
a lo largo de su ejecución.
Como ejemplo informal ante el problema de la “búsqueda de un elemento en un arreglo ordenado”, pueden considerarse dos formas diferentes de implementar un algoritmo que resuelva
el problema:
Si se realiza una búsqueda lineal, eventualmente habrá que recorrer todo el vector para
encontrar el elemento, con lo cual el “tiempo” crecerá linealmente con el largo n del arreglo
y por lo tanto la cantidad de operaciones será proporcional a n.
Si se realiza una búsqueda binaria, en cada paso se busca en la “mitad” del arreglo donde
el elemento puede estar, entonces resulta que la cantidad de operaciones será proporcional
a log n.
Se puede concluir entonces que la segunda forma de resolver el problema es más eficiente.
2.
Conceptos básicos
El análisis de un algoritmo consiste en determinar la cantidad de operaciones que realiza
el algoritmo. Para ello se tendrán en cuenta sólo ciertas operaciones simples de los algoritmos,
p.e.: asignación, comparación, suma, resta, etc.
Es importante notar que cada operación de las mencionadas tiene un costo asociado para el
compilador que se utilice (como se mencionó en la introducción no es práctico tener en cuenta
la máquina particular en que se ejecute).
En general la cantidad de operaciones realizadas por un algoritmo depende de diversos factores, como por ejemplo
Tamaño de la entrada (medida de las dimensiones de la entrada, por ejemplo, la dimensión
de un vector a ordenar).
Instancia particular de los datos del problema (por ejemplo, el orden de los datos de un
vector de entrada).
El análisis en concreto consistirá en determinar el resultado de contar el total de operaciones
realizadas. Para esto habrá que valerse de herramientas matemáticas.
Según el contexto y la finalidad del análisis se necesitarán distintos niveles de precisión, por
lo tanto se verán tres posibles puntos de vista para el análisis:
1
1. Contar las operaciones simples con sus costos.
2. Contar las operaciones simples sin tener en cuenta costos.
3. Contar sólo una operación predefinida y/o básica.
A continuación se verán más en detalle estas alternativas utilizando el siguiente ejemplo:
1
2
3
4
5
6
7
8
9
10
11
int minimo (int* A, int n) {
int min;
min = A[0];
int i;
for (i=1; i<n; i=i+1) {
if (A[i] < min) {
min = A[i];
}
}
return min;
}
Ejemplo 1: Encontrar el mínimo de un arreglo a de tamaño n
Se contabilizarán para el cálculo del costo las siguientes operaciones: asignación, comparación
de elementos, comparación de variable de control, incremento de la variable de control.
2.1.
Contar las operaciones teniendo en cuenta el costo
Este enfoque consiste en asignar un costo a cada operación para llegar a una expresión
matemática que nos permita determinar el costo del algoritmo.
Para una mejor comprensión, se analizará el Ejemplo 1. Sean las siguientes definiciones:
C1 = costo de asignación
C2 = costo de comparación
C3 = incremento variable de control
Se tienen 2 asignaciones inicialmente, en las línea 3 y en la 5 asignando a la variable i el
valor 1. El for hace que en cada operación haya una comparación (n veces) y el incremento de
una variable de control (n − 1 veces). En cada iteración se realiza a su vez una comparación en
la línea 6 y en caso de que se cumpla la condición se realiza una asignación (línea 7).
T = 2C1 + nC2 + (n − 1)C3 + (n − 1)C2 + x(n − 1)C1
= 2C1 + nC2 + (n − 1)(xC1 + C2 + C3 )
Donde x ∈ [0, 1] es la cantidad de veces que se ejecuta la asignación para el mínimo hasta
el momento, divido entre la cantidad de veces que se realiza la comparación. Se distinguen dos
casos extremos:
Si x = 0, entonces el mínimo está en el primer lugar. Este es el mejor caso ya que se
minimiza el costo del Tiempo de Ejecución.
Si x = 1, entonces la secuencia está ordenada de forma decreciente. Este es el peor caso.
2.2.
Contar las operaciones sin tener en cuenta el costo
Este enfoque implica despreciar la diferencia en el costo entre las operaciones. Centrándonos
en el Ejemplo 1 se tiene que es un caso es similar anterior respecto a la detección de operaciones
a considerar. En cuanto al costo es un caso particular del mismo, tomando C1 = C2 = C3 = 1.
Se llega a la siguiente expresión para T:
T = 2 + n + 2(n − 1) + x(n − 1)
= (x + 3)n − x
2
2.3.
Complejidad de un algoritmo
La decisión de qué contar y considerar durante el análisis de un algoritmo es muy importante.
En general para decidir cuáles son las operaciones más relevantes en un algoritmo dado, primero
se deben identificar aquellas que constituyan una parte fundamental e integral en la forma del
algoritmo y determinar cuáles no (por ejemplo, comparaciones sobre los índices de control en un
loop no son relevantes en general). Un caso de interés podría ser la estimación del costo en etapas
tempranas del estudio del método particular, por ejemplo, un pseudocódigo, donde claramente
no existen operaciones de control como las mencionadas anteriormente, pero sí se encuentran
explícitas de alguna manera en las operaciones que caracterizan dicho método.
En particular buscamos entonces contar una operación básica, la cual termina determinando la forma del algoritmo. Dos clases de operaciones son consideradas en general como operaciones básicas: operaciones de comparación y de aritmética/asignación. A modo de ejemplo,
en algoritmos de ordenamiento o búsqueda se considera como operación básica a la comparación
entre los elementos a ordenar o buscar.
Retomando el Ejemplo 1, se podría describir informalmente (pseudocódigo) como:
1. Suponer que el mínimo temporal es el primer elemento.
2. Recorrer el arreglo comparando mínimo temporal con los sucesivos elementos.
2.1. Si el elemento es menor, tomarlo como nuevo mínimo temporal.
Notar que el pseudocódigo expuesto no tiene en forma explícita estructuras o variables de
control algunas, pero si se basa en la comparación entre el mínimo temporal y los sucesivos
elementos.
Considerando lo anterior y volviendo al código del Ejemplo 1, la operación básica es la
siguiente:
A[i] < min
Recordando que no se consideran otro tipo de comparaciones como variables de control, al
contar solamente la operación básica la función T queda de la siguiente forma:
T (n) = n − 1
Notar que se hacen n − 1 comparaciones en el peor y mejor caso, es decir, no existe distinción
entre los mismos. Con las otras formas de conteo se consideraban todas las operaciones y la que
marcaba la diferencia entre mejor y peor caso era la asignación dentro del “if”, que ahora no es
tenida en cuenta.
3.
Definiciones
Definición (Algoritmo). Dado un conjunto de operaciones, se tiene un algoritmo si se cumplen
las siguientes propiedades:
Secuencia finita de pasos.
Cada paso correctamente definido.
Cada paso debe ejecutarse en un tiempo finito.
Termina en algún momento.
Devuelve el resultado esperado.
Tiene un dominio de definición.
3
Definición (Dominio de definición). Un dominio de definición D, es el conjunto de todas
las entradas posibles.
En el Ejemplo 1 serían todas las secuencias (arreglos) que pueden formarse con los elementos
que según la declaración contiene el arreglo. En este caso son todos los números enteros, teniendo
por lo tanto hay infinitas entradas posibles.
Por otro lado el dominio de definición es el conjunto de entradas para la cual fue diseñado
el algoritmo y en ese sentido asegura la correctitud del mismo. El mismo algoritmo con otro
dominio de definición, puede no ser correcto. Por ejemplo, si está diseñado para trabajar sobre
números naturales no tiene por qué funcionar si se pretende usar sobre números reales.
En general el dominio de definición se expresa en función del tamaño de la entrada:
Dn = {e ∈ D/tamaño(e) = n}
1
Se denotará T (e) al costo (individual) de la entrada e (según la forma de estudio 1, 2 ó 3).
Definición (Análisis). Se busca determinar, analizando un algoritmo dado, el costo del mismo
expresándolo como una función T que depende del tamaño n de la entrada: T (n)
Definición (Análisis en el Peor caso, Mejor caso y Caso promedio). Se observó anteriormente
que en muchos casos el comportamiento de un algoritmo cambia de forma significativa según
la instancia concreta (datos de entrada) del problema a resolver. No es viable estudiar un caso
genérico. Se tratará de obtener la función T (n) en:
Peor caso: se obtendrá el mayor costo posible. Se define formalmente como:
TW (n) = máx {T (e)/e ∈ Dn }
El conjunto de entradas que conforman el peor caso es entonces:
{e ∈ Dn /T (e) = TW (n)}
Mejor caso: se tendrá el menor costo posible:
TB (n) = mı́n {T (e)/e ∈ Dn }
La entradas que conforman el mejor caso son:
{e ∈ Dn /T (e) = TB (n)}
Caso promedio: costo en un caso promedio, basado en ciertas hipótesis y probabilidades. TA (n)
es el promedio de todas las T(e), e ∈ Dn .
X
TA (n) =
T (e)P (e)
e∈Dn
Donde P (e) es la probabilidad que se dé la entrada e, que depende del contexto del problema
en que se encuentre el algoritmo. El caso medio es:
{e ∈ Dn /T (e) = TA (n)}
Observar que “W” es por “worst”, “B” es por “best” y “A” es por “average”.
Como punto importante se debe destacar que el estudio por casos corresponde a un tamaño
de entrada n dado (genérico). Un error frecuente es considerar que, por ejemplo, el mejor caso
se da con tamaños de entrada con n = 1.
1 Notar que esto es un abuso de notación, ya que se venía tratando a T como una función que va de los naturales
a los reales no negativos. Es decir, que hace corresponder tamaños de entradas con costos. En este caso se usa de
una forma tal que hace corresponder entradas en sí mismas con costos.
4
4.
Ejemplo: Find
El problema se trata sobre encontrar un elemento x dado, en una secuencia A dada. Si el
elemento está, devuelve la posición. Sino devuelve el valor -1. Se considera que todos los elementos
de la secuencia son distintos.
1
2
3
4
5
6
7
8
9
10
11
int find (int* A, int n, int x) {
int i = 0;
while ((i < n) && (A[i] != x)) {
i++;
}
if (i < n) {
return i;
} else {
return -1;
}
}
Ejemplo 2: Find
A continuación se hace el análisis considerando solamente la operación básica.
Mejor caso: secuencia donde x está en el primerer lugar: TB (n) = 1
Peor caso: hay dos configuraciones en las que se da el peor caso:
X no está en A
X es el último elemento de A
TW (n) = n
Caso medio: suponemos que si x ∈ A, todas las posiciones son equiprobables.
Partimos de la expresión anteriormente mencionada:
X
TA (n) =
T (e)P (e)
e∈Dn
Debemos simplificarla para poder entenderla y para que resulte práctica. Recordemos que
Dn es el conjunto de todas las secuencias de largo n. En la práctica cabe notar que el
algoritmo dado se comporta (ejecuta) de la misma forma para todas las secuencias que
tengan a x en un lugar específico i: no interesa el resto de los elementos que compongan la
secuencia, si x está en el lugar i-ésimo se realizan i comparaciones y el algoritmo termina.
Sucede algo similar si el elemento no está en la secuencia.
Caso 1: x ∈ A
Observando lo anterior, podemos subdividir Dn en subconjuntos Dni , donde Dni está
formado por las secuencias que tienen a x en la posición i-ésima, 1 ≤ i ≤ n. Teniendo
en cuenta esto, se puede simplificar el problema considerando que solamente existen
n entradas, ei con 1 ≤ i ≤ n, diferenciadas en la posición en donde se encuentra x.
Entonces se tiene que P (ei ) = n1 , debido a la hipótesis de equiprobabilidad. También
que T (ei ) = i. Por lo tanto:
TA (n) =
n
X
i=1
T (ei )P (ei ) =
n
n
X
1
1X
1 n(n + 1)
n+1
i=
i=
=
n
n
n
2
2
i=1
i=1
5
Caso 2: x puede estar o no en A. Se le asigna una probabilidad a este suceso: P (x ∈ A) = q
Tomamos como entradas válidas las del caso anterior, pero se agrega en+1 que representa a las instancias que no tienen a x. Se tiene entonces que:
q
,1 ≤ i ≤ n
n
P (en+1 ) = 1 − q
P (ei ) =
TA (ei ) = i, 1 ≤ i ≤ n
n
n
X
qX
q n(n + 1)
q
i = (1 − q)n +
TA (n) =
T (ei )P (ei ) = n(1 − q) +
i = (1 − q)n +
n
n
n
2
i=1
i=1
i=1
q
q
= (1 − )n +
2
2
n+1
X
Notar que con q = 1 estamos en el caso 1. Si q = 0, tenemos que x 6∈ A y el costo es
n.
5.
Comportamiento asintótico
En muchos casos resultará de interés investigar el costo de un algoritmo para entradas de
tamaño n arbitrariamente grande. En otras palabras y de forma más precisa, interesa conocer
el costo cuando n tiende a infinito. Esto implica que hay términos del costo que se pueden
despreciar ya que su aporte es ínfimo. En definitiva interesa determinar la tasa de crecimiento
de T (n). Por ejemplo podría comportarse como: log n, n3 , 2n o n!
Se plantea un ejemplo en donde se comparan tasas de crecimiento. Sean A1 y A2 dos algoritmos con costos TA1 (n) = 10−6 2n y TA2 (n) = 10−6 n3 . Supongamos que los costos están medidos
en segundos. Se muestra tiempos de ejecución para algunos valores de n en la Tabla 1.
n
5
10
20
30
40
45
100
TA1 (n)
3,2 × 10−5 s
10−3 s
1s
18 min
13 días
4,1 años
4 × 1017 años
TA2 (n)
1,25 × 10−4 s
10−3 s
8 × 10−3 s
27 × 10−3 s
64 × 10−3 s
0,09 s
1s
Tabla 1: Ejemplos de algunos valores para los tiempos de costos del ejemplo
¿Qué pasa si contamos con una computadora un millón de veces más rápida? En la Tabla 2
vemos que la ineficiencia sigue de todas maneras con valores reducidos de n.
n
45
65
TA1 (n)
35 s
1,2 años
Tabla 2: Valores obtenidos con una computadora mucho más rápida
La tasa de crecimiento exponencial implica una notable mayor sensibilidad al tamaño de la
entrada y menor sensibilidad a la velocidad de procesamiento.
5.1.
Noción informal
Dado un algoritmo con tiempo de ejecución T (n) y una función f (n), se dice que T (n) es del
orden de f (n) si T (n) es acotada por un múltiplo real positivo de f (n). O sea, T (n) ≤ kf (n)
donde k ∈ R+ .
6
Por ejemplo, si se tiene un algoritmo con T (n) = 20n2 + 15n + 6, se cumple dicha propiedad
con f (n) = n2 :
T (n) = 20n2 + 15n + 6 ≤ 20n2 + 15n + 6
⇒ T (n) ≤ 20n2 + 15n2 + 6, ∀n ≥ 1
⇒ T (n) ≤ 20n2 + 15n2 + 6n2 , ∀n ≥ 1
⇒ T (n) ≤ 41n2 , ∀n ≥ 1
Observar que: funciones como n3 , 3n2 , 27n2 y n4 también cumplen lo antedicho por lo tanto
se tiene un conjunto de funciones que “acotan” a T (n).
Funciones g(n) como 3n, 27n, 5n y 4 son todas del orden de f (n) = n porque siempre se
puede encontrar un k ∈ R de forma que se cumpla g(n) ≤ kf (n). Por lo antedicho las funciones
que cumplan esta desigualdad forman un conjunto de funciones, cada una de las cuales es del
orden de f (n).
5.2.
Notación asintótica
Recordar primero que N es el conjunto de los números naturales, R el de los reales, R+ el de
los reales positivos y R∗ el de los reales positivos y el cero (reales no negativos).
Definición (Orden: cota superior). Dada una funcion f : N → R∗ , se define O(f (n)) como:
O(f (n)) = {g : N → R∗ /∃c ∈ R+ , ∃n0 ∈ N, ∀n ≥ n0 , g(n) ≤ cf (n)}
Se dice que una función T : N → R∗ es del orden de una función f si se cumple que
T (n) ∈ O(f (n)) (abreviado frecuentemente como T (n) ∈ O(f )). Es decir, si T cumple ser alguna
de las funciones g del conjunto definido.
Definición (Omega: cota inferior). Dada una funcion f : N → R∗ , se define Ω(n) como:
Ω(f (n)) = {g : N → R∗ /∃c ∈ R+ , ∃n0 ∈ N, ∀n ≥ n0 , g(n) ≥ cf (n)}
Definición (Orden exacto). Dada una funcion f : N → R∗ , se define Θ(f (n)) como:
Θ(f (n)) = O(f (n)) ∩ Ω(f (n))
Hay algunas observaciones importantes:
Dadas dos funciones f y g tal que f ∈ O(g), es común ver el abuso de notación f = O(g).
Sin embargo, en este curso no usaremos esta forma de escribirlo.
Dada T (n), si TW (n) es el costo en el peor caso y TW (n) ∈ O(f (n)) para alguna f (n)
entonces T (n) ∈ O(f (n)), ya que T (n) ≤ TW (n) por ser el peor caso. No puede afirmarse
algo similar para T (n) si Tw (n) ∈ Ω(f (n)).
Similarmente TB (n) ∈ Ω(f (n)) ⇒ T (n) ∈ Ω(f (n)).
A continuación se muestran algunas propiedades.
Propiedad 1. f ∈ O(g), g ∈ O(h) ⇒ f ∈ O(h)
Demostración.
f ∈ O(g) ⇒ ∃c1 ∈ R+ , ∃n1 ∈ N, ∀n ≥ n1 , f (n) ≤ c1 g(n)
g ∈ O(h) ⇒ ∃c2 ∈ R+ , ∃n2 ∈ N, ∀n ≥ n2 , g(n) ≤ c2 h(n)
Se debe demostrar que:
∃c ∈ R+ , ∃n0 ∈ N, ∀n0 ≥ n, f (n) ≤ c h(n)
7
Se puede deducir lo siguiente:
f (n) ≤ c1 g(n) ≤ c1 c2 h(n)
Por lo tanto se cumple esta propiedad tomando c = c1 c2 y n0 = máx (n1 , n2 ).
Propiedad 2. f ∈ O(g) ⇔ g ∈ Ω(f )
Propiedad 3. f ∈ Θ(g) ⇔ g ∈ Θ(f )
Demostración. Se demuestra la doble inclusión para un sólo lado ya que para el otro es básicamente lo mismo.
f ∈ Θ(g) entonces f ∈ O(g) y f ∈ Ω(g), por definición de Θ. A su vez por Propiedad 2:
f ∈ O(g) ⇒ g ∈ Ω(f )
f ∈ Ω(g) ⇒ g ∈ O(f )
Entonces por definición de T heta, g ∈ Θ(n).
Propiedad 4. Θ define una relación de equivalencia.
Propiedad 5. f + g ∈ O(máx (f, g))
Para la Propiedad 5, se define lo siguiente:
Definición. Se cumple f > g si y sólo si ∃n0 ∈ N tal que ∀n ∈ N que cumple n > n0 entonces
f (n) > g(n).
Algunas observaciones:
Cuando utilizamos la notación anterior notamos que además de despreciar los términos
de menor orden también se están despreciando las constantes multiplicativas, las cuales
pueden determinar cuál de dos algoritmos de un mismo orden es mejor que otro.
Es importante recalcar que para algoritmos que se comportan de manera más eficiente
para entradas arbitrariamente grandes puede ocurrir que no se comporten tan bien para
entradas más chicas.
Cabe destacar que hay algoritmos para los cuales no tiene sentido hablar una entrada arbitrariamente grande. Por otro lado, hay varias clases de algoritmos para los cuales sí tiene
sentido, como pueden ser los algoritmos de ordenación, de generación de permutaciones,
etc.
8
Descargar