diff --git a/2026/julio-diciembre/intermedios/complejidad-computacional/complejidad-computacional.md b/2026/julio-diciembre/intermedios/complejidad-computacional/complejidad-computacional.md index 683ad90..82d1e9c 100644 --- a/2026/julio-diciembre/intermedios/complejidad-computacional/complejidad-computacional.md +++ b/2026/julio-diciembre/intermedios/complejidad-computacional/complejidad-computacional.md @@ -14,53 +14,61 @@ A continuación, aprenderemos qué es la notación Big O ($O$), cómo analizar t En términos sencillos, una operación $O(1)$ (tiempo constante) es aquella que tarda exactamente la misma cantidad de tiempo en ejecutarse sin importar qué tan grande sea la entrada. -Analicemos un ejemplo línea por línea: +Analicemos un bloque de código de ejemplo línea por línea: ```cpp -int a = 5; // O(1) - Asignar un valor toma un tiempo fijo. -int b = 10; // O(1) - Otra asignación constante. -int suma = a + b; // O(1) - La CPU suma dos números en un ciclo de reloj. -cout << suma; // O(1) - Imprimir un número simple. +int a = 5; // O(1) - Asignar un valor toma un tiempo fijo +int b = 10; // O(1) - Otra asignación constante +int suma = a + b; // O(1) - La CPU suma dos números en un ciclo de reloj +cout << suma; // O(1) - Imprimir un número simple toma un tiempo fijo ``` -**Conclusión:** Todo el bloque de código se ejecuta en tiempo $O(1)$. No importa si sumamos `5 + 10` o `1000000 + 2000000`, la computadora tardará lo mismo. +**Conclusión:** Todo el bloque de código se ejecuta en tiempo $O(1)$. + +No importa si sumamos `5 + 10` o `1000000 + 2000000`, la computadora tardará lo mismo. --- -## 2. Complejidad Temporal (Big O) y Casos Comunes +## 2. Complejidad Temporal (Big O) + +Para calcular la complejidad total de un programa, sumamos la complejidad de sus partes, pero **solo nos quedamos con el término dominante** (el "peor de los casos"). -Para calcular la complejidad total de un programa, sumamos la complejidad de sus partes, pero **solo nos quedamos con el término dominante** (el "peor de los casos"). Si un algoritmo tiene una parte $O(n)$ y otra $O(n^2)$, la complejidad final es $O(n^2)$. +Si un algoritmo tiene una parte $O(n)$ y otra $O(n^2)$, la complejidad final es $O(n^2)$. Veamos los escenarios principales: ### A. Ciclo Simple: $O(n)$ ```cpp -int n = 1000; -for (int i = 0; i < n; i++) { - cout << i << "\n"; // Esta línea O(1) se ejecuta 'n' veces. +int n = 1000; // O(1) - Asignar un valor toma un tiempo fijo +for (int i = 0; i < n; i++) { // O(n) - Ciclo se ejecuta n veces + cout << i << "\n"; // O(1) - Imprimir i toma un tiempo fijo } ``` -El tiempo de ejecución crece linealmente. Si $n$ se duplica, el tiempo de ejecución se duplica. Su complejidad es **$O(n)$**. +**Conclusión:** Todo el bloque de código se ejecuta en tiempo **$O(n)$**. + +El tiempo de ejecución crece linealmente. Si $n$ se duplica, el tiempo de ejecución se duplica. ### B. Ciclos Anidados: $O(n^2)$ ```cpp -int n = 1000; -for (int i = 0; i < n; i++) { // Se ejecuta 'n' veces - for (int j = 0; j < n; j++) { // Por cada 'i', se ejecuta 'n' veces - cout << i << " " << j << "\n"; // O(1) ejecutado n * n veces +int n = 1000; // O(1) - Asignar un valor toma un tiempo fijo +for (int i = 0; i < n; i++) { // O(n) - Bloque externo itera n veces + for (int j = 0; j < n; j++) { // O(n^2) - Bloque interno itera n veces por cada i + cout << i << " " << j << "\n"; // O(1) - Imprimir i toma un tiempo fijo } } ``` -Su complejidad es **$O(n^2)$** (tiempo cuadrático). +**Conclusión:** Todo el bloque de código se ejecuta en tiempo **$O(n^2)$**. -> **Nota importante:** ¿Es $O(n^2)$ una complejidad mala? **Depende del tamaño de tu entrada ($n$).** En programación competitiva, los jueces suelen procesar unas $10^8$ operaciones por segundo. Si $n = 1000$, entonces $n^2 = 1,000,000$ (esto pasará rapidísimo). Pero si $n = 10^5$, $n^2 = 10^{10}$ operaciones, lo que tomará muchos segundos y resultará en un **TLE**. +> _Nota importante:_ ¿Es $O(n^2)$ una complejidad mala? **Depende del tamaño de la entrada ($n$).** + +En programación competitiva, los jueces suelen procesar unas $10^8$ operaciones por segundo. Si $n = 1000$, entonces $n^2 = 1,000,000$ (esto pasará rapidísimo). Pero si $n = 10^5$, $n^2 = 10^{10}$ operaciones, lo que tomará muchos segundos y resultará en un **Time Limit Exceeded (TLE)**. --- @@ -73,41 +81,51 @@ Supongamos que el problema nos pide sumar todos los números del 1 al $N$. La primera idea suele ser usar un ciclo simple: ```cpp -long long suma = 0; -for(int i = 1; i <= n; i++) { - suma += i; +long long suma = 0; // O(1) - Asignar un valor toma un tiempo fijo +for(int i = 1; i <= n; i++) { // O(n) - Ciclo itera n veces + suma += i; // O(1) - Suma i toma un tiempo fijo } ``` -Esto es **$O(n)$**. Pero, ¿qué pasa si el problema dice que $N = 10^{10}$? El ciclo iterará diez mil millones de veces, superando el límite de un segundo del juez. Nos dará **TLE**. +El bloque de código tiene **complejidad $O(n)$**. + +Pero, ¿qué pasa si el problema establece un valor para $N = 10^{10}$? el ciclo iterará diez mil millones de veces, superando el límite de un segundo del juez; el algoritmo producirá un **TLE**. ### La solución Matemática $O(1)$ -En lugar de sumar uno por uno, busquemos un patrón (el método que descubrió Gauss en la primaria). Si sumamos del 1 al 100: +En lugar de sumar los números uno por uno, se identifica un patrón lógico. + +El método que descubrió Carl Friedrich Gauss, se atribuye a una anécdota infantil del matemático quien resolvió el problema en segundos al identificar un patrón lógico en lugar de sumar número por número. + +Se dio cuenta de que si emparejaba los números de los extremos opuestos de la secuencia, la suma siempre daba el mismo resultado: - $1 + 100 = 101$ - $2 + 99 = 101$ - $3 + 98 = 101$ -Notamos que los extremos siempre suman $(n + 1)$, y como estamos emparejando números, tenemos exactamente $\frac{n}{2}$ pares. De aquí deducimos la fórmula mágica: +Nótese que los extremos siempre suman 101 = $(n + 1)$, y como estamos emparejando números, tenemos exactamente 50 = $\frac{n}{2}$ pares. -$$\text{Suma} = \frac{n \times (n + 1)}{2}$$ +De aquí se deduce la fórmula general, mágica, para sumar los primeros $n$ números naturales consecutivos: + +$$\text{S} = \frac{n \times (n + 1)}{2}$$ ```cpp -long long suma = (n * (n + 1)) / 2; // Una sola operación matemática +long long suma = ( n * ( n + 1 ) ) / 2; // O(1) - Una sola operación matemática ``` -**¡Felicidades!** Acabas de reducir un algoritmo que daba TLE con complejidad $O(n)$, a un algoritmo perfecto con complejidad **$O(1)$**. +**¡Felicidades!** acabas de reducir un algoritmo que daba TLE con complejidad $O(n)$, a un algoritmo con **complejidad $O(1)$**. --- ## 4. Complejidad Espacial (Memoria) -Así como analizamos cuánto tiempo toma un algoritmo, debemos saber **cuánta memoria RAM consume**. Los ejemplos anteriores solo declaran variables simples (`int`, `long long`), por lo que su complejidad espacial es **$O(1)$**. +Hemos analizamos cuánto tiempo toma un algoritmo, pero también debemos saber **cuánta memoria consume**. + +Los ejemplos anteriores solo declaran variables simples (`int`, `long long`), por lo que su complejidad espacial es **$O(1)$**. -Sin embargo, cuando usamos **arreglos (Arrays) o Vectores**, la memoria crece. Para calcularlo en bytes, debes recordar los tamaños de los tipos de datos (un `int` estándar pesa 4 bytes). +Cuando usamos **arreglos (Arrays) o Vectores**, la cantidad de memoria utilizada crece. Para calcularlo en bytes, debes recordar los tamaños de los tipos de datos (un `int` estándar pesa 4 bytes). **¿Cuánto pesa un arreglo en C++?** @@ -116,18 +134,18 @@ int arreglo[1000]; ``` -- Tenemos 1000 enteros. +- Tenemos 1,000 enteros. - Cada entero pesa 4 bytes. -- Total: $1000 \times 4 = 4000 \text{ bytes}$. +- Total: $1,000 \times 4 = 4,000 \text{ bytes}$. - Esto es aproximadamente **$4 \text{ KB}$**. -Si declararas un arreglo gigante `int arreglo[100000000]`, pesaría unos $400 \text{ MB}$, lo cual podría causarte un **Memory Limit Exceeded (MLE)** si el juez solo permite $256 \text{ MB}$. +Si declararas un arreglo gigante `int arreglo[100000000]`, pesaría unos $400 \text{ MB}$, lo cual podría provocar un **Memory Limit Exceeded (MLE)** si el juez solo permite $256 \text{ MB}$. --- ## Problemas de Práctica -Ahora que entiendes cómo analizar el tiempo de tu código y cómo una simple fórmula matemática puede salvarte de un TLE, es hora de programarlo. +Ahora que entiendes cómo analizar el tiempo de tu código y cómo una simple fórmula matemática puede salvarte de un _TLE_, es hora de programarlo. **Resuelve el siguiente problema aplicando la suma de Gauss:**