Complejidad y simulación
Qué vamos a ver hoy
| Min | Tema |
|---|---|
| 0–25 | Contar operaciones y notación O |
| 25–40 | Tabla de restricciones contra complejidad |
| 40–60 | Costos ocultos en Python |
| 60–70 | Medir tiempos y generar casos grandes |
| 70–90 | Simulación y casos borde: Permutations |
Dos soluciones correctas, un solo AC
Problema: dado un arreglo de n ≤ 2·10⁵ enteros, contar cuántos valores distintos tiene.
# Solución A
vistos = []
for x in a:
if x not in vistos: # recorre la lista
vistos.append(x)
print(len(vistos))
# Solución B print(len(set(a)))
Solución A TLE
Cada x not in vistos recorre hasta n elementos. En total son hasta n² = 4·10¹⁰ comparaciones.
Solución B AC
Insertar en un set cuesta O(1) en promedio. En total son unas 2·10⁵ operaciones.
Las dos dan la respuesta correcta. Lo que cambia es la cantidad de operaciones.
Contar operaciones
Cuántas veces se ejecuta cada línea en función de n.
Cada celda es una vuelta del bucle
Las complejidades más comunes
| Complejidad | Nombre | Ejemplo |
|---|---|---|
O(1) | constante | acceder a a[i], fórmula de Gauss |
O(log n) | logarítmica | búsqueda binaria, dividir n entre 2 hasta llegar a 1 |
O(n) | lineal | recorrer el arreglo una vez, sum(a) |
O(n log n) | ordenar con sort | |
O(n²) | cuadrática | revisar todos los pares (i, j) |
O(2ⁿ) | exponencial | revisar todos los subconjuntos |
O(n!) | factorial | revisar todas las permutaciones |
Se escribe el término que más crece y sin constantes: 3n² + 5n + 7 operaciones es O(n²).
¿Cabe en 1 segundo?
Supuesto: en el juez, PyPy hace unas 10⁸ operaciones simples por segundo y C++ unas 10⁹. Son órdenes de magnitud. Desde n = 10⁶ se muestra también C++. Medido en un Apple M4, C++ fue entre 1 y 9 veces más rápido que PyPy según el código. La longitud de cada barra está en escala logarítmica.
De la restricción a la complejidad
| Restricción | Complejidad aceptable |
|---|---|
| n ≤ 10 | O(n!) |
| n ≤ 20 | O(2ⁿ) |
| n ≤ 500 | O(n³) |
| n ≤ 5000 | O(n²) |
| n ≤ 2·10⁵ | O(n log n) |
| n ≤ 10⁶ o más | O(n) u O(log n) |
La tabla viene de pedir unas 10⁸ operaciones como máximo. Para C++ es una regla con margen, porque hace entre 10⁸ y 10⁹ operaciones simples por segundo.
PyPy hace unas 10⁸ en bucles simples, así que usa la tabla casi sin margen. Con muchas listas pequeñas, tuplas o diccionarios es más lento, así que conviene dejar margen.
Uso práctico: leer las restricciones primero y descartar las ideas que no caben.
¿Qué complejidad se necesita?
Haga clic en cada tarjeta para ver la respuesta.
¿Cuál es la complejidad de cada fragmento?
for i in range(n):
for j in range(5):
s += a[i] * j
O(n). El bucle interno siempre da 5 vueltas.
i = n
while i > 0:
i //= 2
O(log n). i se divide entre 2 en cada vuelta.
for i in range(n):
if a[i] in b: # b es una lista de tamaño n
c += 1
O(n²). in sobre una lista recorre la lista.
a.sort()
for i in range(n):
j = bisect_left(a, x - a[i])
O(n log n). Ordenar es n log n y cada bisect es log n.
Costos ocultos en Python
Operaciones que se escriben en una línea y recorren toda la lista.
Buscar un valor: lista o set
Lista: revisa uno por uno
Set: calcula dónde está
Sacar el primer elemento de una lista
a.pop(0) mueve todo el resto
d.popleft() solo mueve el inicio
Costos ocultos más comunes
| Operación | Costo | Alternativa |
|---|---|---|
x in lista | O(n) | x in conjunto: O(1) |
lista.pop(0) | O(n) | deque.popleft(): O(1) |
lista.insert(0, x) | O(n) | deque.appendleft(x) |
lista.index(x) | O(n) | dict de valor a posición |
lista.count(x) | O(n) | Counter una sola vez |
a[i:j] | O(j − i) | trabajar con índices |
s = s + t en un bucle | O(len) por vuelta | lista + "".join |
min(a), sum(a) | O(n) | calcular una vez, fuera del bucle |
Ninguna de estas operaciones es un problema si se hace una vez. El problema es hacerlas dentro de un bucle de n vueltas.
Cuánto cuestan en la práctica
10⁴ búsquedas en 10⁵ elementos
x in lista0.25 sx in conjunto0.0004 sSacar 10⁵ elementos por el inicio
lista.pop(0)0.61 sdeque.popleft()0.002 sPyPy 3.11, Apple M4. En cada par, las barras usan la misma escala.
Medir y probar
Comprobar el tiempo en el computador antes de enviar.
Generar un caso grande y medir
import random
n = 200000
print(n)
print(" ".join(str(random.randint(1, 10**9))
for _ in range(n)))
pypy3 gen.py > grande.txt time pypy3 sol.py < grande.txt
import time
t0 = time.perf_counter()
# ... código a medir ...
t1 = time.perf_counter()
print(f"{t1 - t0:.3f} s", file=sys.stderr)
Se escribe en sys.stderr para no mezclar la medición con la salida. Hay que borrarlo antes de enviar.
Comparar con una solución lenta
import random, subprocess
for it in range(500):
n = random.randint(1, 8)
a = [random.randint(1, 10) for _ in range(n)]
caso = f"{n}\n{' '.join(map(str, a))}\n"
r1 = subprocess.run(["pypy3", "lenta.py"], input=caso,
capture_output=True, text=True).stdout
r2 = subprocess.run(["pypy3", "rapida.py"], input=caso,
capture_output=True, text=True).stdout
if r1 != r2:
print("Diferencia en:\n" + caso)
print("lenta:", r1, "rapida:", r2)
break
- La solución lenta prueba todo y es fácil de escribir bien.
- Los casos son pequeños para que la lenta termine rápido.
- Cuando hay diferencia, el caso que falla es corto y se puede revisar a mano.
Simulación
Hacer exactamente lo que dice el enunciado y revisar los casos borde.
Cómo resolver un problema de simulación
- Escribir los pasos del enunciado en orden, en español.
- Elegir cómo guardar el estado (variables, listas).
- Traducir cada paso a código.
- Contar cuántos pasos hace la simulación y comparar con la tabla.
Casos borde que siempre hay que probar
- n = 1 (y n = 0 si las restricciones lo permiten)
- El valor máximo de n
- Todos los elementos iguales
- Arreglo ya ordenado y ordenado al revés
- Valores máximos (10⁹, 10¹⁸)
- Respuesta "imposible" o "NO"
Una permutación sin vecinos consecutivos
Construir una permutación de 1…n donde dos vecinos nunca difieran en 1. Si no existe, imprimir NO SOLUTION. Restricción: n ≤ 10⁶.
def main():
n = int(input())
if n == 2 or n == 3:
print("NO SOLUTION")
return
p = list(range(2, n + 1, 2)) + list(range(1, n + 1, 2))
print(*p)
main()
Dos pares seguidos difieren en 2. Lo mismo dos impares. En la unión, el último par (n o n − 1) y el 1 difieren en más de 1 cuando n ≥ 4.
Problemas de la práctica
CSES
Codeforces Div. 4
AtCoder (problemas B)
En Coins, la búsqueda completa hace hasta 51³ ≈ 1.3·10⁵ vueltas: cabe sin problema.
Antes de programar
- Leer n en las restricciones y buscar la complejidad en la tabla.
- Contar las operaciones de la idea, incluyendo los costos ocultos.
- Si la idea no cabe, buscar otra antes de escribir código.
- Probar con un caso grande generado y medir el tiempo.
- Probar los casos borde.