2 de mayo de 2018

La O mayúscula

La notación de la \(O\) mayúscula (big O notation) es una construcción matemática que nos permite, a grandes rasgos, describir el comportamiento de una función en su limite.

  • Es miembro de la familia de notaciones conocidas como notaciones de Bachmann-Landau o notaciones asintóticas.

  • Otros miembros de la familia de notaciones asintóticas son las notaciones \(o\), \(\Omega\) y \(\theta\).

¿Que es una asíntota?

En geometría analítica, una asíntota es una linea que delimita el comportamiento de una función en su límite.

  • Sean \(f(x)\) una función y \(l(x)\) una linea. \(l\) es una asíntota de \(f\) si \[\lim_{x\to\infty} d(f(x),l(x)) = 0,\] donde \(d\) es la distancia entre dos puntos.

Esto es, la distancia entre la función y la linea delimitadora tienden a cero.

  • Otras propiedades, como el que las lineas no se crucen e incluso que l sea una linea no son importantes para nosotros.

Definición de \(O\)

Formalmente, \(O\) se define como:

Sean \(f\) y \(g\) dos funciones con dominio en los números reales positivos. Tenemos que \[f(x)=O(g(x)),\] o de forma alternativa \[f(x) \in O(g(x)),\] si y sólo si

\[\limsup_{x\to\infty} \left| \frac{f(x)}{g(x)} \right| < \infty.\]

Definición de \(O\)

Una caracterización equivalente de \(O\) es:

\(f(x)=O(g(x))\) si y sólo si:

  • Existe un real positivo \(M\) y existe \(x_0\) en el dominio de \(f\) y \(g\) tales que \(x \geq x_0\) implica que \[|f(x)| \leq Mg(x).\]
   
Si \(f(x)=O(g(x))\) decimos que \(f\) es del orden de \(g\).

Ejemplos:

Sean \(f(x)=3x + 10\) y \(g(x)=x\). ¿Es \(f\) del orden de \(g\)?

Sea \(M=10\), notemos que \[f(x) = 3x + 10\leq 10x = 10g(x)\] para toda \(x \geq 2\): \(f(2)=16\) y \(10g(2)=20\).

Sigue que \(f(2)\leq 10g(2)\) y la desigualdad crece con \(x\).

Método dos:

\[\limsup_{x\to\infty} \left| \frac{f(x)}{g(x)} \right| =\lim_{x\to\infty} \left| \frac{ 3x + 10}{x} \right| = 13 < \infty.\]

Por lo tanto, \(3x+10\) es del orden de \(x\).

   
En general, todas las funciones lineales son del orden de \(x\).

Ejemplo.

Sean \(f(x)=x^2+50x+1\) y \(g(x)=x^3-100\).

¿Es \(f\) del orden de \(g\)?

Método dos:

\[\limsup_{x\to\infty} \left| \frac{f(x)}{g(x)} \right| = \lim_ {x\to\infty} \left| \frac{x^2+50x+1}{x^3-10}\right| \]

Método dos:

Por L'Hospital:

\[\lim_{x\to\infty} \left| \frac{f(x)}{g(x)} \right| = \lim_{x\to\infty} \left| \frac{f'(x)}{g'(x)} \right|,\] de donde

\[ \lim_{x\to\infty} \left| \frac{f'(x)}{g'(x)} \right| = \lim_{x\to\infty} \left| \frac{2x+50}{3x^2} \right| =\lim_{x\to\infty} \left| \frac{2}{6x} \right|=0 < \infty.\]

Por lo tanto, \(x^2+50x+1= O(x^3-100)\).

Ejemplo parte 3:

   
En general, todas las funciones polinomiales son del orden del orden del sumando con el exponente más alto.

Ejemplo.

Sean \(f(x)=x^{100}\) y \(g(x)=1.00001^{x}\).

¿Es \(f\) del orden de \(g\)?

\[ \lim_{x\to\infty} \left| \frac{f'(x)}{g'(x)} \right| = \lim_{x\to\infty} \left| \frac{100 x^{99}}{9.99995\times{}10^{-6}\times 1.00001^x} \right|\]

y, en general, para 99 derivadas vamos a tener:

\[ \lim_{x\to\infty} \left| \frac{f(x)}{g(x)} \right| = \lim_{x\to\infty} \left| \frac{\alpha\beta x^{\beta-1}}{9.99995\times{}10^{-6}\gamma 1.00001^x} \right|,\] con \(\beta\) decreciente y eventualmente 0. Sigue que el limite es 0.

Sigue que \(f\) es del orden de \(g\).

Ejemplo parte 2:

   
En general, todas las funciones polinomiales son del orden de cualquier exponencial.

Ejemplo

Sean \(f(x)=x^{2}+5000\) y \(g(x)={x^2}-5000\).

¿Es \(f\) del orden de \(g\)?

\[ \lim_{x\to\infty} \left| \frac{f(x)}{g(x)} \right| =\left| \frac{f'(x)}{g'(x)} \right| =\left| \frac{2x}{2x} \right| = 1 < \infty.\] De donde \(f\) es del orden de \(g\).

Ejemplo

Sean \(f(x)=10^{100}x^{3}\) y \(g(x)=0.0001{x^3}\).

¿Es \(f\) del orden de \(g\)?

Ejemplo

Sean \(f(x)=10^{100}x^{3}\) y \(g(x)=0.0001{x^3}\).

¿Es \(f\) del orden de \(g\)?

\[ \lim_{x\to\infty} \left| \frac{f(x)}{g(x)} \right| = \left| \frac{10^{100}{x^3}}{0.0001x^{3}} \right| = \frac{10^{100}}{0.0001} < \infty.\]

Por lo tanto \(f\) es del orden de \(g\).

Ejemplo

Sean \(f(x)= \log_2 x\) y \(g(x)= \log_{10}x\).

¿Es \(f\) del orden de \(g\)?

Ejemplo

Sean \(f(x)= \log_2 x\) y \(g(x)= \log_{10}x\).

¿Es \(f\) del orden de \(g\)?

Notemos que: \[\begin{align} 2^{\log_2 x} &= x\\ log_{10}(2^{\log_2 x}) &= \log_{10} x\\ (\log_2 x) \log_{10}(2) &= \log_{10} x\\ \end{align}\]

de donde para toda x > 0: \[(\log_2 x) \leq \frac{1}{\log_{10}(2)} \log_{10} x.\]

Ejemplo

Sean \(f(x)= 2.1^x\) y \(g(x)= 2^x\).

¿Es \(f\) del orden de \(g\)?

Ejemplo

Sean \(f(x)= 2.1^x\) y \(g(x)= 2^x\).

¿Es \(f\) del orden de \(g\)?

\[ \lim_{x\to\infty} \left| \frac{f(x)}{g(x)} \right| = \lim_{x\to\infty} \left| \frac{2.1^x}{2^x} \right| = \lim_{x\to\infty} \left(\frac{2.1}{2} \right)^x = \infty.\]

Por lo tanto \(f\) no es del orden de \(g\).

Rapidez

Si \(f(n) = O(g(n))\) pero \(g(n) \neq O(f(n))\) entonces decimos que \(g\) es de mayor orden (crece más rápido) que \(f\).

Si \(f(n) = O(g(n))\) y \(g(n) = O(f(n))\) entonces decimos que \(g\) y \(f\) tienen el mismo orden.

Ejemplos:

¿Cuál de las siguientes funciones es de mayor orden?

  • \(f(n) = n^2\) ó \(g(n) = n\sin n\).
  • \(f(n) = n\) ó \(g(n) = n(\cos (n^2)+2)\).
  • \(f(n) = n \log n\) ó \(g(n) = n\).
  • \(f(n)= n^{\log n}\) ó \(g(n) = n^{n/3}\).

Propiedades

  • Si \(f\) puede ser escrita como una suma finita de otras funciones, aquella que crece más rápido determina el orden de \(f(n)\).

    • Por ejemplo: \(f(n)=n^{\log n} + n^3 + 100 n^2 + n^{x/3}\) es de orden …

Propiedades

  • Si \(f\) puede ser escrita como una suma finita de otras funciones, aquella que crece más rápido determina el orden de \(f(n)\).

    • Por ejemplo: \(f(n)=n^{\log n} + n^3 + 100 n^2 + n^{n/3}\) es de orden \(O(n^{x/3})\).
  • Producto: Si \(f_1(n) = O(g_1(n))\) y \(f_2(n) = O(g_2(n))\) entonces \[f_1(n)f_2(n) = O(g_1(n)g_2(n)).\]

  • Por ejemplo: \(O(2n\times3n^2) = O(6n^3)\).

Propiedades

  • Suma: Si \(f_1(n) = O(g_1(n))\) y \(f_2(n) = O(g_2(n))\) entonces \[f_1(n)+f_2(n) = O(g_1(n)+g_2(n)).\]

  • Por ejemplo: \(O(2n+3n^2) = O(3n^2)\).

  • Si \(C\) es una constante diferente de 0 entonces, para toda función \(f(n)\), \[O(Cf(n))=O(f(n))\] y \[O(f(n)+C)=O(f(n)).\]

  • Por ejemplo: \(O(3n^2) = O(n^2)\).

Propiedades

  • Para toda constante función constante \(f(n) = C\), \(C>0\), tenemos que \(f(n) = O(1)\).

    • La clase de funciones \(O(1)\) se le llama la clase de funciones constantes.
  • No importa la base de los logaritmos: Sean \(f_1(n) = \log_{b_1}\) y \(f_1(n) = \log_{b_2}\), con \(b_1,b_2>1\), entonces \(f_1(n)\) y \(f_2(n)\) son del mismo orden.

    • Sea \(g(n)\) tal que \(0 < \lim_{n\to\infty} g(n) < \infty\), entonces, para toda \(f(n)\) no acotada: \[O(g(n)f(n))=O(g(n)+f(n))=O(f(n)).\]

Propiedades

  • Reflexividad: \(f(n) = O(f(n))\)
  • Transitividad: Si \(f(n) = O(g(n))\) y \(g(n)=O(h(n))\) entonces \(f(n)=O(h(n))\).
   
La notación nos define una clase jerárquica sobre la el orden de crecimiento de todas las funciones positivas.

Machine with Concrete

"Machine with Concrete" por Arthur Ganson, http://arthurganson.com/project/machine-with-concrete/

Construida en 1992, el motor en el lado izquierdo gira a 200rpm mientras que el último engrane esta esta pegado a un bloque de concreto. Cada uno de los 12 engranes disminuye la velocidad de rotación en \(\frac{1}{50}\).

¿Porqué no se a roto esta máquina?

Solución.

La velocidad de rotación del último engrane es de \[200\times{} \left( \frac{1}{50} \right) ^{12}\] rotaciones por minuto. Lo anterior equivale a una rotación cada \(4.306\times 10^{-13}\) años.

En otras palabras, ¡el último engrane realiza poco más de 4 rotaciones cada un 10 trillones de años!

Ordenes de Complejidad.

Sea \(c\) una constante mayor a 1.

Notación Nombre
\(O(1)\) orden constante
\(O(\log \log n)\) orden sublogarítmico
\(O(\log n)\) orden logarítmico
\(O(\sqrt{n})\) orden sublineal
\(O(n)\) orden lineal o de primer orden
\(O(n \log^* n)\) \(n\) log-estrella \(n\)

Ordenes de Complejidad.

Notación Nombre
\(O(n \log n)\) orden lineal logarítmico o quasilineal
\(O(n^2)\) orden cuadrático o de segundo orden
\(O(n^3)\) orden cúbica o de tercer orden, etc.
\(O(n^c)\) orden polinomial o algebraico
\(O(c^n)\) orden exponencial
\(O(n!)\) orden factorial

Tiempo y espacio de ejecución de programas.

¿Porqué la notación O?

Un parámetro primordial durante el desarrollo de proyectos de software es el poder controlar los recursos computacionales que un programa o sistema va ha utilizar durante su ejecución.

Para el análisis de algoritmos y la teoría de la complejidad computacional, los recursos más importantes son el tiempo y la memoria requeridas para la ejecución de un programa.

La notación \(O\) es usada para la clasificación de algoritmos ya que esta es robusta (depende poco) con respecto a diferencias de implementación así como del hardware disponible:

En general, los ordenes de ejecución se mantienen bajo diferencias en lenguaje de programación, habilidad del programador y diferencias de hardware.

¿Porqué la notación O?

La variable \(n\) representa el tamaño de la entrada tanto en bits, bytes, palabras o números de elementos de un problema de tamaño acotado, no importa cuál se use ya que, por la propiedad multiplicativa, el orden no va ha cambiar.

Por ejemplo:

Para un algoritmo de ordenación como merge sort, \(n\) es el número de elementos a ordenar ya que, normalmente, estos van a ser de un tamaño fijo en bits.

Tiempo y espacio de ejecución de programas.

En general, en el análisis de algoritmos y la teoría de la complejidad vamos a considerar los recursos usados por un algoritmo en tres casos distintos:

  • Mejor caso (best case).
  • Peor caso (worst case).
  • Caso promedio, medio o esperado (average case).

De estos, los últimos dos son considerados los más importantes.

Mejor Caso

El mejor caso es el desempeño de un algoritmo en condiciones ideales.

  • Este caso es poco usado para medir el desempeño de algoritmos.
  • En general, todo algoritmo se puede modificar para tener desempeño excelente en un número finito de entradas.
  • Por lo anterior, se considera que la útilidad de este caso es casi nula.

Peor Caso y Caso Promedio.

En términos informales, el caso promedio es aquel que se espera encontrar en la mayoría de las veces.

  • Formalmente, es el orden del tiempo o espacio de ejecución requerido durante la ejecución de un algoritmo en función de su entrada.

En términos informales, es el peor caso se da en las entradas donde el algoritmo tiene el desempeño más lento.

  • Formalmente, es el supremo del orden del tiempo o espacio requeridos para la ejecución del algoritmo en un subconjunto infinito de posibles entradas.

Peor Caso y Caso Promedio.

  • El determinar estos casos no es trivial y frecuentemente requiere conocimientos avanzados de análisis de algoritmos y probabilidad.

  • Son medidas estándares en la industria.

  • Normalmente se requiere dar la información para ambos casos cuando se especifica el desempeño de un algoritmo.

  • Es muy recomendable establecer que tan frecuentemente se esperan los peores casos.

  • Es muy controversial el preferir un caso sobre el otro para determinar el desempeño de un algoritmo.

Ejemplos:

Insertion Sort

def insertionSort_time(n_lista):

    for index in range(1,len(n_lista)):
        actual = n_lista[index]
        posicion = index
        
        while posicion>0 and n_lista[posicion-1]>actual:
            n_lista[posicion]=n_lista[posicion-1]
            posicion = posicion-1           
        n_lista[posicion]=actual
        
    return n_lista

Insertion Sort

Tiempo:

  • Mejor caso: \(O(n)\)
  • Peor caso: \(O(n^2)\)
  • Caso promedio: \(O(n^2)\)

Espacio:

  • \(O(n)\)

Quick Sort

def quicksort_time(lista):
    quicksort_aux(lista,0,len(lista)-1)
    
def quicksort_aux(lista,inicio, fin):
    if inicio < fin:

        pivote = particion_time(lista,inicio,fin)

        quicksort_aux(lista, inicio, pivote-1)
        quicksort_aux(lista, pivote+1, fin)

def particion_time(lista, inicio, fin):
    pivote = lista[inicio]
    izquierda = inicio+1
    derecha = fin
    bandera = False

Quicksort

    while not bandera:
        while izquierda <= derecha and lista[izquierda] <= pivote:
            izquierda = izquierda + 1
        while lista[derecha] >= pivote and derecha >=izquierda:
            derecha = derecha -1
        if derecha < izquierda:
            bandera= True
        else:
            # swap places
            temp=lista[izquierda]
            lista[izquierda]=lista[derecha]
            lista[derecha]=temp
    # swap inicio with lista[derecha]
    temp=lista[inicio]
    lista[inicio]=lista[derecha]
    lista[derecha]=temp
    return derecha

Quicksort

Tiempo:

  • Mejor caso: \(O(n \log n)\)
    • \(O(n)\) con optimización natural.
  • Peor caso: \(O(n^2)\)
  • Caso promedio: \(O((n \log n)\)

Espacio:

  • \(O(n)\)
  • \(O(\log n)\) de espacio auxiliar.

Merge Sort

def merge(ls, rs, acc):
    if not ls:
        acc.extend(rs)
        return acc
    if not rs:
        acc.extend(ls)
        return acc
    l = ls[0]
    r = rs[0]
    if l < r :
        ls.pop(0)
        acc.append(l)
        return   (merge(ls, rs, acc))
    else:
        rs.pop(0)
        acc.append(r)
    return (merge(ls, rs, acc))

Merge Sort

def mergeSort(lst):
    
    if len(lst)==1 :
        return lst
    
    ls = lst[:len(lst)/2]
    rs = lst[len(lst)/2:]
    
    return(merge (mergeSort(ls),mergeSort(rs),[]))

Merge Sort

Tiempo:

  • Mejor caso: \(O(n \log n)\),
    • \(O(n)\) con optimización natural.
  • Peor caso: \(O(n \log n)\)
  • Caso promedio: \(O(n \log n)\)

Espacio:

  • \(O(n)\)

Análisis Empírico de Rendimiento.

Recuerden la práctica 12:

Desempeño sobre listas aleatorias

Análisis Empírico de Rendimiento.

Recuerden la práctica 12:

Desempeño sobre listas ya ordenadas

Análisis Empírico de Rendimiento.

Pasos para un Análisis Empírico

1.- Encontrar las funciones y herramientas que el lenguaje y el sistema operativo ofrecen para medir el uso de recursos de un programa y el como usarlos.

  • Por ejemplo, la función time() en Python nos da el tiempo actual del sistema en segundos (y fracciones) como un número flotante. Al realizar el siguiente código:
tiempoIni = time()
# Proceso
print(time() - tiempoIni)

Imprimimos en pantalla cuanto tardo, aproximadamente, en ejecutarse el código entre las líneas tiempoIni = time() y print(time() - tiempoIni).

Pasos para un Análisis Empírico

2.- Obtener una muestra d entradas de distintos tipos y tamaños.

3.- Analizar los resultados.

Midiendo el tiempo de ejecución de un algoritmo de forma analitica.

El tiempo de ejecución se puede medir contando el número de accesos a memoria.

Sin embargo, es más común medir por pasos del programa: (Fuente: http://slideplayer.es/slide/3121639/)

  • Los comentarios y las instrucciones de declaración no son pasos de programa.
  • Las expresiones y las instrucciones de asignación representan un paso de programa.
  • Si las expresiones contienen llamadas a funciones al número de pasos se le suma al número de pasos de la función.
  • Si la llamada a la función tiene paso de parámetros por valor hay que tener en cuenta las asignaciones a estos parámetros.

Midiendo el tiempo de ejecución de un algoritmo.

  • Si la función es recursiva deben considerarse también las variables locales, ya que deben ser almacenadas en la pila del sistema.
  • Si las expresiones manejan tipos compuestos (por ejemplo arreglos) el número de pasos depende del tamaño de los datos.
  • Las instrucciones de iteración.
  • La ejecución de la parte de control cuenta como el número de pasos de la expresión que debe ser evaluada.
  • El numero de pasos necesarios para ejecutar el cuerpo del ciclo debe multiplicarse por el número de veces que se ejecuta el ciclo.

Midiendo el tiempo de ejecución de un algoritmo.

  • La ejecución de la parte de control (if, else) cuenta como el número de pasos de la expresión que debe ser evaluada.
  • La instrucción de retorno return cuenta como el número de pasos de la expresión que debe ser evaluada.

Ejemplo.

Sea \(n\) la longitud de la lista lista:

Linea Pasos Accesos a memoria Frecuencia Total
def sumatoria(lista):
   s = 0 1 1 1 1
   for i in lista: 1 2 \(n\) \(n\)
        s = i + s 1 3 \(n\) \(n\)
return s 1 1 1 1

  Total Pasos = \(2n+2\)

Ejemplo.

Sea \(n\) la longitud de la lista lista:

Linea Pasos Frecuencia Total
def dobleSumatoria(lista):
   s = 0 1 1 1
   for i in lista: 1 \(n\) \(n\)
        for i in lista: 1 \(n^2\) \(n^2\)
            s = i + s 1 \(n^2\) \(n^2\)
return s 1 1 1

  Total Pasos = \(2n^2 + n + 2\)

Compensación espacio y tiempo en los algoritmos

Es muchos casos, es posible el disminuir el tiempo de ejecución de un algoritmo al aumentar el espacio usado y viceversa. Este fenómeno se conoce como space-time o time-memory trade-off, compensación espacio y tiempo.

Los casos más comunes son:

  • Código compacto o loop unrolling.

  • Uso de tablas de búsqueda o recalcular los datos

  • Uso de datos comprimido o sin compresión.

  • Reproducción de imagines en tiempo real o almacenado.

Código compacto o loop unrolling

Loop unrolling, desenrollado de ciclos, es el acto de expresar una operación recursiva linea por linea en lugar de usar un ciclo o una recursión.

  • Por ejemplo, en lugar de usar un ciclo for desde o hasta \(n-1\) usamos \(n \times m\) lineas de código expresando cada operación \(n\) veces.

  • Esa una opción de optimización muy común para los compiladores.

  • El usar un ciclo es más lento ya que tenemos que hacer todas las operaciones de comparación y asignación.

  • El usar un ciclo desenrollado usa más memoria ya que el programa en sí es más largo.

Tablas de búsqueda

  • Una tabla de búsqueda (lookup table) es una estructura de acceso aleatorio, comúnmente un arreglo o diccionario, en donde guardamos resultados de cómputo en lugar de calcularlos.
  • Al usar una tabla podemos acelerar la ejecución de ciertos procesos al reutilizar resultados previamente obtenido.
  • El usar esta tabla requiere de memoria extra.
  • Visto por otro lado, podemos modificar un algoritmo que usa tablas para que estas se generen en el momento, requiriendo menos memoria pero a costo de mayor tiempo de cómputo.

  • Esta es una instancia de las técnicas conocidas como Memoización.

Ejemplo:

def fibonacci_recursivo(numero):
    if numero == 1:     #Caso base 
        return 0
    if numero == 2 or numero == 3:
        return 1
    return fibonacci_recursivo(numero-1) + fibonacci_recursivo(numero-2)

Ejemplo:

memoria = {1:0, 2:1, 3:1}

def fibonacci_memo(numero):
  #Si el número ya se encuentra calculado, 
  #se regresa el valor ya ya no se hacen más cálculos
    if numero in memoria:     
        return memoria[numero]
    memoria[numero] = fibonacci_memo(numero-1) 
        + fibonacci_memo(numero-2)
    return memoria[numero]

Notaciones \(\Omega\) y \(\Theta\)

Notación \(o\) (\(o\) minúscula):

\[f(x)=o(g(x))\ \Leftrightarrow\;\limsup_{x \to \infty} \left|\frac{f(x)}{g(x)}\right|= 0.\]

Notación \(\Omega\): \[f(n)=O(g(n)) \Leftrightarrow g(n) = \Omega(f(n)).\] \[f(x)=\Omega(g(x))\ \Leftrightarrow\;\liminf_{x \to \infty} \left|\frac{f(x)}{g(x)}\right|> 0.\]

Notación \(\Theta\):

\(f(n)= \Theta(g(n))\) si \(f(n)=O(g(n))\) y \(f(n)=\Omega (g(n))\).

Complejidad

Problemas Computables

Para esta sección del curso, los objetos de estudio van a ser problemas que, en principio, se pueden resolver por medio de un algoritmo. Este tipo de problemas con llamados problemas computables.

Ejemplos de problemas computables:

  • Encontrar la raíz cuadrada de un número.

  • Calcular el n-esimo dígito de \(\pi\).

  • Ordenar una lista finita.

  • Encontrar la ruta más corta entre dos direcciones.

  • Calcular la órbita aproximada del cometa Halley a partir de las observaciones disponibles y la teoría de gravitación de Newton.

Clases de Complejidad

La ciencias de la complejidad es la disciplina científica que se enfoca en catalogar y encontrar propiedades de objetos basados en su dificultad (para problemas) o su profundidad aparente.

En la teoría de la complejidad computacional, las clases de complejidad son una serie de problemas que se consideran equivalentes en el sentido de que ocupan la misma cantidad de recursos computacionales (tiempo o memoria).

  • Un problema se demuestra que pertenece a cierta clase de complejidad si el tener una solución a un problema perteneciente a esta clase nos ayuda a resolver el primer problema en un tiempo corto o espacio pequeño.

  • Los requerimientos de memoria y de tiempo se consideran por separado.

Clases de Complejidad

  • Hay cientos de clases de complejidad computacional. En este curso nos enfocaremos en tres clases: \(P\), \(NP\) y \(NP\text{-}C\).

  • La clasificación se hace en términos de la notación \(O\) mayúscula.

  • Ya que estamos usando la notación \(O\), tenemos una relación jerárquica entre las clases:
    • Hay clases más difíciles que otras, y estas, en general, contienen a las más fáciles.

Clases de Complejidad

La clase \(P\)

La clase \(PTIME\) o \(P\) se define como aquella que contiene a los problemas de decisión (si o no) que se pueden resolver en tiempo polinomial por una máquina de Turing. Una máquina de Turing es de tiempo polinomial si el número de transiciones en función del tamaño de la entrada es del orden \(O(n^c)\) para alguna \(c\geq1\) constante.

Ya que muchos problemas cotidianos se pueden replantear como problemas de decisión, vamos a definir de manera informal a esta clase como:

  • Todos los problemas que se pueden resolver por un algoritmos cuyo tiempo de ejecución (número de pasos) es del orden de \(O(n^c)\) para alguna \(c\geq1\) constante.

Vamos a llamar a esta clase como la clase de los problemas y algoritmos polinomiales.

Problemas Polinomiales.

Canónicamente, se considera que esta clase contiene a todos los problemas computacionales que se pueden resolver de manera efectiva, en otras palabras que son tratables.

  • En otras palabras, esta clase contiene a todos los problemas que se pueden resolver de manera realista en una computadora.

Ejemplos de problemas Polinomiales.

Algunos problemas fundamentales en \(P\) son:

  • Ordenar una lista.
  • La mayoría de los problemas de uso común de decisión y optimización en la programación lineal.
  • La pertenencia a un lenguaje libre de contexto.
  • Encontrar el camino más corto entre dos nodos de una gráfica (algoritmo de Dijkstra).
  • La transformada de Furrier.
  • El Problema del Valor de un Circuito: calcular la salida de este.

La clase \(NP\)

Comúnmente, la clase \(NP\) se interpreta como la clase de problemas que no se pueden resolver en tiempo polinomial. ESTA ES UNA INTERPRETACIÓN ERRÓNEA!

La definición correcta es: La clase \(NP\) es aquella que esta compuesta de problemas que se pueden resolver en tiempo polinomial por una máquina de Turing no determinista.

  • Esta definición incluye a todos los problemas en \(P\) y descarta a una infinidad de problemas que no se pueden resolver en tiempo polinomial.

  • Es equivalente a pedir que la respuesta se pueda verificar en tiempo polinomial dado un certificado.

  • El entender esta definición requiere conocimiento avanzado de máquinas de Turing que no es parte de este curso.

La clase \(NP\)-Difícil

  • Por lo anterior, vamos a manejar un concepto menos formal.

Problemas \(NP\)-Hard (\(NP\)-Difícil)

De manera informal, un problema esta en \(NP\)-Hard si es, al menos, tan difícil de resolver que el problema más difícil en \(NP\).

  • De forma más precisa, un problema es \(NP\)-Hard si todo problema en \(NP\) se puede reducir en tiempo polinomial a éste.

Para nuestros propósitos, podemos pensar en los problemas \(NP\)-Hard como una clase de problemas en \(NP\) que no son polinomiales.

Problemas \(NP\)-\(C\)

Un problema es \(NP\)-\(C\) (\(NP\)-Completos) si es esta en \(NP\) y además es \(NP\)-Hard.

  • Una forma de pensar en los problemas \(NP\)-Completos es en un conjunto de problemas que se sospecha que no son polinomiales, pero no se a demostrado que lo son.

  • El resolver la pregunta \(NP=P\) o \(NP \neq P\) es considerada uno de los problemas sin resolver más importantes en la ciencia actualmente. Como recompeza a quien lo resuelva hay un premio de 1 millón de dolares, aunado a fama eterna.

NP vs NP-C

Problemas \(NP\)-\(C\)

Un problema es \(NP\)-\(C\) (\(NP\)-Completos) si es esta en \(NP\) y además es \(NP\)-Hard.

  • Una forma de pensar en los problemas \(NP\)-Completos es en un conjunto de problemas que se sospecha que no son polinomiales, pero no se a demostrado que lo son.

Problemas \(NP\)-\(C\)

P vs NP

Ejemplos de problemas NP-C.

  • Encontrar el camino más largo en una gráfica.
  • La satisfacibilidad Booleana.
  • El Problema del agente viajero:
    • Dada una lista de ciudades, ¿cual es la ruta más corta que visita cada ciudad una sola vez y regresa al punto de partida?
  • El problema de la mochila:
    • ¿Cuál la mejor forma de empacar mis cosas?
  • La 3 coloración.
    • ¿Se puede colorear este mapa con solo 3 colores?
  • El problema del clan.

Y muchos más.

Algoritmos "Greedy" (codiciosos)

¿Cómo atacar un problema NP-C?

Existen varios métodos de intentar dar una solución buena a un problema NP-C. Entre estos se encuentran los Algoritmos "Greedy" o codiciosos.

Un algoritmo greedy (codicioso) es un algoritmo que fue diseñado bajo la heurística de dividir un problema en problemas más pequeños y obtener una solución óptima en estos, esperando que la solución conjunta sea buena.