Skip to Content

Complejidad temporal

Recursos
FuenteRecursoNotas
IUSACO3 - Algorithm Analysis

este módulo se basa en este capítulo

CPH2 - Time Complexity

Introducción y ejemplos

PAPS15 - Time Complexity

Más en profundidad. En particular, 5.2 da una definición formal de Big O.

YouTubeIntroduction to Big-O

Si se prefiere ver un video


En los concursos de programación, el programa tiene que terminar de ejecutarse dentro de un plazo para recibir puntaje. En USACO, este límite es de 22 segundos para envíos en C++ y de 44 segundos para envíos en Java/Python. Una estimación conservadora de la cantidad de operaciones que el servidor del juez puede manejar por segundo es 10810^8, aunque podría acercarse a 51085 \cdot 10^8 si los factores constantes son buenosSi no se sabe qué son los factores constantes, no hay problema: los explicamos más abajo..

Cálculos de complejidad

Queremos un método para calcular cuántas operaciones hace falta para ejecutar cada algoritmo, en función del tamaño de la entrada nn. Por suerte, esto se puede hacer con relativa facilidad usando la notación Big O , que expresa la complejidad temporal del peor caso como una función de nn cuando nn se vuelve arbitrariamente grande. La complejidad es una cota superior de la cantidad de pasos que requiere un algoritmo en función del tamaño de la entrada. En notación Big O, denotamos la complejidad de una función como O(f(n))\mathcal{O}(f(n)), donde en general se omiten los factores constantes y los términos de menor orden de f(n)f(n). Veamos algunos ejemplos de cómo funciona, a continuación.

El código siguiente es O(1)\mathcal{O}(1), porque ejecuta una cantidad constante de operaciones.

int a = 5; int b = 7; int c = 4; int d = a + b + c + 153;
int a = 5; int b = 7; int c = 4; int d = a + b + c + 153;
a = 5 b = 7 c = 4 d = a + b + c + 153

Las operaciones de entrada y salida también se asumen O(1)\mathcal{O}(1). En los ejemplos siguientes, asumimos que el código dentro de los bucles es O(1)\mathcal{O}(1).

La complejidad temporal de un bucle es la cantidad de iteraciones que ejecuta. Por ejemplo, los siguientes fragmentos son ambos O(n)\mathcal{O}(n).

for (int i = 1; i <= n; i++) { // código de tiempo constante aquí }
int i = 0; while (i < n) { // código de tiempo constante aquí i++; }
for (int i = 1; i <= n; i++) { // código de tiempo constante aquí }
int i = 0; while (i < n) { // código de tiempo constante aquí i++; }
for i in range(1, n + 1): pass # código de tiempo constante aquí
i = 0 while i < n: # código de tiempo constante aquí i += 1

Como ignoramos los factores constantes y los términos de menor orden, los siguientes ejemplos también son O(n)\mathcal{O}(n):

for (int i = 1; i <= 5 * n + 17; i++) { // código de tiempo constante aquí }
for (int i = 1; i <= n + 457737; i++) { // código de tiempo constante aquí }
for (int i = 1; i <= 5 * n + 17; i++) { // código de tiempo constante aquí }
for (int i = 1; i <= n + 457737; i++) { // código de tiempo constante aquí }
for i in range(5 * n + 17): pass # código de tiempo constante aquí for i in range(n + 457737): pass # código de tiempo constante aquí

La complejidad temporal de varios bucles se obtiene multiplicando las complejidades temporales de cada uno. Este ejemplo es O(nm)\mathcal{O}(nm), porque el bucle externo corre O(n)\mathcal{O}(n) iteraciones y el interno O(m)\mathcal{O}(m).

for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { // código de tiempo constante aquí } }
for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { // código de tiempo constante aquí } }
for i in range(n): for j in range(m): pass # código de tiempo constante aquí

En este ejemplo, el bucle externo corre O(n)\mathcal{O}(n) iteraciones, y el interno corre entre 11 y nn iteraciones (como máximo nn). Como la notación Big O calcula la complejidad temporal del peor caso, tratamos el bucle interno como un factor de nn.También podemos hacer un poco de cuentas para calcular exactamente cuántas veces se ejecuta el código: 1+2+…+n = n*(n+1)/2 = (n^2 + n)/2 = O(n^2) Así, este código es O(n2)\mathcal{O}(n^2).

for (int i = 1; i <= n; i++) { for (int j = i; j <= n; j++) { // código de tiempo constante aquí } }
for (int i = 1; i <= n; i++) { for (int j = i; j <= n; j++) { // código de tiempo constante aquí } }
for i in range(n): for j in range(i, n): pass # código de tiempo constante aquí

Si un algoritmo tiene varios bloques, su complejidad temporal es la peor entre las de cada bloque. Por ejemplo, el código siguiente es O(n2)\mathcal{O}(n^2).

for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { // código de tiempo constante aquí } } for (int i = 1; i <= n + 58834; i++) { // más código de tiempo constante aquí }
for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { // código de tiempo constante aquí } } for (int i = 1; i <= n + 58834; i++) { // más código de tiempo constante aquí }
for i in range(n): for j in range(n): pass # código de tiempo constante aquí for i in range(n + 58834): pass # más código de tiempo constante aquí

El código siguiente es O(n2+m)\mathcal{O}(n^2 + m), porque consiste en dos bloques de complejidad O(n2)\mathcal{O}(n^2) y O(m)\mathcal{O}(m), y ninguno es una función de menor orden respecto de la otra.

for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { // código de tiempo constante aquí } } for (int i = 1; i <= m; i++) { // más código de tiempo constante aquí }
for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { // código de tiempo constante aquí } } for (int i = 1; i <= m; i++) { // más código de tiempo constante aquí }
for i in range(n): for j in range(n): pass # código de tiempo constante aquí for i in range(m): pass # más código de tiempo constante aquí

Complejidades y restricciones comunes

Los factores de complejidad de algunos algoritmos y estructuras de datos comunes son los siguientes:

  • Fórmulas matemáticas que solo calculan una respuesta: O(1)\mathcal{O}(1)
  • Búsqueda binaria: O(logn)\mathcal{O}(\log n)
  • Conjunto/mapa ordenado o cola de prioridad: O(logn)\mathcal{O}(\log n) por operación
  • Factorización en primos de un entero, o comprobar de forma naive si un entero es primo o compuesto: O(n)\mathcal{O}(\sqrt{n})
  • Leer nn elementos de entrada: O(n)\mathcal{O}(n)
  • Recorrer un arreglo o una lista de nn elementos: O(n)\mathcal{O}(n)
  • Ordenamiento: normalmente O(nlogn)\mathcal{O}(n \log n) para los algoritmos de ordenamiento por defecto (mergesort, Collections.sort, Arrays.sort)
  • Java Quicksort, función Arrays.sort sobre primitivos: O(n2)\mathcal{O}(n^2)
  • Recorrer todos los subconjuntos de tamaño kk de los elementos de entrada: O(nk)\mathcal{O}(n^k). Por ejemplo, recorrer todas las ternas es O(n3)\mathcal{O}(n^3).
  • Recorrer todos los subconjuntos: O(2n)\mathcal{O}(2^n)
  • Recorrer todas las permutaciones: O(n!)\mathcal{O}(n!)

Estas son cotas superiores conservadoras del valor de nn para cada complejidad temporal. A veces se puede con más, pero esto permite chequear rápido si un algoritmo es viable.

nnComplejidades posibles
n10n \le 10O(n!)\mathcal{O}(n!), O(n7)\mathcal{O}(n^7), O(n6)\mathcal{O}(n^6)
n20n \le 20O(2nn)\mathcal{O}(2^n \cdot n), O(n5)\mathcal{O}(n^5)
n80n \le 80O(n4)\mathcal{O}(n^4)
n400n \le 400O(n3)\mathcal{O}(n^3)
n7500n \le 7500O(n2)\mathcal{O}(n^2)
n7104n \le 7 \cdot 10^4O(nn)\mathcal{O}(n \sqrt n)
n5105n \le 5 \cdot 10^5O(nlogn)\mathcal{O}(n \log n)
n5106n \le 5 \cdot 10^6O(n)\mathcal{O}(n)
n1018n \le 10^{18}O(log2n)\mathcal{O}(\log^2 n), O(logn)\mathcal{O}(\log n), O(1)\mathcal{O}(1)

Factor constante

El factor constante refiere a que distintas operaciones con la misma complejidad tardan un poco distinto en ejecutarse. Por ejemplo, tres sumas tardan un poco más que una sola suma. Otro ejemplo: aunque la búsqueda binaria sobre un arreglo y la inserción en un conjunto ordenado son ambas O(logn)\mathcal{O}(\log n), la búsqueda binaria es notablemente más rápida.

El factor constante se ignora por completo en la notación Big O. La mayoría de las veces está bien, pero si el límite de tiempo es particularmente ajustado, se puede recibir límite de tiempo excedido (TLE) con la complejidad esperada. Cuando pasa eso, hay que tener en cuenta el factor constante. Por ejemplo, un código que recorre todas las ternas ordenadas en O(n3)\mathcal{O}(n^3) se puede acelerar por un factor de 66 si solo hace falta recorrer las ternas no ordenadas.

Por ahora, no hay que preocuparse por optimizar los factores constantes: alcanza con tenerlos presentes.

Definición formal de la notación Big O

Sean ff y gg funciones no negativas de R0\mathbb{R}_{\ge 0} a R0\mathbb{R}_{\ge 0}. Si existen constantes positivas n0n_0 y cc tales que f(n)cg(n)f(n) \le c \cdot g(n) siempre que nn0n \ge n_0, decimos que f(n)=O(g(n))f(n) = \mathcal{O}(g(n)).

Por lo tanto, podríamos decir que la complejidad temporal de una función lineal, O(n)\mathcal{O}(n), también es O(n/2)\mathcal{O}(n/2), O(2n)\mathcal{O}(2n), O(n2)\mathcal{O}(n^2), O(2n)\mathcal{O}(2^n), O(nn)\mathcal{O}(n^n), etc. Sin embargo, en general escribimos la función más simple entre las más restrictivas, que en el caso de la función lineal de arriba es O(n)\mathcal{O}(n).

P vs. NP

P es la clase de problemas que se pueden resolver en tiempo polinomial (O(n2),O(n3),O(n100),\mathcal{O}(n^2), \mathcal{O}(n^3), \mathcal{O}(n^{100}), \dots). NP, de nondeterministic polynomial time (tiempo polinomial no determinista), es el conjunto de problemas cuyas soluciones se pueden verificar en tiempo polinomial.

Un ejemplo común de un problema en NP es una versión generalizada de Sudoku, donde una solución se verifica fácilmente en tiempo polinomial, pero no se sabe si una solución se puede calcular en tiempo polinomial. «P vs. NP» es el problema clásico abierto que pregunta si todo problema que se puede verificar en tiempo polinomial también se puede resolver en tiempo polinomial.

Si interesa aprender más sobre P vs. NP, está este video de YouTube .

Cuestionario

Pregunta 1/4

¿Qué es la complejidad temporal?