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