2_Análisis de Algoritmos

29
Cap´ ıtulo 2 An´ alisis de algoritmos El an´ alisis de algoritmos es el campo de la algor´ ıtmica que estudia la eficiencia de los algoritmos: el tiempo necesario para su ejecuci ´ on y la cantidad de memoria que consumen. El estudio de la eficiencia nos interesa: para estimar los recursos computacionales (tiempo de proceso y espacio de memoria) que consume un algoritmo en funci ´ on de la talla de las instancias del problema que resuelve, y para comparar dos algoritmos que resuelven un mismo problema de modo que podamos seleccionar el m´ as eficiente. Centraremos el discurso en el consumo de tiempo y s ´ olo al final nos ocuparemos del consumo de espa- cio. Hablaremos de complejidad o coste temporal y espacial para referirnos al consumo de estos recursos. ¿Es tan importante estu- diar la eficiencia cuando los computadores son ca- da vez m´ as r´ apidos y la memoria es cada vez m´ as barata? La ✭✭ley de Moore✮✮ conjetura que la poten- cia de los ordenadores se duplica cada 18 meses. Cabe pensar que el algorit- mo que pueda resultar hoy lento ser´ a alg´ un d´ ıa sufi- cientemente r´ apido. Pero la ✭✭ley de Moore✮✮ tiene una vigencia esperada de unos 20 a˜ nos: los l´ ımites del mundo f´ ısico impiden duplicar la velocidad ilimi- tadamente. Y en cualquier caso, hay problemas para los que los algoritmos m´ as eficientes tienen un coste computacional que crece tanto con la talla del prob- lema que por mucho que evolucionen los computa- dores seguir´ an siendo ine- ficientes para instancias de talla moderada. Una primera idea consiste en estudiar el tiempo real necesario para resolver instancias concretas del problema con una implementaci´ on particular del algoritmo ejecut´ andose en un sistema computador con- creto. Surge entonces una primera cuesti´ on: ¿con qu´ e lenguaje de programaci´ on hemos de implementar el algoritmo? La respuesta es inmediata si el objetivo es la comparaci´ on de dos o m´ as algoritmos distintos: el lenguaje de programaci´ on resulta indiferente si los dos algoritmos se implementan con el mismo. Pero no basta con usar el mismo lenguaje para garantizar una comparaci´ on justa: hemos de ser cuidadosos y realizar todas las implementaciones con el mismo grado de competencia y usando habilidades t´ ecnicas sim- ilares. No ser´ ıa justo implementar uno de los programas de forma directa y burda cuando otro se desarrolla prestando atenci´ on a todo tipo de detalles. En cualquier caso, los algoritmos son independientes de los lenguajes de programaci ´ on y de los sistemas computadores, as´ ı que el an´ alisis del tiempo y la memoria necesarios para la ejecuci ´ on deber´ ıan ser ajenos a estos elementos. ¿C´ omo podemos obtener an´ alisis de tiempos de ejecuci ´ on y consumo de memoria sin tener en cuenta ✭✭detalles✮✮ como el computador o el lenguaje de programaci´ on usados? Un enfoque del an´ alisis de algoritmos se centra en el estudio de la evoluci´ on asint´ otica del n´ umero de operaciones elementales y del n´ umero de celdas de memoria en funci´ on de la talla del problema. Ciertas t´ ecnicas que estudiaremos permiten establecer algunos resultados sobre el comportamiento de un algoritmo a partir de estimaciones muy groseras sobre el n´ umero de operaciones que han de ejecutarse o el n´ umero de celdas de memoria consumidas. Estudiaremos, fundamentalmente, cotas al coste en el mejor y el peor de los casos. Tambi´ en consideraremos otras caracter´ ısticas del coste temporal y espacial de los algoritmos, como el coste promedio o el coste amortizado. 2.1. Medici´ on de tiempos de ejecuci´ on Vamos a plantearnos, como primer objetivo, aprender a medir tiempos de ejecuci´ on de programas. Con- sideremos un problema concreto: la b´ usqueda de un valor en un vector ordenado de enteros. Se trata de una simplificaci´ on del problema de la b´ usqueda de un nombre en la gu´ ıa telef´ onica. Nuestro problema se enuncia as´ ı: ✭✭dado un vector con n umeros enteros positivos, diferentes y ordenados de menor a mayor, encu´ entrese el ´ ındice de un elemento de valor x y, si no est´ a, ind´ ıquese✮✮. Estudiaremos tres algoritmos diferentes: 1. Una t´ ecnica de b´ usqueda secuencial que resulta evidentemente ineficiente: recorrer el vector com- pletamente y, al encontrar el valor x, memorizar su ´ ındice para devolverlo al final del recorrido del vector. Denominaremos a esta t´ ecnica ✭✭usqueda secuencia na´ ıf✮✮. usqueda secuencial na´ ıf: Naive sequential search. Apuntes de Algor´ ıtmica 2-1

Transcript of 2_Análisis de Algoritmos

  • Captulo 2

    Analisis de algoritmos

    El analisis de algoritmos es el campo de la algortmica que estudia la eficiencia de los algoritmos: el tiemponecesario para su ejecucion y la cantidad de memoria que consumen. El estudio de la eficiencia nos interesa:

    para estimar los recursos computacionales (tiempo de proceso y espacio de memoria) que consumeun algoritmo en funcion de la talla de las instancias del problema que resuelve,

    y para comparar dos algoritmos que resuelven un mismo problema de modo que podamos seleccionarel mas eficiente.

    Centraremos el discurso en el consumo de tiempo y solo al final nos ocuparemos del consumo de espa-cio. Hablaremos de complejidad o coste temporal y espacial para referirnos al consumo de estos recursos. Es tan importante estu-

    diar la eficiencia cuandolos computadores son ca-da vez mas rapidos y lamemoria es cada vez masbarata? La ((ley de Moore))conjetura que la poten-cia de los ordenadores seduplica cada 18 meses.Cabe pensar que el algorit-mo que pueda resultar hoylento sera algun da sufi-cientemente rapido. Perola ((ley de Moore)) tieneuna vigencia esperada deunos 20 anos: los lmitesdel mundo fsico impidenduplicar la velocidad ilimi-tadamente. Y en cualquiercaso, hay problemas paralos que los algoritmos maseficientes tienen un costecomputacional que crecetanto con la talla del prob-lema que por mucho queevolucionen los computa-dores seguiran siendo ine-ficientes para instancias detalla moderada.

    Una primera idea consiste en estudiar el tiempo real necesario para resolver instancias concretas delproblema con una implementacion particular del algoritmo ejecutandose en un sistema computador con-creto. Surge entonces una primera cuestion: con que lenguaje de programacion hemos de implementar elalgoritmo? La respuesta es inmediata si el objetivo es la comparacion de dos o mas algoritmos distintos:el lenguaje de programacion resulta indiferente si los dos algoritmos se implementan con el mismo. Perono basta con usar el mismo lenguaje para garantizar una comparacion justa: hemos de ser cuidadosos yrealizar todas las implementaciones con el mismo grado de competencia y usando habilidades tecnicas sim-ilares. No sera justo implementar uno de los programas de forma directa y burda cuando otro se desarrollaprestando atencion a todo tipo de detalles.

    En cualquier caso, los algoritmos son independientes de los lenguajes de programacion y de los sistemascomputadores, as que el analisis del tiempo y la memoria necesarios para la ejecucion deberan ser ajenos aestos elementos. Como podemos obtener analisis de tiempos de ejecucion y consumo de memoria sin teneren cuenta ((detalles)) como el computador o el lenguaje de programacion usados? Un enfoque del analisisde algoritmos se centra en el estudio de la evolucion asintotica del numero de operaciones elementales ydel numero de celdas de memoria en funcion de la talla del problema. Ciertas tecnicas que estudiaremospermiten establecer algunos resultados sobre el comportamiento de un algoritmo a partir de estimacionesmuy groseras sobre el numero de operaciones que han de ejecutarse o el numero de celdas de memoriaconsumidas. Estudiaremos, fundamentalmente, cotas al coste en el mejor y el peor de los casos. Tambienconsideraremos otras caractersticas del coste temporal y espacial de los algoritmos, como el coste promedioo el coste amortizado.

    2.1. Medicion de tiempos de ejecucion

    Vamos a plantearnos, como primer objetivo, aprender a medir tiempos de ejecucion de programas. Con-sideremos un problema concreto: la busqueda de un valor en un vector ordenado de enteros. Se trata deuna simplificacion del problema de la busqueda de un nombre en la gua telefonica. Nuestro problema seenuncia as: ((dado un vector con n numeros enteros positivos, diferentes y ordenados de menor a mayor,encuentrese el ndice de un elemento de valor x y, si no esta, indquese)). Estudiaremos tres algoritmosdiferentes:

    1. Una tecnica de busqueda secuencial que resulta evidentemente ineficiente: recorrer el vector com-pletamente y, al encontrar el valor x, memorizar su ndice para devolverlo al final del recorrido delvector. Denominaremos a esta tecnica ((busqueda secuencia naf)). Busqueda secuencial

    naf: Naive sequentialsearch.

    Apuntes de Algortmica 2-1

  • 2.1 Medicion de tiempos de ejecucion 2004/09/24-15:52

    2. Un refinamiento de la tecnica anterior: recorrer el vector hasta encontrar el valor x, momento en elque se devuelve su ndice, o hasta llegar a un valor que permita concluir que el valor buscado noesta presente, momento en el que detenemos la busqueda y avisamos de este hecho. Esta tecnica sedenominara ((busqueda secuencial)), sin mas.Busqueda secuencial:

    Sequential search.3. Y una tecnica iterativa muy diferente que considera en cada instante la busqueda en un ((subvector))

    del vector original (y que inicialmente es el propio vector original). Con cada iteracion se compara xcon el elemento central de un ((subvector)) y

    si coinciden, finalizar la busqueda devolviendo el ndice del elemento;

    si x es menor, aplicar el mismo procedimiento al subvector izquierdo;

    y si x es mayor, al derecho.

    Si en algun momento se pretende efectuar la busqueda en un subvector vaco, el metodo finalizaconcluyendo que el elemento buscado no se encuentra en el vector. Llamaremos a este metodo((busqueda binaria)).Busqueda binaria:

    Binary search.Implementemos los tres algoritmos con el lenguaje de programacion Python. El vector sera una lista y

    el valor especial con el que indicamos que no se encontro el valor buscado sera None:Evidentemente, el primermetodo es poco razon-able: ((deberamos)) abor-tar la busqueda tan pron-to encontramos el elemen-to buscado en el vec-tor o estamos seguros deque no esta en el vec-tor, como hace el se-gundo metodo. No ob-stante, se trata de un er-ror relativamente frecuenteen programadores primeri-zos, as que tiene cierto in-teres estudiar que repercu-siones tiene su comision.

    Python permite expresarla busqueda secuencial deforma mas rapida medi-ante el uso del metodoa.index(x), pero no serajusto usar este metodo enuna comparacion, pues suejecucion se realiza di-rectamente en codigo demaquina (generado a partirde una rutina escrita en C)y, por tanto, es significati-vamente mas rapida.

    search.py

    1 def naive sequential search(a, x):2 index = None3 for i in xrange(len(a)):4 if x == a[i]:5 index = i6 return index7

    8 def sequential search(a, x):9 if x < a[0]:

    10 return None11 for i in xrange(len(a)):12 if x == a[i]:13 return i14 elif x < a[i]:15 return None16 return None17

    18 def binary search(a, x):19 left, right = 0, len(a)20 while left < right:21 i = (right + left) / 222 if x == a[i]:23 return i24 elif x < a[i]:25 right = i26 else:27 left = i + 128 return None

    2.1.1. La funcion clock

    Ocupemonos ya de la cuestion tecnica de la medicion del tiempo de ejecucion. El modulo time de lacoleccion de bibliotecas estandar de Python ofrece una funcion para la medicion de tiempos. Su nombre esSegun la documentacion,

    la funcion clock de la bib-lioteca estandar de Pythonno funciona correctamenteen Microsoft Windows: nomide el tiempo de CPUtranscurrido, sino el tiem-po real transcurrido. Debetenerse en cuenta que am-bos no se corresponden enun sistema multitarea.

    clock y devuelve el numero de segundos (un valor en coma flotante) que ha dedicado la CPU a la ejecuciondel programa hasta el punto en que se efectua la llamada a la funcion. Si efectuamos dos medidas de tiempo,una antes de efectuar el calculo y otra inmediatamente despues, el tiempo transcurrido sera la diferenciaentre las dos medidas.

    2-2 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    1 from time import clock2

    3 t1 = clock()4 acciones5 t2 = clock()6 t = t2 - t1

    Las funciones de busqueda disenadas necesitan de vectores y valores de x concretos para su ejecucion.Asumamos, por el momento, un vector de tamano fijo (pongamos que n = 10) cuyos elementos son enterosno repetidos en un rango determinado (por ejemplo, en el rango [0..5n 1]) y que buscamos un elementocualquiera de los presentes en el vector:

    timing1.py

    1 from search import naive sequential search2 from time import clock3 from random import seed, randrange, sample4

    5 # Generacion de un vector aleatorio.6 seed(0) # Semilla del generador de numeros aleatorios.7 n = 10 # Talla del vector.8 a = sorted(sample(xrange(5*n), n)) # Vector ordenado de n valores aleatorios en [0..5n1] sin repeticion.9 x = a[ randrange(n) ] # Seleccion de un elemento al azar.

    10

    11 # Ejecucion con medicion de tiempo.12 t1 = clock()13 index = naive sequential search(a, x)14 t2 = clock()15 t = t2 - t116

    17 print Tiempotranscurrido: %.8fsegundos % t

    Este es el resultado de su ejecucion con el interprete de Python 2.4a en un ordenador con procesador El modulo randomofrece funciones para lageneracion de numerosaleatorios. La semilla seinicializa con seed. Lafuncion sample seleccionauna muestra aleatoriasin reemplazamiento deuna poblacion descritamediante una secuenciao iterable; por ejemplo,sample([1,2,3], 2)devuelve, al azar, unade estas listas: [1, 2],[2, 1], [2, 3], [3, 2],[1, 3] o [3, 1]. Lallamada randrange(m)genera un numero enteroaleatorio en el rango[0..m 1] con una dis-tribucion uniforme. Lafuncion sorted devuelveuna lista ordenada demenor a mayor con losmismos valores quecontiene la secuenciaque se proporciona comoargumento.

    Intel Pentium 4 a 2.6 GHz, 512 Mb de memoria RAM y sistema operativo Linux con nucleo 2.6:

    Tiempo transcurrido: 0.00000000 segundos

    Como puede tardar cero segundos la ejecucion de la funcion? Hay un problema con la funcion clockque no hemos comentado: la resolucion del reloj esta en el entorno de las centesimas de segundo, as queno podemos medir eventos que transcurran en menos de una centesima.

    2.1.2. Ejecucion repetida un numero fijo de veces

    Podemos superar este problema si repetimos la ejecucion del programa un numero suficiente de veces:timing2.py

    1 from search import naive sequential search2 from time import clock3 from random import seed, randrange, sample4

    5 # Generacion de un vector aleatorio.6 seed(0)7 n = 108 a = sorted(sample(xrange(5*n), n))9 x = a[ randrange(n) ]

    10

    11 # Ejecucion repetida con medicion de tiempo.12 r = 10000013 t1 = clock()14 for i in xrange(r):15 index = naive sequential search(a, x)

    Apuntes de Algortmica 2-3

  • 2.1 Medicion de tiempos de ejecucion 2004/09/24-15:52

    16 t2 = clock()17 for i in xrange(r):18 pass19 t3 = clock()20

    21 t = ((t2 - t1) - (t3 - t2)) / r22 print Tiempomedioporejecucion: %.8fsegundos % t

    Notese que el calculo del tiempo transcurrido se ha complicado un poco. El objetivo del bucle entre losinstantes t2 y t3 es poder descontar el tiempo que supone la ejecucion del bucle que repite el calculo. Heaqu el resultado:

    Tiempo medio por ejecucion: 0.00000430 segundos

    Como se puede ver, hemos entrado en la escala de lo que el reloj puede medir. Como determinamosel numero de repeticiones necesarias para entrar en la escala de lo que el reloj puede medir en funcion deltamano del vector? Si hacemos pruebas con vectores de menor tamano, el numero de repeticiones debera sermayor. Podramos determinar este numero para cada tamano de vector por prueba y error, pero resultaraextremadamente pesado.

    2.1.3. Ejecucion repetida hasta superar una cantidad de tiempo

    Una tecnica que permite solucionar este problema consiste en repetir la ejecucion de la rutina durante almenos una cantidad de tiempo preestablecida. Si la rutina efectua el calculo muy rapidamente, tendra querepetirse un gran numero de veces para conseguir que transcurra el tiempo necesario. Si, por contra, larutina se ejecuta en un lapso de tiempo grande, una sola ejecucion sera suficiente para tener una medicionrazonablemente precisa. Este programa expresa esta idea en Python:

    timing3.py

    1 from search import naive sequential search2 from time import clock3 from random import seed, randrange, sample4

    5 tmin = 2 # Tiempo mnimo de ejecucion: dos segundos.6

    7 seed(0)8 n = 109 a = sorted(sample(xrange(5*n), n))

    10 x = a[ randrange(n) ]11

    12 t1 = t2 = clock()13 r = 014 while t2 - t1 < tmin:15 index = naive sequential search(a, x)16 t2 = clock()17 r += 118

    19 t3 = t4 = clock()20 aux = 021 while aux < r:22 t4 - t323 t4 = clock()24 aux += 125

    26 t = ((t2 - t1) - (t4 - t3)) / r27 print Tiempomedioporejecucion: %.8fsegundos % t

    Tiempo medio por ejecucion: 0.00000432 segundos

    2-4 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    Las lneas 1924 tienen por objeto estimar el tiempo que requiere la ejecucion del codigo extra quehemos anadido para aplicar la tecnica de medicion, pues contienen el mismo numero de operaciones (unbucle que se itera el mismo numero de veces y en el que cada iteracion supone efectuar una comparacion,una resta, una llamada a clock, una asignacion y un incremento.

    Pasemos a ocuparnos del estudio de la evolucion del tiempo de ejecucion con la talla de las instancias delproblema. Pero, antes, detengamonos a considerar que entendemos exactamente por talla de una instancia.

    2.2. Talla de una instancia

    Podemos definir informalmente la talla de una instancia del problema como la cantidad de memoria nece-saria para describirla. Dicha especificacion pasa por la definicion de una codificacion, es decir, una cor-respondencia entre los datos y cadenas de smbolos de un determinado alfabeto. La ocupacion espacialguarda relacion con el numero de dichos smbolos.

    Un numero entero y positivo n puede codificarse, por ejemplo, con un alfabeto unario, como {1},formando una cadena con n repeticiones del mismo smbolo. En el alfabeto binario {0,1}, el mismo numeropodra codificarse en binario natural. El numero de smbolos necesarios para codificar n con la primeracodificacion es s1(n) = n. La segunda codificacion solo requiere s2(n) = 1+blgnc smbolos. La diferenciaen la cantidad de memoria necesaria si usamos una u otra codificacion es enorme, como se puede ver alcomparar la columnas 2 y 3 en la tabla 2.1.

    n s1(n) s2(n)

    0 0 11 1 12 2 23 3 24 4 35 5 36 6 37 7 38 8 49 9 4

    10 10 411 11 4

    .

    .

    .

    .

    .

    .

    .

    .

    .

    1 023 1 023 10.

    .

    .

    .

    .

    .

    .

    .

    .

    1 048 575 1 048 575 20.

    .

    .

    .

    .

    .

    .

    .

    .

    1 099 511 627 775 1 099 511 627 775 40.

    .

    .

    .

    .

    .

    .

    .

    .

    Tabla 2.1: Smbolos necesarios para expresar unentero positivo n usando una codificacion unaria(s1(n)) y una codificacion en binario natural (s2(n)).

    Hay una gran diferencia entre el numero de smbolos necesarios para codificar un entero positivo con unalfabeto unario (base 1) y para codificarlo en binario natural (base 2). No sera mejor aun usar un sistemade numeracion en una base mayor? El numero de smbolos necesarios para expresar un entero positivo n enbase 10 es s10(n) = 1+ blog10 nc. Como log10 n = lgn/ lg10, el numero de cifras necesarias en base 2 soloes unas 3.32 veces mayor que en base 10. Si bien se trata de una reduccion del numero de smbolos, esta semantiene constante para cualquier valor de n. En cambio, la ratio entre el numero de smbolos necesariosal codificar en base 1 y en binario natural no es constante.

    Llamamos codificacion razonable a toda codificacion que permite representar la informacion con unnumero de smbolos proporcional al requerido con el sistema binario. Notese que es necesario que elalfabeto tenga al menos dos smbolos para que sea razonable.

    Si usamos una codificacion razonable y medimos la talla de la instancia en funcion del numero desmbolos necesarios para expresarla, diremos que hacemos uso del criterio del coste logartmico: unnumero entero positivo puede expresarse con un numero de smbolos proporcional a su logaritmo; loselementos de un conjunto con n valores pueden expresarse con un numero de smbolos proporcional allogaritmo de n.

    Apuntes de Algortmica 2-5

  • 2.3 Perfiles de ejecucion: evolucion del tiempo de ejecucion en funcion de la talla de las instancias2004/09/24-15:52

    Los computadores actuales suelen usar una cantidad de memoria fija para representar valores escalares.Es habitual, por ejemplo, usar 32 o 64 bits para codificar los numeros enteros sin signo. Cualquier can-tidad que podamos expresar con la cantidad de bits correspondiente ocupa el mismo numero de celdasde memoria. El criterio del coste uniforme es una simplificacion inspirada en este hecho y que asumeque toda cantidad numerica puede expresarse usando una unica celda de memoria. As, un vector con nenteros positivos, por ejemplo, ocupara n celdas de memoria. El criterio del coste uniforme ayuda notable-mente a simplificar los analisis de coste que efectuaremos, as que lo adoptaremos en adelante salvo cuandoindiquemos lo contrario.

    Cabe decir, finalmente, que la talla de un problema no tiene por que venir determinada por un unicoparametro. Si multiplicamos dos matrices, una de dimension p q y otra de dimension q r, la talla delproblema depende de los tres valores: p, q y r.

    2.3. Perfiles de ejecucion: evolucion del tiempo de ejecucion enfuncion de la talla de las instancias

    Hemos de decidir que entendemos por ((talla de una instancia)) en el contexto del problema de busquedaque estamos resolviendo por tres procedimientos diferentes. Asumiremos el criterio del coste uniforme,as que no nos preocuparemos por la magnitud de los valores del vector ni, en consecuencia, del numerode bits necesarios para codificar cada uno de ellos. La talla del problema, que denotaremos con la letra n,vendra determinada unicamente por el tamano del vector sobre el que se efectua la busqueda. Estudiaremosla evolucion del tiempo de ejecucion con el parametro n.

    timing4.py

    1 from search import naive sequential search2 from time import clock3 from random import seed, randrange, sample4

    5 seed(0)6

    7 tmin = 1 # Tiempo mnimo de ejecucion: un segundo.8

    9 for n in xrange(1, 11):10 a = sorted(sample(xrange(5*n), n))11 x = a[ randrange(n) ]12

    13 t1 = t2 = clock()14 r = 015 while t2 - t1 < tmin:16 index = naive sequential search(a, x)17 t2 = clock()18 r += 119

    20 t3 = t4 = clock()21 aux = 022 while aux < r:23 t4 - t324 t4 = clock()25 aux += 126

    27 t = ((t2 - t1) - (t3 - t2)) / r28 print Tiempomedioporejecucionparan= %d: %.8fsegundos % (n, t)

    Tiempo medio por ejecucion para n=1: 0.00000197 segundos

    Tiempo medio por ejecucion para n=2: 0.00000228 segundos

    Tiempo medio por ejecucion para n=3: 0.00000258 segundos

    Tiempo medio por ejecucion para n=4: 0.00000274 segundos

    Tiempo medio por ejecucion para n=5: 0.00000292 segundos

    Tiempo medio por ejecucion para n=6: 0.00000330 segundos

    Tiempo medio por ejecucion para n=7: 0.00000369 segundos

    2-6 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    Tiempo medio por ejecucion para n=8: 0.00000378 segundos

    Tiempo medio por ejecucion para n=9: 0.00000410 segundos

    Tiempo medio por ejecucion para n=10: 0.00000432 segundos

    La figura 2.1 muestra graficamente el resultado de esta medicion de tiempos de ejecucion de naive_sequential_search.Se puede apreciar que una lnea recta se ajustara razonablemente bien a los puntos de la grafica: el tiemponecesario crece proporcionalmente con la longitud del vector.

    n0 1 2 3 4 5 6 7 8 9 10

    t(en

    segu

    ndos

    )

    0e+000.0e+00

    1.0e-06

    2.0e-06

    3.0e-06

    4.0e-06

    5.0e-06

    Figura 2.1: Medicion de tiempo de ejecu-cion de naive_sequential_search para vec-tores de tamano entre 1 y 10.

    Si repetimos el experimento con la funcion sequential_search obtenemos los resultados que se muestrangraficamente en la figura 2.2. Se puede apreciar una mayor variabilidad en el tiempo medido. Ello se debea que el metodo de busqueda secuencial es sensible a la ubicacion en el vector del valor buscado: porejemplo, acaba mas rapidamente si el elemento buscado ocupa las primeras posiciones y tarda mas si ocupalas ultimas.

    n0 1 2 3 4 5 6 7 8 9 10

    t(en

    segu

    ndos

    )

    0e+000.0e+00

    1.0e-06

    2.0e-06

    3.0e-06

    4.0e-06

    5.0e-06

    Figura 2.2: Medicion de tiempo de ejecu-cion de sequential_search para vectores detamano entre 1 y 10.

    Podemos efectuar una medida de tiempo para cada una de las posibles ubicaciones del elemento bus-cado. El resultado se muestra en la figura 2.3. El tiempo mnimo para cada valor de n corresponde a labusqueda del valor que ocupa la primera posicion del vector. Es su mejor caso. El tiempo maximo corre-sponde a la busqueda del valor que esta en ultima posicion, y es el peor caso al que se enfrenta la rutina(equivalente, en tiempo de ejecucion, a buscar un valor mayor que el del ultimo elemento del vector).

    Podemos acotar superior e inferiormente cada medida de tiempo con sendas funciones de n, como semuestra en la figura 2.4. Una funcion constante constituye una cota inferior ajustada y una lnea recta acotasuperiormente, tambien de forma ajustada, los tiempos maximos para cada valor de n.

    Podemos repetir el experimento para el metodo de busqueda secuencial naf. La figura 2.5 muestra elresultado. Podemos destacar que, a diferencia de lo que ocurre con la busqueda secuencial, el tiempo parael mejor de los casos se acota superior e inferiormente por sendas lneas recta (no constante).

    Si realizamos el mismo experimento con el metodo de busqueda binaria obtenemos los resultados quese muestran en la figura 2.6. En este caso, la cota inferior sigue siendo una constante, pero la cota superiormas ajustada es una funcion logartmica.

    Apuntes de Algortmica 2-7

  • 2.4 Comparacion de perfiles de ejecucion 2004/09/24-15:52

    n0 1 2 3 4 5 6 7 8 9 10

    t(en

    segu

    ndos

    )

    0e+000.0e+00

    1.0e-06

    2.0e-06

    3.0e-06

    4.0e-06

    5.0e-06

    Figura 2.3: Medicion de tiempo de eje-cucion de sequential_search para vectoresde tamano comprendido entre 1 y 10. Paracada vector buscamos todos sus elementos,uno con cada ejecucion.

    n0 1 2 3 4 5 6 7 8 9 10

    t(en

    segu

    ndos

    )

    0e+000.0e+00

    1.0e-06

    2.0e-06

    3.0e-06

    4.0e-06

    5.0e-06

    Figura 2.4: En trazo discontinuo, cotassuperior e inferior para las medidas detiempo con sequential_search y las ejecu-ciones con vectores de tallas comprendidasentre 1 y 10.

    2.4. Comparacion de perfiles de ejecucion

    La figura 2.7 permite comparar las cotas superior e inferior que hemos mostrado en las graficas de las fig-uras 2.4 y 2.6. A tenor de la grafica, parece que sequential_search sea mas eficiente que binary_search.Pero si atendemos a la tendencia de la cota superior del coste, es posible que las dos curvas se crucen y quebinary_search pase a ser mas eficiente a partir de cierto valor de n. Comprobemoslo. La figura 2.8 muestralos resultados de la medicion de tiempos para sequential_search y binary_search. Se puede apreciar (figu-ra 2.9) que para valores de n superiores a 15, el metodo binary_search presenta un comportamiento mejorque sequential_search para el peor de los casos. Este comportamiento es tanto mejor cuanto mayor es elvalor de n.

    Estamos comparando los algoritmos por el tiempo que necesitan para resolver el peor caso que se lesplantea para cada valor de n. Es este un buen criterio de comparacion? Si nuestro programa debe garantizarun tiempo de respuesta determinado, es obvio que s: si acotamos el tiempo maximo que necesita pararesolver un problema de tamano n, sabremos si un algoritmo es o no es adecuado. Pero no es el unico criterioque podemos considerar. Como hemos visto, los metodos naive_sequential_search y sequential_searchpresentan identico comportamiento para el peor de los casos. Pero es obvio que resulta mas adecuado elmetodo sequential_search, pues, ademas, presenta un mejor comportamiento en el mejor de los casos.

    Hay una alegacion que hacer a la comparacion basada en el coste en el peor (y mejor) de los casos, y siel peor caso para cada valor de n corresponde a una instancia muy improbable? No sera mejor seleccionarel metodo que se comporta mejor por termino medio? En principio, el tiempo promedio se obtiene ejecu-tando el programa sobre diferentes instancias de igual talla y calculando la media del tiempo de ejecucion.Pero hay un problema: dada una talla , que instancias usamos para estimar la media? Diferentes instanciasproporcionaran diferentes valores del tiempo promedio para esa talla. Si, por ejemplo, seleccionamos in-stancias en los que el numero buscado aparece en las primeras posiciones obtendremos un tiempo promediomenor que si escogemos instancias en las que el numero buscado aparece en las ultimas posiciones. Paraefectuar una estimacion valida debieramos seleccionar instancias siguiendo una distribucion de probabili-dad similar a la que se da en la explotacion del algoritmo. En nuestro caso, supondremos que hay identicaprobabilidad de que la busqueda del algoritmo secuencial se detenga en cualquier punto del vector.

    La figura 2.10 muestra, ademas de las cotas inferior y superior al tiempo de ejecucion de sequen-tial_search y binary_search, el tiempo promedio para cada valor de n. El coste temporal promedio de

    2-8 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    n0 1 2 3 4 5 6 7 8 9 10

    t(en

    segu

    ndos

    )

    0e+000.0e+00

    1.0e-06

    2.0e-06

    3.0e-06

    4.0e-06

    5.0e-06

    Figura 2.5: Medicion de tiempo de eje-cucion de naive_sequential_search paravectores de tamano entre 1 y 10. Laslneas discontinuas acotan superior e infe-riormente los valores de tiempo obtenidosal ejecutar el programa de busqueda sobrecada uno de los elementos del vector.

    n0 1 2 3 4 5 6 7 8 9 10

    t(en

    segu

    ndos

    )

    0e+000.0e+00

    1.0e-06

    2.0e-06

    3.0e-06

    4.0e-06

    5.0e-06

    Figura 2.6: Medicion de tiempo de eje-cucion de binary_search para vectores detamano entre 1 y 10. Para cada tamano devector se efectua la busqueda de cada unode sus elementos. Las curvas acotan supe-rior e inferiormente los valores obtenidos.

    n0 1 2 3 4 5 6 7 8 9 10

    t(en

    segu

    ndos

    )

    0e+000.0e+00

    1.0e-06

    2.0e-06

    3.0e-06

    4.0e-06

    5.0e-06

    Figura 2.7: Comparacion entre cotas su-perior e inferior para las ejecuciones de se-quential_search (trazo discontinuo) y bi-nary_search (trazo continuo) para vectoresde talla comprendida entre 1 y 10.

    n0 10 20 30 40 50 60 70 80 90 100

    t(en

    segu

    ndos

    )

    0e+000.0e+00

    5.0e-06

    1.0e-05

    1.5e-05

    2.0e-05

    2.5e-05

    n0 10 20 30 40 50 60 70 80 90 100

    Figura 2.8: A la izquierda, resultado de la medicion de tiempo de ejecucion de sequential_search para vectoresde tamanos comprendidos entre 1 y 100. A la derecha, dem para binary_search. La escala para el tiempo es lamisma en las dos figuras.

    Apuntes de Algortmica 2-9

  • 2.4 Comparacion de perfiles de ejecucion 2004/09/24-15:52

    n0 10 20 30 40 50 60 70 80 90 100

    t(en

    segu

    ndos

    )

    0e+000.0e+00

    5.0e-06

    1.0e-05

    1.5e-05

    2.0e-05

    2.5e-05

    Figura 2.9: Comparacion entre cotas su-perior e inferior para las ejecuciones de se-quential_search (trazo discontinuo) y bi-nary_search (trazo continuo) sobre vec-tores de talla entre 1 y 10.

    n0 10 20 30 40 50 60 70 80 90 100

    t(en

    segu

    ndos

    )

    0e+000.0e+00

    5.0e-06

    1.0e-05

    1.5e-05

    2.0e-05

    2.5e-05

    Figura 2.10: En trazo grueso se muestrael tiempo promedio para las ejecuciones desequential_search (lnea discontinua) y bi-nary_search (lnea continua), y en trazo fi-no, las respectivas cotas superior e inferior.

    sequential_search es, bajo este supuesto de equiprobabilidad, la semisuma del coste para el mejor y para elpeor de los casos. No ocurre lo mismo con la busqueda binaria. Mas adelante estudiaremos analticamenteEl tiempo promedio no

    siempre coincide con lasemisuma del tiempo en elmejor y en el peor de loscasos, aunque en este casoocurra as.

    este comportamiento.Podemos observar que tambien en tiempo promedio resulta mas eficiente el metodo binary_search

    cuando n es suficientemente grande: el tiempo promedio de sequential_search se puede ajustar bien conuna lnea recta mientras que el de binary_search se describe mejor con una funcion logartmica.

    Cabe destacar que hemos supuesto que la probabilidad de que busquemos cualquiera de los elementosdel vector es identica. En una aplicacion practica, no obstante, puede que no sea el caso y, por tanto, que elcoste promedio manifieste un comportamiento diferente.

    Del estudio realizado podemos sacar algunas conclusiones:

    La complejidad temporal relaciona el tiempo necesario para resolver una instancia del problema conla talla del problema.

    El tiempo de ejecucion no es funcion de la talla en el sentido de que no siempre existe un unicovalor de tiempo asociado a la resolucion de cualquier instancia con una talla determinada (ni siquieradescontando los errores de precision en la medida). Hay un rango de valores posibles para el tiempode ejecucion en funcion de la talla. El rango viene fijado por el tiempo de ejecucion en el mejor ypeor de los casos.

    Cabe esperar que el tiempo de ejecucion crezca con la talla del problema. No necesariamenteocurre as si comparamos el tiempo de ejecucion de instancias particulares, pues algunas resultanmas ((favorables)) que otras. Por regla general observamos esta tendencia si consideramos el tiempode ejecucion en el peor de los casos o en promedio.El tiempo de ejecucion promedio puede utilizarse como criterio de comparacion entre algoritmos,pero en tal caso hemos de prestar atencion a la distribucion de probabilidad de las instancias paracada valor de la talla estudiado.

    Un programa puede resultar mas eficiente que otro para instancias de talla pequena, pero dejar deserlo cuando nos enfrentamos a instancias de talla suficientemente grande. Los ordenadores suelen

    2-10 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    resultar eficaces cuando abordamos problemas de gran talla, por lo que resulta relevante centrar elestudio en el comportamiento del tiempo de ejecucion en valores suficientemente grandes de la talladel problema.

    2.5. Conteo de pasos

    Las tecnicas de medicion de tiempo presentan un gran inconveniente: deben efectuarse a partir de unaimplementacion concreta del algoritmo. Por otra parte, las medidas obtenidas son imprecisas. Vamos aplantear un metodo que sacrifica el detalle para dar una vision mas basta del coste temporal, pero que puederesultar suficiente para efectuar comparaciones. Una ventaja fundamental del metodo que proponemosahora es que no requiere que dispongamos de un implementacion concreta del algoritmo. Pretendemosalcanzar as independencia del sistema computador y del lenguaje de programacion. Nos basaremos enel conteo de pasos. Un paso es una instruccion o conjunto de instrucciones cuyo tiempo de ejecucionesta acotado por un valor constante. Consideremos estas dos asignaciones en el lenguaje de programacionPython:

    1 a = 12 b = a * 10 + 1

    Ciertamente la segunda ha de tardar mas tiempo en ejecutarse que la primera: comporta una multipli-cacion, una suma y una asignacion, cuando la primera es una simple asignacion; pero ambas sentencias seejecutan en tiempo constante, as que cada una de ellas se considera un paso.

    Notese que el concepto de paso es independiente del lenguaje de programacion. Cada una de las dossentencias del ejemplo se computa como un paso, y seguiran contando como pasos individuales si estu-vieran escritas en un lenguaje de programacion diferente.

    2.5.1. Conteo de pasos en algoritmos iterativos

    Estudiemos ahora este fragmento de programa:

    1 s = 02 for i in xrange(n):3 s += i

    La primera lnea se considera un paso, evidentemente. La segunda lnea es especial: su ejecucion se repiten veces, as que el tiempo de ejecucion depende del valor de n. No podemos acotar su tiempo de ejecucionpor una constante: siempre habra un valor suficientemente grande de n que haga que el tiempo de ejecucionsea superior a cualquier valor constante fijado de antemano. No podemos considerar que esta lnea cuente,pues, como un solo paso, sino n. La tercera lnea, por s sola, es un paso, pero se encuentra en un bucle quese ejecuta n veces. Hemos de contarla, pues, como n pasos. El numero de pasos que requiere la ejecucionde las tres lneas es 1+n+n = 2n+1.

    Veamos un fragmento de programa algo mas complejo:1 s = 02 for i in xrange(n):3 for j in xrange(n):4 s += i

    La tercera lnea comporta la ejecucion de n iteraciones de un bucle, pero el bucle completo se ejecutan veces (esta dentro de otro bucle), as que su ejecucion aporta un numero total de n2 pasos. La lnea4 aporta otros n2 pasos. El numero de pasos que supone la ejecucion de este fragmento de programa es1+n+n2 +n2 = 2n2 +n+1.

    Este otro fragmento requiere algo de calma al efectuar el conteo:

    1 s = 02 for i in xrange(n):3 for j in xrange(3):4 s += i

    Al contener un bucle dentro de otro, se puede pensar que el numero de pasos sera un polinomio de grado2 con n, pero no es as. El bucle indexado con j se ejecuta tres veces. El numero de pasos que supone

    Apuntes de Algortmica 2-11

  • 2.5 Conteo de pasos 2004/09/24-15:52

    la ejecucion de las lneas 3 y 4 es, pues, 6. Y el numero de pasos que resulta de ejecutar el fragmento deprograma es 1+n+3n+3n = 7n+1.

    El conteo de pasos recoge suficiente informacion sobre un algoritmo como para que sepamos si el tiem-po crece en proporcion directa a la talla del problema, o a su cuadrado, o a su logaritmo, etc. Puede resultarsuficiente para una primera comparacion entre dos algoritmos: sabemos que si el tiempo de ejecucion deun algoritmo crece linealmente sera peor a partir de determinado valor de la talla del problema que otro quecrezca en proporcion a su logaritmo.

    Hay cierta arbitrariedad en la definicion del concepto de paso. Estas lneas, por ejemplo, han sidocomputadas antes como dos pasos:

    1 a = 12 b = a * 10 + 1

    Pero, de acuerdo con la definicion, tambien pueden computarse como un solo paso: el tiempo de ejecucionde ambas lneas puede acotarse por una constante (por ejemplo, una que sea el doble de la constante quela usada cuando cada lnea se computo como un paso). No hay problema en ello. El conteo de pasos nopretende estimar con exactitud el tiempo de ejecucion.

    Contemos el numero de pasos del metodo de busqueda secuencial naf, cuya implementacion en Pythonrecordamos ahora:

    1 def naive sequential search(a, x):2 index = None3 for i in xrange(len(a)):4 if a[i] == x:5 index = i6 return index

    La primera de las lneas es una cabecera de funcion. Por s sola no parece comportar coste alguno.Podemos considerar, eso s, que cuando se llame a la funcion se ejecutara un paso, pues las acciones propiasde la llamada a funcion requieren un tiempo total constante: crear un registro de activacion en la pila dellamadas a funcion, apilar la direccion del vector a y del valor x, reservar espacio para variables locales,guardar en pila la direccion de memoria desde la que se efectua la llamada y saltar a una nueva direccionde memoria. La asignacion de la lnea 2 es un paso. El bucle efectua tantas iteraciones como tamano tieneel vector a. Si denotamos dicho tamano con n, el numero de pasos que comporta el bucle en s es n. Lacomparacion del valor de a[i] con el valor de x se efectua n veces, as que la lnea 4 aporta a nuestra cuentaotros n pasos. La lnea 5 solo se ejecuta una vez (estamos suponiendo que los n elementos del vector sondiferentes), as que solo aporta un paso, aunque este dentro de un bucle. Finalmente, la devolucion delndice cuenta como un solo paso, aunque supone la ejecucion de algunas acciones relativamente complejas(desapilar el registro de activacion y saltar a la direccion desde la que se efectuo la llamada a la funcion).El numero total de pasos es, pues, 1+1+n+n+1+1 = 2n+4.

    Procedimientos como naive_sequential_search permiten encontrar una relacion entre la talla del vector,n, y el numero de pasos. No siempre es as. Consideremos el otro metodo de busqueda secuencial:

    1 def sequential search(a, x):2 if x < a[0]:3 return None4 for i in xrange(len(a)):5 if x == a[i]:6 return i7 elif x < a[i]:8 return None9 return None

    El numero de pasos depende ahora de la talla del vector y del valor buscado. Podemos, eso s, acotarsuperior e inferiormente el numero de pasos: en el mejor caso se ejecutan las lneas 1 (llamada a la funcion),2 y 3 y en el peor caso se ejecuta una vez las lneas 1, 2 y 9 y n veces las lneas 4, 5 y 7. As pues, el numerode pasos es siempre mayor o igual que 3 y menor o igual que 3n+3. Como se ve, en ese caso no es posiblehablar de una funcion que exprese el coste temporal en funcion de n y hemos de pasar a considerar elnumero de pasos en el mejor y en el peor de los casos. Podemos proporcionar ambos costes para describirel comportamiento del algoritmo.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PROBLEMAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-1 Calcula el numero de pasos, en funcion del entero n, de estos programas Python. Si hay un mejory peor caso, presenta una cota inferior y una superior, respectivamente, para el numeros de pasos.

    2-12 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    a) 1 def f 1(n):2 i = n3 s = 04 while i > 0:5 s += i6 i -= 17 return s

    b) 1 def f 2(n):2 n = int(raw input())3 s = n

    4 return s

    c) 1 def f 3(n):2 v = range(n)3 s = 04 for i in v:5 s += i6 return s

    d) 1 def f 4(n):2 for i in range(n):3 if i == 10:4 break5 return i

    e) 1 def f 5(n):2 for i in xrange(n):3 if i == 10:4 break

    5 return i

    f) 1 def f 6(n):2 return range(n)

    g) 1 def f 7(n):2 for i in xrange(n):3 aux = range(n)4 for j in xrange(n):5 del aux[0]

    h) 1 def f 8(n):2 for i in xrange(n):3 if i == 10:4 continue5 return i

    i) 1 def f 9(n):2 v = range(n)3 s = 04 for i in v:5 for j in v:6 s += i*j7 return s

    j) 1 def f 10(n):2 for i in xrange(n):3 print i4 if i == n/2:5 return

    2-2 Te mostramos a continuacion tres procedimientos para ordenar, de menor a mayor, los elementosde un vector. Calcula el numero de pasos que comporta su ejecucion en funcion del tamano del vector quese suministra como parametro:

    a) Ordenacion por seleccion.

    1 def selection sort(a):2 for i in xrange(n-1):3 k = i4 for j in xrange(i+1, n):5 if a[j] < a[k]: k = j6 a[i], a[k] = a[k], a[i]

    b) Ordenacion por el metodo de la burbuja.1 def bubblesort(a):2 for i in xrange(len(a)):3 for j in xrange(len(a)-1-i):4 if a[j] > a[j+1]:5 a[j], a[j+1] = a[j+1], a[j]

    c) Ordenacion por insercion.

    1 def insertion sort(a):2 for i in xrange(1, n):3 x = a[i]4 j = i-15 while j >= 0 and x < a[j]:6 a[j+1] = a[j]7 j -= 18 a[j+1] = x

    Apuntes de Algortmica 2-13

  • 2.5 Conteo de pasos 2004/09/24-15:52

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

    Analicemos el numero de pasos con el que binary_search resuelve el problema de la busqueda. Recorde-mos su implementacion en Python:

    1 def binary search(a, x):2 left, right = 0, len(a)3 while left < right:4 i = (right + left) / 25 if a[i] == x:6 return i7 elif a[i] > x:8 right = i9 else:

    10 left = i + 111 return None

    Nuevamente el numero de pasos depende del valor buscado, y no solo de n. Con el fin de simplificar elestudio supondremos que n es potencia de 2. Si el elemento buscado ocupa la posicion n/2 del vector, seejecutan 6 pasos. Que ocurre en otro caso? Depende: si el valor del elemento en la posicion n/2 es mayorque x, se busca en un vector con n/2 elementos; si es menor, se busca en un vector con n/21 elementos.Consideremos el peor de los casos: que nos toque buscar en el vector con n/2 elementos. Aplicando unrazonamiento similar y considerando tambien el peor caso, tendremos que continuar la busqueda en unvector de n/4 elementos. Y as hasta llegar a considerar un vector con un solo elemento. Esto ocurrira trasrepetir el proceso lgn veces. Cada una de las iteraciones supone la ejecucion de 5 pasos, excepto la ultima,que ejecuta 4. A estos pasos hay que sumar el de la llamada a la funcion y el de la inicializacion de lasvariables left y right. El numero de pasos ejecutados en el peor de los casos es, pues, 5 lgn+2.

    2.5.2. Conteo de pasos en algoritmos recursivos

    Los algoritmos que hemos visto son de naturaleza iterativa, es decir, se basan en la repeticion de un calculocontrolado por un bucle (for o while). Tambien consideraremos en el texto algoritmos recursivos, esto es,algoritmos que expresamos con funciones que, al resolver una instancia, se llaman a s mismas sobre otrasinstancias de menor talla. El procedimiento de busqueda binaria puede expresarse recursivamente:

    1 def recursive binary search(a, x, left, right):2 if left < right:3 i = (right + left) / 24 if a[i] == x:5 return i6 elif a[i] > x:7 return binary search(a, x, left, i)8 else:9 return binary search(a, x, i+1, right)

    10 else:11 return None

    Hemos de invocar el metodo recursivo con recursive_binary_search(a, x, 0, len(a)) para buscar elelemento x en a. Cuando n vale cero, el algoritmo requiere la ejecucion de 3 pasos. Para valores de nmayores, nuestro algoritmo presenta un mejor y un peor caso. El numero de pasos de un algoritmo recursivopuede expresarse, en primera instancia, mediante una ecuacion recursiva. Supongamos que n es potenciade 2 y mayor que cero. El mejor caso requiere la ejecucion de 5 pasos. El peor caso obliga a ejecutar unnumero de pasos que podemos expresar mediante esta ecuacion recursiva:

    f (n) ={

    6+ f (n/2), si n > 1;5, si n = 1.

    El termino superior de la ecuacion es el denominado ((termino)) o ((caso general)) (es una expresion recursi-va) y el inferior define su ((caso base)) (cuando finaliza la recursion).

    Las ecuaciones recursivas admiten, en muchos casos, una expresion cerrada. Decimos que la expresioncerrada es la solucion de la ecuacion recursiva. En la seccion B.12 de los apendices se resumen algunas

    2-14 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    tecnicas para la resolucion de ecuaciones recursivas. Una de las tecnicas comunmente utilizadas es eldesplegado, y consiste en sustituir reiteradamente la parte izquierda de la ecuacion por su parte derecha conobjeto de identificar un patron:

    f (n) = 6+ f (n/2) = 6+6+ f (n/4) = 6+6+6+ f (n/8) = = 6k+ f (n/2k).El desplegado finaliza cuando n/2k = 1, es decir, cuando k = lgn, pues entonces llegamos al caso base dela ecuacion recursiva:

    f (n) = 6k+ f (n/2k) = 6lgn+5.El metodo del desple-gado permite formularuna hipotesis sobre laexpresion cerrada deuna ecuacion recursiva,pero no constituye unademostracion de quees solucion. Podemosdemostrar por induccionque lo es, pero en aras debrevedad lo dejamos comoejercicio al lector.

    El numero de pasos de la version recursiva de la busqueda binaria es, pues, comparable al de la versioniterativa.

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PROBLEMAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-3 Calcula el numero de pasos, en funcion del entero n, de estos programas Python. Si hay un mejory peor caso, presenta una cota inferior y una superior, respectivamente, para el numeros de pasos.

    a) 1 def f 1(n):2 if n == 1:3 return 14 else:5 return f 1(n-1) * 2

    b) 1 def f 2(n):2 if n == 1:3 return 14 else:5 return f 2(n-2) + 1

    c) 1 def f 3(n):

    2 if n == 1:3 return 14 else:5 return min(f 3(n-1)*2, f 3(n-2)*3)

    d) 1 def f 4(n):2 if n == 1:3 return 14 else:5 s = 06 for i in xrange(n):7 s += i8 return f 4(n/2) + s

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

    2.6. Notacion asintotica

    Dada la definicion tan laxa del concepto de paso, un analisis excesivamente detallado puede no merecer lapena. Cuando decimos que el coste en el peor de los casos es 2n+4 o 3n+3, que significan exactamentesus diferentes constantes? Hay cierta arbitrariedad en su valor, pues dependen de que criterio adoptamos alconsiderar que contamos como un solo paso. Podramos decir que el numero de pasos es c1n+ c0, para c1y c0 constantes, y estaramos proporcionando la misma informacion fundamental: que el numero de pasoscrece en proporcion directa al valor de n.

    La denominada notacion asintotica es una forma de describir el comportamiento de las funciones decoste temporal (o espacial) en funcion de la talla que resulta muy concisa y, a la vez, simplifica los analisisde coste. El objetivo de la notacion asintotica es agrupar las funciones de N en R0 en una serie defamilias atendiendo a su crecimiento. Cada familia de funciones se caracteriza por estar acotada superiory/o inferiormente para valores de n suficientemente grandes por una funcion afectada por cierta constantepositiva.

    Empezamos por estudiar un agrupamiento de las funciones atendiendo a su cota superior.

    2.6.1. Cota asintotica superior: notacion ((orden de))

    Dada una funcion g : N R0, denotamos con O(g(n)) al conjunto de funciones f (n) tales que existenuna constante positiva c y un entero n0 que hacen que f (n) c g(n) para n n0:

    O(g(n))={ f : N R0 | c > 0,n0 > 0 : f (n) c g(n),n n0}. (2.1)La expresion O(g(n)) se lee como ((orden de g(n))) y f (n) O(g(n)) se lee tanto diciendo (( f (n) En ingles se dice ((big-

    oh of f (n))), es decir, ((omayuscula de f (n))).pertenece a O(g(n)))) como (( f (n) es O(g(n)))). En ocasiones usamos la expresion f (n) = O(g(n)) paraindicar que f (n) O(g(n)). Si f (n) O(g(n)) decimos que g(n) es una cota superior asintotica de f (n)Cota superior asintotica:Asymptotic upper bound.y que f (n) es ((orden de g(n))).

    Apuntes de Algortmica 2-15

  • 2.6 Notacion asintotica 2004/09/24-15:52

    La figura 2.11 ilustra la idea de que una funcion f (n) sea orden de otra, g(n): existe un valor c tal queinicialmente f (n) puede ser mayor o menor que c veces g(n), pero a partir de cierto valor n0, f (n) es menoro igual que c g(n).

    n

    c g(n)

    f (n) O(g(n))

    n0

    Figura 2.11: f (n) es O(g(n)) porque, paran mayor o igual que un n0 dado y para un cpositivo determinado, f (n) c g(n).

    Unos ejemplos ayudaran a entender el concepto de ((orden de)):

    La funcion f (n) = n+ 1, por ejemplo, pertenece a O(n), pues siempre hay un valor n0 y un valor cpara los que n+ 1 c n si n n0. Consideremos, por ejemplo, n0 = 1 y c = 2: n+ 1 es menor oigual que 2n para todo valor de n mayor o igual que 1.

    La funcion f (n) = 5n+12 tambien es O(n). Se observa 5n+12 6n para todo n mayor o igual que12.

    La funcion f (n) = 4n2 + 2n+ 1 es O(n2). El valor de f (n) es menor o igual que 5n2 para todo nmayor o igual que 3.

    Vamos a presentar una serie de resultados que nos permitiran clasificar rapidamente una funcion de Nen R0 en el orden al que pertenece. El primero de ellos nos permite calcular inmediatamente el orden deun polinomio:

    Teorema 2.1 Si f (n) = amnm + +a1n1 +a0 es un polinomio de grado m, entonces f (n) O(nm).

    Demostracion.

    f (n) = amnm + +a1n1 +a0n0 |am|nm + + |a1|n1 + |a0|n0 (|am|+ + |a1|+ |a0|)nm,

    para n 1. Si escogemos c = |am|+ + |a1|+ |a0| y n0 = 1 tenemos f (n) O(nm).

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PROBLEMAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-4 Calcula el orden de estas funciones:

    a) f (n) = 3n+2.b) f (n) = 100n+6.

    c) f (n) = 10n24n+12.d) f (n) = 2n2 +1000000n+16.

    e) f (n) = 10n10 +4n4 +2.f) f (n) = 2.

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

    Teorema 2.2 (Regla de los maximos) Dadas dos funciones f , g : N R0, se cumple O( f (n)+g(n)) =O(max( f (n),g(n))).

    Demostracion. Obviamente, f (n) + g(n) = min( f (n),g(n)) +max( f (n),g(n)) y 0 min( f (n),g(n)) max( f (n),g(n)). As pues,

    max( f (n),g(n)) f (n)+g(n) 2max( f (n),g(n)). (2.2)

    Consideremos cualquier funcion t(n) O( f (n)+g(n)). Sea c una constante tal que t(n) c( f (n)+g(n))para todo n suficientemente grande. Por (2.2), t(n) c(max( f (n),g(n))) para algun valor de c y los mismosvalores de n, as que O( f (n)+g(n)) O(max( f (n),g(n))).

    2-16 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    En el sentido contrario, consideremos cualquier funcion t(n) O(max( f (n),g(n))). Existe una con-stante c tal que t(n) c(max( f (n),g(n))), para todo n suficientemente grande. Tambien por (2.2), tenemost(n) c( f (n)+g(n)), as que O(max( f (n),g(n))) O( f (n)+g(n)).

    La regla de los maximos nos permite simplificar funciones que son suma de varios terminos al per-mitir considerar unicamente el mayor de ellos. Una funcion como f (n) = n3 + n lgn, por ejemplo, esO(max(n3,n lgn)) = O(n3). La regla de los maximos no se limita a funciones que son suma de otras dos:se puede generalizar a cualquier numero de sumandos.

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PROBLEMAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-5 Son ciertas las siguientes afirmaciones?

    a) 3n+2 O(n).b) 100n+6 O(n).

    c) 10n2 +4n+2 O(n2).d) 10n2 +4n+2 O(n3).

    e) 6 2n +n2 O(2n).f) 6 2n +n2 O(n100).

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

    Presentamos a continuacion (sin demostracion) algunos resultados y propiedades interesantes a la horade determinar el orden de una funcion. En todos ellos suponemos que las funciones tienen como dominioN y como rango R0:

    Transitividad. f (n) O(g(n))g(n) O(h(n)) f (n) O(h(n)).Reflexividad. f (n) O( f (n)).Regla de la constante. O(c f (n)) = O( f (n)).Regla del producto. f1(n) O(g1(n)) f2(n) O(g2(n)) f1(n) f2(n) O(g1(n) g2(n)).

    Si una funcion f (n) pertenece a O(n), por ejemplo, tambien pertenece a O(n2), ya que O(n) O(n2).No obstante, se debe procurar siempre indicar el orden de una funcion con su cota mas ajustada. As, def (n) = n+1 no decimos que es O(n2) (aunque, evidentemente, lo es), sino que es O(n).

    Hay una relacion de inclusion entre las diferentes familias de funciones: Evidentemente, esta rela-cion no es exhaustiva.

    O(1) O(logn) O(n) O(n) O(n logn) O(n2) O(n3) O(2n) O(nn).

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PROBLEMAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-6 Determina el orden de estas funciones:

    1) f (n) = 4.2) f (n) = 0.1.3) f (n) = 108374.4) f (n) = n.5) f (n) = 10n.6) f (n) = 0.028764n.7) f (n) = pin.8) f (n) = n+3.9) f (n) = 2n+1093842.

    10) f (n) = n2.11) f (n) = 4n2.12) f (n) = n2 +n.

    13) f (n) = 8n2 +n.14) f (n) = 0.1n2 +2n+1.15) f (n) = 20n3n2 +1000n.16) f (n) = n6 +n3 +10n.17) f (n) = n100 +n99.18) f (n) = 2n.19) f (n) = 2n +n100.20) f (n) = 3n +2n.21) f (n) = lgn.22) f (n) = logn.23) f (n) = n+ lgn.24) f (n) = n logn.

    25) f (n) = n2 +n logn.26) f (n) = n logn2.27) f (n) = n2 lgn.28) f (n) = n2 +n2 lnn.29) f (n) = lnn2.30) f (n) = n lgn3.

    31) f (n)={

    3n+2, si n par;8n2 +n, si n impar.

    32) f (n) ={

    3n+2, si n par;8n+n, si n impar.

    33) f (n) ={

    3n+2, si n < 10;n2 +n, si n 10.

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

    Las funciones que pertenecen a cada familia de funciones tienen adjetivos que las identifican, como semuestra en la tabla 2.2. As, decimos que el coste temporal de un algoritmo es lineal cuando su tiempo deejecucion o numero de pasos es O(n) y cubico cuando es O(n3). De hecho, abusando del lenguaje decimosque el algoritmo es de tiempo lineal o cubico, respectivamente.

    Apuntes de Algortmica 2-17

  • 2.6 Notacion asintotica 2004/09/24-15:52

    SublinealesConstantes O(1)

    Logartmicas O(logn)

    Polilogartmicas O(logk n)

    O(

    n)

    Lineales O(n)

    SuperlinealesO(n logn)

    Polinomicas Cuadraticas O(n2)

    Cubicas O(n3)

    Exponenciales O(2n)

    O(nn)

    Tabla 2.2: Nombre quereciben las funcionespertenecientes a algunosordenes.

    2.6.2. Cota asintotica inferior: notacion ((omega))

    As como O() proporciona una cota superior asintotica para una funcion, () proporciona una cota infe-rior asintotica. (g(n)) es el conjunto de funciones f (n) tales que existen una constante positiva c y unentero n0 que hacen que f (n) c g(n) para n n0:

    (g(n))={ f : N R0 | c > 0,n0 > 0 : f (n) c g(n),n n0}. (2.3)De una funcion f (n) que pertenece a (g(n)) se dice que ((es (g(n)))) y se lee ((es omega de g(n))).

    La figura 2.12 ilustra la idea de que una funcion f (n) sea (g(n)).

    n

    c g(n)

    f (n) (g(n))

    n0

    Figura 2.12: f (n) es (g(n)): a partir decierto valor n0 de n, f (n) siempre es mayor oigual que c veces g(n), para algun valor posi-tivo de c.

    En ocasiones usamos la notacion f (n) =(g(n)) para expresar f (n)(g(n)). Por ejemplo, la funcionf (n) = n+1 es (n), ya que n+1 n para todo n 1 y c = 1.

    Hay un teorema analogo al teorema 2.1:

    Teorema 2.3 Si f (n) = amnm + am1nm1 + + a1n+ a0 es un polinomio de grado m, con am > 0, en-tonces f (n) (nm).Demostracion.

    f (n) = amnm +am1nm1 +a1n1 +a0n0 amnm|am1|nm1 |a1|n1|a0|n0= amnm (|am1|nm1 + + |a1|n1 + |a0|n0).

    Hemos demostrado antes () que |am1|nm1 + + |a1|n1 + |a0|n0 O(nm1), as que existen un n0 y unaconstante c tales que, para todo n mayor que n0, se cumple |am1|nm1 + + |a1|n1 + |a0|n0 c nm1.Por tanto,

    f (n) amnm (|am1|nm1 + + |a1|n1 + |a0|n0) amnm c nm1para todo n n0.

    Tenemos quef (n) amnm c nm1 cnm

    si c observa 0 < c < am y para todo n mayor que n0 = camc . Por tanto, f (n) (nm).

    2-18 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    Al expresar la de una funcion debe usarse la cota mas ajustada posible, es decir, si decimos f (n) =(g(n)), usamos la funcion g(n) mas ((grande)) posible.

    He aqu algunas propiedades equivalentes a otras que ya hemos visto al estudiar la notacion ((orden de)):

    Regla de los maximos. ( f (n)+g(n)) = (max( f (n),g(n))).Transitividad. f (n) (g(n))g(n) (h(n)) f (n) (h(n)).Reflexividad. f (n) ( f (n)).Regla de la constante. (c f (n)) = ( f (n)).Regla del producto. f1(n) (g1(n)) f2(n) (g2(n)) f1(n) f2(n) (g1(n) g2(n)).

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PROBLEMAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-7 Determina la cota inferior asintotica de las funciones del ejercicio 2-6.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

    2.6.3. Cota asintotica superior e inferior: notacion ((zeta))

    Cuando una funcion f (n) es a la vez O(g(n)) y (g(n)), decimos que es (g(n)) (que se lee ((zeta deg(n)))).

    (g(n))=O(g(n))(g(n)). (2.4)Una funcion f (n) que pertenece a (g(n)) esta asintoticamente acotada superior e inferiormente,

    por sendas funciones proporcionales a g(n) a partir de un valor determinado de n (vease la figura 2.13):

    (g(n)) = { f (n) | c1,c2 > 0,n0 > 0 : c1g(n) f (n) c2g(n),n n0}. (2.5)En ocasiones usamos f (n) = (g(n)) para expresar f (n) (g(n)) y decimos que (( f es zeta de g)).

    n

    c2 g(n)

    c1 g(n)

    f (n) (g(n))

    n0

    Figura 2.13: f (n) es (g(n)) porque esO(g(n)) y (g(n)).

    Con se induce una relacion de equivalencia sobre las funciones de N en R0. Cada conjunto (g(n))es una clase de equivalencia. Las clases de equivalencia reciben el nombre de clases de complejidad.

    Evidentemente, y a tenor de los teoremas 2.1 y 2.3, si f (n) = amnm +am1nm1 + +a1n+a0 es unpolinomio de grado m, con am > 0, entonces f (n) (nm).

    Las siguientes propiedades son de interes:

    Transitividad. f (n) (g(n))g(n) (h(n)) f (n) (h(n)).Reflexividad. f (n) ( f (n)).Simetra traspuesta (o regla de la dualidad). f (n) (g(n)) g(n) ( f (n)).Regla de la constante. (c f (n)) = ( f (n)).Regla del producto. f1(n) (g1(n)) f2(n) (g2(n)) f1(n) f2(n) (g1(n) g2(n)).

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PROBLEMAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-8 Determina ((la zeta)) de las funciones del ejercicio 2-6.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

    Apuntes de Algortmica 2-19

  • 2.7 Expresion del coste temporal con la notacion asintotica 2004/09/24-15:52

    2.6.4. Expresiones aritmeticas con cotas

    Sabemos que si f (n)O(h1(n)) y g(n)O(h2(n)), para f y g funciones de N en R0, la suma f (n)+g(n)es O(max(h1(n),h2(n))). En ocasiones usaremos expresiones como O(h1(n))+O(h2(n)) para denotar elorden de la suma de las funciones f y g, que es O(h1(n)) +O(h2(n)) = O(max(h1(n),h2(n))). Noteseque no se trata de ecuaciones, pues llegaramos a un absurdo si deducimos de O(n)+O(n) = O(n) queO(n) = 0.

    2.7. Expresion del coste temporal con la notacion asintotica

    Habamos visto que el numero de pasos que comporta la ejecucion de naive_sequential_search era f (n) =2n+ 4. Se trata de una funcion O(n) y (n), es decir, (n). Decimos que naive_sequential_search esun algoritmo de coste temporal lineal (o simplemente ((lineal)), si se sobreentiende que hablamos del costetemporal).

    El busqueda secuencial sequential_search presentaba un cote temporal comprendido entre 3 (mejor delos casos) y 3n+3 (peor de los casos). De la primera funcion podemos decir que es (1), y de la segunda,que es (n). No obstante, del peor de los casos solo suele ofrecerse su cota superior asintotica y del mejorde los casos, su cota inferior asintotica. As, decimos que sequential_search es (1) y O(n). Noteseque estamos extendiendo el uso de la notacion asintotica al coste temporal en s, cuando este no es unafuncion. Se trata de un uso comun de la notacion asintotica: la notacion ((orden de)) se usa generalmentepara acotar superiormente el coste temporal en el peor de los casos, y la notacion ((omega)) para acotarinferiormente el coste temporal en el mejor de los casos. Siguiendo este proceder, podemos decir que elmetodo binary_search es (1) y O(lgn).

    Por regla general resulta mas informativo el coste en el peor de los casos. Muchas veces, al describirel coste temporal de un algoritmo, se omite su coste temporal en el mejor de los casos (o si cota asintoticainferior). Solo cuando la cotas asintoticas inferior y superior coinciden, se indica explcitamente con eluso de la notacion . Podramos resumir y sintetizar la informacion considerada mas relevante en los tresalgoritmos estudiados as:

    naive_sequential_search es (n).

    sequential_search es O(n).

    binary_search es O(lgn).

    La expresion de costes mediante su cota superior (y posiblemente inferior) no solo sintetiza la informa-cion esencial: resulta, ademas, util a la hora de simplificar los calculos necesarios para efectuar el coste.Anotamos el aporte al coste global de cada una de las lneas de naive_sequential_search:

    1 def naive sequential search(a, x): # (1) por la llamada.2 index = None # Asignacion (1)3 for i in xrange(len(a)): # Bucle que se ejecuta (n) veces.4 if a[i] == x: # Comparacion (1) que se repite (n) veces.5 index = i # Asignacion (1) que se ejecuta una sola vez.6 return index # (1) por fin de rutina y devolucion de valor.

    No se cuentan los pasos individuales, sino la contribucion al coste asintotico global, que es independi-ente de factores constantes. El coste global es (1)+(1)+(n)+(n)+(1)+(1), que es (n).

    Analicemos el coste de sequential_search:1 def sequential search(a, x): # (1).2 if x < a[0]: # (1).3 return None # (1), pero cero veces o una, o sea, O(1).4 for i in xrange(len(a)): # Bucle que se ejecuta cero o n veces: O(n) pasos.5 if x == a[i]: # Comparacion (1) que se ejecuta O(n) veces.6 return i # (1), pero cero o una vez: O(1).7 elif x < a[i]: # Comparacion (1), que se ejecuta O(n) veces.8 return None # (1), pero cero o una vez: O(1).9 return None # (1), pero cero o una vez: O(1).

    El coste temporal global es (1)+(1)+O(1)+O(n)+O(n) (1)+O(1)+O(n) (1)+O(1)+O(1),o sea, O(n).

    Y, finalmente, analicemos el coste temporal de binary_search:

    2-20 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    1 def binary search(a, x):2 left, right = 0, len(a) # (1)3 while left < right: # O(lgn) veces.4 i = (right + left) / 2 # (1), ejecutado O(lgn) veces.5 if a[i] == x: # (1), ejecutado O(lgn) veces.6 return i # (1), ejecutado a lo sumo una vez: O(1).7 elif a[i] > x: # (1), ejecutado O(lgn) veces.8 right = i # (1), ejecutado O(lgn) veces.9 else:

    10 left = i + 1 # (1), ejecutado O(lgn) veces.11 return None # (1), a lo sumo una vez.

    Operando de modo similar a como ya hemos hecho obtenemos un coste temporal O(lgn).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PROBLEMAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-9 Analiza el coste temporal asintotico de los algoritmos del ejercicio 2-2. 2-10 La siguiente funcion calcula el producto de dos matrices, A, de dimension pq, y B, de dimensionq r:

    1 def matrix product(A, B):2 p, q, r = len(A), len(A[0]), len(B[0])3 C = [ [0]*r for i in xrange(p) ]4 for i in xrange(p):5 for j in xrange(r):6 C[i][j] = sum( A[i][k]*B[k][j] for k in xrange(q) )7 return C

    Acota asintoticamente, superior e inferiormente, el coste temporal del algoritmo.. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

    2.8. Una comparacion de costes de diferentes clases de equiva-lencia

    Decimos que naive_sequential_search y sequential_search son lineales, y que binary_search es logartmi-co. Que implica, a efectos practicos, que el coste temporal sea logartmico, lineal, exponencial, etc.?Resulta interesante tener cierta intuicion sobre lo que supone clasificar un algoritmo por su coste temporalen el peor de los casos como perteneciente a un orden determinado. Lo mejor sera que comparemos algunasgraficas de crecimiento. Empecemos por los crecimientos sublineales y lineal, cuyas graficas se muestranen la figura 2.14.

    c0

    c1 logn

    c2 n

    c3 n

    n

    t

    Figura 2.14: Graficas de crecimientode funciones sublineales y lineal.

    A la vista de la figura, reflexionemos sobre el crecimiento de algunas funciones de coste y las implica-ciones que tienen al evaluar la eficiencia de un algoritmo:

    Un algoritmo de coste constante ejecuta un numero de instrucciones acotado por una constante inde-pendiente de la talla del problema.Un algoritmo que soluciona un problema en tiempo constante es lo ideal: a la larga es mejor quecualquier algoritmo de coste no constante.

    Apuntes de Algortmica 2-21

  • 2.8 Una comparacion de costes de diferentes clases de equivalencia 2004/09/24-15:52

    El coste de un algoritmo logartmico crece muy lentamente conforme n crece. Por ejemplo, si resolverun problema de talla n = 10 tarda 1 s, puede que tarde 2 s en resolver un problema 10 veces masgrande (n = 100) y, en tal caso, solo 3 s en resolver uno 100 veces mayor (n = 1000). Cada vez queel problema es 10 veces mas grande, se incrementa en 1 s el tiempo necesario (el factor exacto queincrementa el tiempo en un microsegundo depende de constantes como la base del logaritmo).Un algoritmo cuyo coste es (

    n) crece a un ritmo superior que otro que es (logn), pero no llega

    a presentar un crecimiento lineal. Cuando la talla se multiplica por 4, el coste se multiplica por 2.

    Analicemos ahora los crecimientos lineal y superlineales que se muestran graficamente en la figura 2.15.

    c3 n

    n

    t

    c4 n logn

    c5 n2

    c6 n3c7 2nc8 nn

    Figura 2.15: Graficas de crecimientode funciones lineal y superlineales.

    Un algoritmo (n logn) presenta un crecimiento del coste ligeramente superior al de un algoritmolineal. Por ejemplo, si tardamos 10s en resolver un problema de talla 1 000, tardaremos 22s, pocomas del doble, en resolver un problema de talla 2 000.

    Un algoritmo cuadratico empieza a dejar de ser util para tallas medias o grandes, pues pasar a tratarcon un problema el doble de grande requiere cuatro veces mas tiempo.

    Un algoritmo cubico solo es util para problemas pequenos: duplicar el tamano del problema haceque se tarde ocho veces mas tiempo.

    Un algoritmo exponencial raramente es util. Si resolver un problema de talla 10 requiere una cantidadde tiempo determinada con un algoritmo (2n), tratar con uno de talla 20 requiere unas 1 000 vecesmas tiempo!

    La tabla 2.3 tambien ayuda a tomar conciencia de las tasas de crecimiento. Supongamos que las instan-cias de un problema de talla n = 1 se resuelven con varios algoritmos constantes, lineales, etc. en 1s. Enla tabla se muestra el tiempo aproximado que cuesta resolver problemas con cada uno de ellos. El ultimonumero es barbaro: un eon son mil millones de anos y los cientficos estiman actualmente que la edad deluniverso es de entre 13 y 14 eones. No tiene sentido seguir estudiando como crece el coste de un algoritmoexponencial. En la tabla 2.4 se muestran tiempos con tallas de problema mayores para el resto de costes.

    Complejidad temp. n = 1 n = 5 n = 10 n = 50 n = 100Constante 1s 1s 1s 1s 1sLogartmico 1s 1.7s 2s 2.7s 3sLineal 1s 5s 10s 50s 100sn logn 1s 8.5s 11s 86s 201sCuadratico 1s 25s 100s 2.5ms 10msCubico 1s 125s 1ms 125ms 1sExponencial (2n) 1s 32s 1ms 35.75 anos 40106 eones

    Tabla 2.3: Tiempo de eje-cucion en funcion de n paraalgoritmos con diferentescostes.

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PROBLEMAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-11 Si la ejecucion de un programa requiere 1 s de tiempo para resolver una instancia de talla 100,cuanto tiempo requerira resolver uno de talla 1 000 en cada uno de estos casos?

    a) El algoritmo es lineal.

    2-22 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    Complejidad temp. n = 1000 n = 10000 n = 100000Constante 1s 1s 1sLogartmico 4s 5s 6sLineal 1ms 10ms 100msn logn 4ms 50ms 600msCuadratico 1s 1 minuto y 40s 2 horas y 46 minutosCubico 16.5 minutos 11.5 das casi 32 anos

    Tabla 2.4: Tiempo de eje-cucion en funcion de n paraalgoritmos con diferentescostes.

    b) El algoritmo es O(n lgn), siendo n la talla de la instancia.c) El algoritmo es cuadratico.d) El algoritmo es cubico.e) El algoritmo es O(2n). 2-12 Si resolver una instancia de talla 100 con determinado algoritmo requiere 1 , cual es la mayortalla que podemos resolver en un minuto en cada uno de estos casos?

    a) El algoritmo es lineal.b) El algoritmo es O(n lgn), siendo n la talla de la instancia.c) El algoritmo es cuadratico.d) El algoritmo es cubico.e) El algoritmo es O(2n).. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

    2.8.1. Algunos teoremas utiles

    Estos teoremas, ademas de los teoremas 2.1 y 2.3, resultan utiles para el calculo de costes de algoritmos:

    Teorema 2.4 Si c es una constante positiva, c (1).

    Teorema 2.5 Si k > 0,

    0inik (nk+1).

    Teorema 2.6 Si r > 1,

    0inri (rn).

    Teorema 2.7 n! (n(n/e)n).

    Teorema 2.8 1in

    1i (logn).

    Teorema 2.9 0in

    12i (1).

    Teorema 2.10 Si 0 x < 1,

    0inxi 1

    1 x .

    Teorema 2.11 Si 0 x < 1,

    0inixi x

    (1 x)2 .

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . PROBLEMAS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2-13 Acota asintotica y superiormente el tiempo de ejecucion de este algoritmo:

    Apuntes de Algortmica 2-23

  • 2.9 Expresiones del coste temporal 2004/09/24-15:52

    1 def sums(n):2 s = 0.03 for i in xrange(n):4 for j in xrange(i):5 s += 1/i6 return s

    . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

    2.9. Expresiones del coste temporal

    El coste de un algoritmo expresado en numero de pasos necesarios para resolver una instancia no siemprees una simple funcion de la talla de la instancia. Puede haber numerosas (incluso infinitas) instancias deuna misma talla y el algoritmo puede requerir la ejecucion de un numero de pasos que depende de los datosconcretos de cada instancia. No siempre podemos, pues, caracterizar el coste temporal con una simplefuncion de la talla de las instancias.

    2.9.1. Costes en el mejor y peor de los casos

    El coste en el peor de los casos es una funcion de n con el mayor numero de pasos necesarios para resolvercualquier instancia de talla n. Suele presentarse en terminos de su cota asintotica superior (notacion ((ordende))). El coste en el mejor de los casos es una funcion de n con el menor numero de pasos necesarios pararesolver cualquier instancia de talla n. Suele presentarse en terminos de su cota asintotica inferior (notacion((omega))).

    Al buscar un elemento en un vector con el metodo binario hemos visto que el coste en el peor de loscasos es proporcional a lgn. Podramos decir que el coste en el peor de los casos es (lgn), pero esfrecuente ofrecer unicamente su cota asintotica superior y decir que es O(lgn). El coste en el mejor de loscasos es constante. Solemos proporcionar unicamente su cota inferior asintotica y decimos que el coste enel mejor de los casos es (1).

    Si decimos de un algoritmo que su coste temporal es (1) y O(lgn) estamos diciendo que su coste en elmejor de los casos es constante y que en el peor de los casos esta acotado asintoticamente por una funcionlogartmica.

    Si la cota asintotica inferior del coste en el mejor de los casos coincide con la cota asintotica superiordel coste en el peor de los casos, se usa la notacion ((zeta)).

    2.9.2. Coste promedio

    El coste en el mejor y peor de los casos ofrece una informacion descriptiva del comportamiento del al-goritmo para valores crecientes de la talla de las instancias. Podemos ofrecer una descripcion mas ricasi presentamos, ademas, informacion sobre su comportamiento esperado. El coste temporal promedioCoste promedio: Average

    cost. es el tiempo de ejecucion promediado para todas las entradas posibles de una talla dada asumiendo unadistribucion de probabilidad para las instancias de igual talla.

    Si In es el conjunto de instancias de talla n y denotamos con t(I) el numero de pasos necesarios pararesolver la instancia I In y con Pr(I) denotamos la probabilidad de que ocurra la instancia I, el costetemporal promedio en funcion de n es

    IIn

    Pr(I)t(I).

    Volvamos al problema de la busqueda en un vector ordenado y supongamos que existe la misma prob-abilidad de que el elemento buscado haga que sequential_search se detenga en la i-esima iteracion, para ientre 1 y n. El numero promedio de pasos que comporta la ejecucion de sequential_search es

    3 1n+1

    + 1in

    3i 1n+1

    =3

    n+1+

    3n+1 1in i

    =3

    n+1+

    3n+1

    n (n+1)2

    =32

    n+3

    n+1.

    O sea, en promedio da, aproximadamente, la mitad de pasos que en el peor de los casos.

    2-24 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    Si la probabilidad de que busquemos uno u otro valor es diferente, el coste promedio puede ser tambiendiferente. Supongamos que la probabilidad de que busquemos un valor que hace que el algoritmo se detengaen la k-esima iteracion es Pr(k)= 1/2k, para k entre 1 y n, y que Pr(0)= 1/2n (notese que 0kn Pr(k)= 1).El numero promedio de pasos en funcion de n es, en ese caso,

    3 12n

    + 1in

    3i 12i

    = 3 12n

    +3 1in

    i2i

    = 3(

    12n

    + 1in

    i2i

    )= 6.

    El numero medio de pasos es, pues, una cantidad constante.Como se puede comprobar, el analisis del coste temporal promedio resulta generalmente mas trabajoso

    que el del coste en el mejor o peor de los casos y es dependiente de las asunciones que hagamos sobrela distribucion de las instancias (y no siempre es facil averiguar la distribucion de los datos reales en laexplotacion del algoritmo).

    2.9.3. Coste amortizado

    En algunos casos, la estimacion de coste para el peor de los casos de una determinada operacion resultaexcesivamente pesimista, pues es posible que las circunstancias que hacen posible el elevado coste seanraras y alcanzables unicamente tras efectuar ciertas operaciones previas. Cuando esto ocurra, presentare-mos analisis de coste de otra naturaleza: el denominado coste amortizado, que es el coste por operacion Coste amortizado:

    Amortized cost.efectuada cuando se lleva a cabo una serie determinada de operaciones consecutivas.No se debe confundir coste amortizado con el coste promedio: el coste amortizado es el coste total de

    una serie de operaciones (en el peor de los casos) dividido por el numero de operaciones efectuadas, sinasumir ninguna distribucion de probabilidad sobre las instancias de cada talla.

    Un contador binario

    Consideremos un ejemplo: una rutina para incrementar un contador codificado en binario natural expresadocomo una secuencia de ceros y unos en un vector de talla n. Un vector como [0,0,0,1,0,0,1,1], porejemplo, representa el numero binario 10011. El numero que resulta de sumarle uno es 10100, que serepresenta con [0,0,0,1,0,1,0,0]. Esta rutina Python efectua el incremento del contador (sin tener encuenta posibles desbordamientos):

    1 def increment(counter):2 i = len(counter)-13 while i >= 0 and counter[i] == 1:4 counter[i] = 05 i -= 16 if i >= 0:7 counter[i] = 1

    El coste en el peor de los casos es O(n): si todo el vector contiene n unos, el bucle lo recorre por completoy modifica todos sus bits.

    Que ocurre si efectuamos n incrementos consecutivos sobre un contador que empieza con valor cero?Parece logico pensar que el coste temporal total sera O(n2). Sin embargo, efectuar esas n operacionesrequiere, en total, tiempo O(n) y no O(n2). Como es posible? Notese que el bit menos significativocambia de valor con cada llamada a increment, pero el segundo bit menos significativo solo cambia de valoruna de cada dos llamadas; el tercer bit solo lo hace una de cada cuatro llamadas; y as sucesivamente. Engeneral, el bit i-esimo (de derecha a izquierda) solo cambia su valor n/2i veces cuando efectuamos nincrementos seguidos sobre un contador originalmente nulo. El bit i-esimo para i mayor que blgnc no semodifica nunca, as que el coste de los n incrementos es

    0iblgnc

    n2i< n

    i0

    12i

    = 2n,

    o sea, O(n).Si el coste de n incrementos es 2n, el coste promedio de cada uno es O(1). Decimos que el coste

    amortizado de n incrementos sobre un contador inicialmente nulo es constante.

    Apuntes de Algortmica 2-25

  • 2.10 Coste espacial 2004/09/24-15:52

    Redimensionamiento de vectores con holgura

    Consideremos un ejemplo diferente: un vector dinamico que debe redimensionarse para anadir por el final,uno a uno, m elementos.

    Planteemonos que ocurre al efectuar una sola adicion en un vector con n elementos. Es una operacionque requiere reservar n+ 1 celdas de memoria, copiar el valor de la n celdas originales, copiar al final elnuevo valor y liberar la memoria en la que residan los n valores originales. Se trata, pues, de una operacionejecutable en tiempo O(n) y efectuar m adiciones requiere tiempo O(m(m+n)) = O(m2 +mn).

    Es posible reducir el coste temporal a solo O(m + n) si ((malgastamos)) algo de memoria. Cuandonecesitemos que el vector almacene n elementos, reservaremos memoria para una cantidad de celdas, N,mayor o igual que n. El valor N sera la capacidad del vector, frente a n, que es su tamano. Si nos pidenanadir una celda al vector y el tamano es menor que la capacidad, la adicion se puede ejecutar en tiempoO(1). Si la capacidad es igual al tamano, habra que aumentar antes la capacidad del vector en un cantidadfija o proporcional a la capacidad actual.

    Si el aumento de capacidad se produce multiplicando por 2 la capacidad anterior, estaremos efectuandouna operacion de coste temporal O(2N), pero las siguientes N adiciones tendran un coste temporal O(1).Si consideramos las N operaciones de adicion consecutivas en su conjunto, su coste total sera O(3N),que dividido por N arroja una cantidad de tiempo constante por operacion. Esta argumentacion puedeextenderse a la adicion de m elementos a un vector de tamano n, con un coste temporal total O(n+m). Siel tamano original, n, es por ejemplo 0 o 1, el coste de amortizado de cada una de las m adiciones es O(1).

    Puede parecer que la memoria desperdiciada es muy grande, pero nunca es mas del doble que el tamanodel vector, una cantidad proporcional a dicho tamano.

    Esta tecnica se conoce como doblado, pero no es necesario que la constante por la que multiplicamosDoblado: Doubling.la capacidad sea igual a 2: cualquier constante mayor que 1 vale.

    Supongamos que cada vez que rebasamos la capacidad de un vector se solicita memoria para cN nuevoselementos, siendo c > 1 y N la capacidad actual. Si efectuamos m adiciones al vector cuando su capacidady tamano es 1, se producira una serie de reservas de memoria que haran que la capacidad del vector siga lasucesion c0, c, c2, . . . , ck, hasta que ck1 < m ck.

    De las m operaciones de adicion, m k tendran coste temporal O(1) y las restantes k operacionestendran, en total, un coste temporal del orden de

    0ik

    ci =ck+11

    c1 .

    Si consideramos (el orden de) la suma del coste de la m k operaciones de coste constante con el de lasrestantes k operaciones, tenemos

    O(

    m k+ ck+11c1

    )= O

    (ck k+ c

    k+11c1

    )= O(ck).

    El numero total de operaciones, m, esta comprendido entre ck1 y ck. El coste amortizado de cada unade ellas es, pues, constante.

    2.10. Coste espacial

    El coste espacial de un algoritmo es la cantidad de memoria que necesita para resolver instancias de unproblema en funcion de la talla. La memoria que consume un algoritmo, al implementarse con un programay ejecutarse sobre un ordenador, puede dividirse en diferentes categoras:

    Una parte de tamano fijo: El espacio necesario para el codigo del programa. El espacio necesario para almacenar variables simples y constantes.

    Y una parte de tamano variable:

    El espacio necesario para almacenar estructuras de datos complejas (vectores, matrices, etc),que puede depender de la talla del problema.

    El espacio necesario para la pila de llamadas a funcion. Las variables locales suelen residir endicha pila, aunque ello solo resulta realmente necesario en el caso de los programas recursivos.El numero de tramas de activacion en la pila depende de la talla del problema en el caso de losprogramas recursivos.

    2-26 Apuntes de Algortmica

  • c 2003, 2004 A. Marzal, M.J. Castro y P. Aibar 2 Analisis de algoritmos

    Dado que nos basta con efectuar analisis de coste espacial asintoticos, nos centraremos en la estimacionde la parte variable, es decir, la cantidad de memoria que depende de la talla del problema. No tendremosen cuenta el espacio necesario para especificar la instancia del problema, solo las variables que hemos deutilizar para resolverla. Por otra parte, nos limitaremos a contar el numero de celdas basicas de memorianecesarias, es decir, cada variable escalar o elemento simple de vector contara como una sola celda, sintener en cuenta el numero mnimo de bits con el que podramos representar los valores almacenados enellas.

    Consideremos el metodo naive_sequential_search: usamos dos variables auxiliares, i e index y la fun-cion no llama a ninguna otra (ni a s misma), as que el coste espacial es constante o, en notacion asintotica(1). Lo mismo ocurre con sequential_search y binary_search: el coste espacial es (1).

    Analicemos recursive_binary_search, que es un metodo recursivo. El numero de variables escalareslocales utilizadas es constante, pero el metodo es recursivo y cada activacion de la funcion comporta lareserva de memoria en la pila de llamadas a funcion. Cada una de las activaciones reserva tanto espaciocomo requieran las variables locales (mas una cantidad de memoria de tamano fijo). El numero de llamadassimultaneas en pila es menor o igual que 1+ blgnc, as que el espacio es (1) y O(lgn).

    Cabe hacer una observacion importante acerca del coste espacial: siempre esta acotado superiormentepor el coste temporal. Si un algoritmo necesita usar cierta cantidad de celdas de memoria, tendra que visitarcada una de las celdas de memoria al menos una vez (de lo contrario, no las necesitara).

    2.11. Complejidad de problemas

    Hasta el momento hemos considerado unicamente el coste temporal de algoritmos concretos. Un mismoproblema, como el de busqueda en un vector ordenado, puede resolverse en tiempo O(n) u O(lgn). EsO(lgn) la menor cota superior al coste temporal con el que podemos resolver ese problema? Observese queformulamos la pregunta del coste en relacion al problema, no a un algoritmo. El campo de estudio que seocupa de la dificultad intrnseca de los problemas recibe el nombre de complejidad de problemas. Resultainteresante saber si hay lmites al coste temporal con el que podemos resolver un problema determinado,pues permite saber cuando hemos disenado un algoritmo optimo, es decir, cuando tenemos la garanta deque no se puede disenar un algoritmo mas eficiente.

    Tomemos por caso el problema de la busqueda en un vector ordenado. Un resultado importante nosasegura que ningun algoritmo basado en la comparacion de los elementos con el valor buscado puederesolver el problema en tiempo con una cota superior asintotica inferior a O(lgn). De acuerdo con ello,el metodo binary_search (o su version recursiva) es un algoritmo optimo. La demostracion se basa en elestudio de los arboles de decision para busqueda.

    Un arbol de decision para un vector de tamano n es un arbol binario cuyas hojas estan etiquetadascon numeros entre 0 y n 1 (los ndices del vector). El arbol de decision asociado a un algoritmo es unarepresentacion formal de las comparaciones que este efectua y sigue estas reglas:

    La raz esta etiquetada con el ndice de la primera entrada con la que se compara el valor buscado.

    Si la etiqueta de un nodo es i, su hijo izquierdo esta etiquetado con el ndice del siguiente elementocon que se efectuara una comparacion cuando el valor buscado es menor que a[i], y el hijo derecho,con el ndice del elemento que compararemos a continuacion si el valor buscado es mayor que a[i].

    La figura 2.16 muestra los arboles de decision asociados a los algoritmos sequential_search y bina-ry_search para vectores de 8 elementos.

    El numero de comparaciones efectuadas al buscar el valor que ocupa una determinada posicion es lalongitud del camino entre la raz y el nodo etiquetado con el ndice de dicha posicion. La profundidad delarbol es la mayor cantidad de comparaciones que debe efectuar el algoritmo. Como cada nodo tiene a losumo dos hijos, el numero de nodos a distancia de la raz menor o igual que k es 1+ 2+ + 2k1. Sitenemos n nodos, la profundidad mnima de un arbol de decision sera 1+blgnc. As pues, siempre hay unaruta de la raz a algun nodo de longitud 1+ blgnc.

    2.11.1. Problemas difciles

    La complejidad de ciertos problemas es exponencial. Tomemos por caso la enumeracion de todas la per-mutaciones de los elementos de un vector de tamano n. Hay n! permutaciones y mostrarlas (o almacenarlas)requiere tiempo n n!. La aproximacion de Stirling indica que n! 2pin( n

    e

    )n, es decir, el coste temporal

    de la enumeracion es O(n

    2pin(

    ne

    )n).Apuntes de Algortmica 2-27

  • 2.11 Complejidad de problemas 2004/09/24-15:52

    0

    1

    2

    3

    4

    5

    6

    7 0

    1

    2

    3

    4

    5

    6

    7

    (a) (b)

    Figura 2.16: Arboles de decision asociadosal algoritmo de busqueda secuencial (a) y alalgoritmo de busqueda binaria (b).

    Resulta prohibitivo utilizar un algoritmo que requiere tiempo exponencial cuando la talla del problemaes moderada o grande. Un problema que solo se puede resolver en tiempo exponencial es un problemaintratable.

    2.11.2. Problemas presumiblemente difciles

    Hay una serie de problemas que resultan de interes y para los que hay unos resultados curiosos. Nadiesabe resolverlos en tiempo polinomico y nadie ha demostrado que requieran tiempo exponencial. Pero sinos dan una solucion, podemos comprobar, en tiempo polinomico, si es o no es valida. Esto implica ques sabramos resolverlos en tiempo polinomico en un ordenador no determinista, es decir, un ordenadorcapaz de llevar en paralelo una cantidad de calculos arbitrariamente grande: bastara con generar todaposible solucion en uno de los hilos de ejecucion paralela y detenernos cuando uno de ellos compruebeque ha alcanzado una solucion. No es posible construir un computador capaz de semejante proeza, perotiene interes considerar este tipo de dispositivos formales desde un punto de vista teorico. La familia delos problemas con esta caracterstica se conoce por NP, siglas del termino ingles ((Non-deterministicallyPolynomial)). Estudiaremos con cierto detalle esta familia en el ultimo captulo.

    El estudio de la familia NP centra su interes sobre cierto tipo de problemas: los denominados proble-mas de decision. Se trata de problemas cuya solucion es siempre ((s)) o ((no)). Dentro de la familia NPhay un subconjunto especial de problemas de decision: la familia de los problemas NP-completos. Losproblemas NP-completos son aquellos problemas NP tan difciles (en un sentido formal) como cualquierotro problema de NP y son particularmente interesantes porque si se consiguiera resolver uno de ellos entiempo polinomico, todos los problemas de NP se resolveran tambien en tiempo polinomico.

    Baste decir, de momento, que cuando clasifiquemos un problema como NP estaremos emitiendo unjuicio sobre la dificultad presumida del problema. Si lo clasificamos como NP-Completo, nuestra presun-cion de dificultad sera mas firme.

    Aun hay otra familia de problemas de interes atendiendo a su dificultad: los problemas NP-Difciles,que son aquellos tan difciles, al menos, como los NP-completos, pero de los que no se ha demostrado queNP-difcil: NP-Hard.sean NP.

    2.11.3. Problemas irresolubles

    Ciertos problemas ni siquiera son resolubles: los problemas indecidibles. El primer prob