Complejidad temporal
| Fuente | Recurso | Notas |
|---|---|---|
| IUSACO | 3 - Algorithm Analysis | este módulo se basa en este capítulo |
| CPH | 2 - Time Complexity | Introducción y ejemplos |
| PAPS1 | 5 - Time Complexity | Más en profundidad. En particular, 5.2 da una definición formal de Big O. |
| YouTube | Introduction 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 segundos para envíos en C++ y de 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 , aunque podría acercarse a 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 . 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 cuando 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 , donde en general se omiten los factores constantes y los términos de menor orden de . Veamos algunos ejemplos de cómo funciona, a continuación.
El código siguiente es , 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 + 153Las operaciones de entrada y salida también se asumen . En los ejemplos siguientes, asumimos que el código dentro de los bucles es .
La complejidad temporal de un bucle es la cantidad de iteraciones que ejecuta. Por ejemplo, los siguientes fragmentos son ambos .
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 += 1Como ignoramos los factores constantes y los términos de menor orden, los siguientes ejemplos también son :
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 , porque el bucle externo corre iteraciones y el interno .
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 iteraciones, y el interno corre entre y iteraciones (como máximo ). Como la notación Big O calcula la complejidad temporal del peor caso, tratamos el bucle interno como un factor de .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 .
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 .
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 , porque consiste en dos bloques de complejidad y , 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:
- Búsqueda binaria:
- Conjunto/mapa ordenado o cola de prioridad: por operación
- Factorización en primos de un entero, o comprobar de forma naive si un entero es primo o compuesto:
- Leer elementos de entrada:
- Recorrer un arreglo o una lista de elementos:
- Ordenamiento: normalmente para los algoritmos de
ordenamiento por defecto (mergesort,
Collections.sort,Arrays.sort) - Java Quicksort, función
Arrays.sortsobre primitivos:- Ver Introduction to Data Structures para más detalles.
- Recorrer todos los subconjuntos de tamaño de los elementos de entrada: . Por ejemplo, recorrer todas las ternas es .
- Recorrer todos los subconjuntos:
- Recorrer todas las permutaciones:
Estas son cotas superiores conservadoras del valor de para cada complejidad temporal. A veces se puede con más, pero esto permite chequear rápido si un algoritmo es viable.
| Complejidades posibles | |
|---|---|
| , , | |
| , | |
| , , |
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 , 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 se puede acelerar por un factor de 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 y funciones no negativas de a . Si existen constantes positivas y tales que siempre que , decimos que .
Por lo tanto, podríamos decir que la complejidad temporal de una función lineal, , también es , , , , , 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 .
P vs. NP
P es la clase de problemas que se pueden resolver en tiempo polinomial (). 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