Sesión 69: Examen de Maestría Staff — 30 Preguntas Fundamentales de Active Recall y Auditoría Mental#
Tiempo estimado: 120-150 minutos. Nivel: De Junior/Trainee a Staff Specialist. Herramientas obligatorias: Hoja en blanco sin renglones, bolígrafo negro y cronómetro.
1. Diccionario de Conceptos del Día (Desde Cero para Junior / Trainee)#
Consulta rápida: Glosario Maestro | Guía de Diagramas
Active Recall (Recuperación Activa Basada en Evidencia)#
- (a) En palabras simples (intuición): Es la diferencia entre leer las respuestas de un examen y creer que te las sabes (reconocimiento pasivo), contra cerrar el libro por completo, tomar una hoja en blanco y verte obligado a explicar y dibujar el algoritmo desde cero sin mirar (recuperación activa). Si no puedes dibujarlo en una hoja en blanco, no lo dominas.
- (b) Definición técnica paso a paso: Principio de la ciencia cognitiva que estimula la memoria a largo plazo forzando al cerebro a reconstruir rutas neuronales de recuperación sin claves contextuales externas. Transforma el conocimiento frágil de memoria a corto plazo en memoria declarativa y procedimental permanente.
- (c) Cómo funciona en Rust (memoria y tipos): Obliga al desarrollador a ser capaz de modelar manualmente en un papel la distribución exacta de bytes en el Stack, los punteros y encabezados en el Heap, los tiempos de vida (lifetimes) y las reglas del Borrow Checker antes de escribir
cargo build. - (d) Error típico de Junior: Ver tutoriales de YouTube o leer soluciones en LeetCode creyendo que con eso "ya aprendió el problema", fracasando rotundamente 48 horas después en la entrevista en vivo.
Invariantes Algorítmicos#
- (a) En palabras simples (intuición): Es una verdad inmutable que nunca cambia, sin importar cuántas veces gire un bucle o cuántas llamadas recursivas se hagan. Es como la ley de la gravedad dentro de tu código: si tu invariante dice que "la mitad izquierda siempre está ordenada", esa condición debe ser cierta antes del bucle, durante el bucle y después del bucle.
- (b) Definición técnica paso a paso: Propiedad matemática P sobre el estado del programa que se cumple al inicio de una iteración o recursión y se preserva estrictamente tras cada paso de transición inductiva. Es la base formal para demostrar la corrección y terminación de cualquier algoritmo.
- (c) Cómo funciona en Rust (memoria y tipos): Se expresa mediante aserciones en código
assert!()o mediante el sistema de tipos de Rust (por ejemplo, tipos envolventes que imposibilitan representar estados inválidos en tiempo de compilación). - (d) Error típico de Junior: Escribir bucles
whileoformodificando punteros al azar con "parches"+1o-1cuando falla un test, sin definir el invariante previo.
Localidad de Caché y Jerarquía de Memoria (L1/L2/L3 vs RAM)#
- (a) En palabras simples (intuición): El procesador de tu computadora es un Ferrari que puede viajar a 300 km/h, pero la memoria RAM principal es un almacén que queda a 5 kilómetros de distancia. Si el Ferrari tiene que ir al almacén por cada dato, su velocidad real baja a 5 km/h. La memoria caché (L1/L2) es una pequeña guantera dentro del Ferrari: guardar datos contiguos en la guantera permite que el Ferrari nunca se detenga.
- (b) Definición técnica paso a paso: Principio físico de silicio donde los datos contiguos en memoria se transfieren automáticamente desde la RAM hacia la caché del chip en líneas de 64 bytes (Cache Lines).
- Acceso a registro de CPU / Caché L1: ~0.5 a 1 nanosegundo.
- Acceso a Caché L2/L3: ~3 a 10 nanosegundos.
- Acceso a memoria RAM principal: ~60 a 100 nanosegundos (100x más lento).
- (c) Cómo funciona en Rust (memoria y tipos): Estructuras contiguas como
Vec<T>o arenas de nodos indexadas conusizemaximizan el hardware prefetcher, mientras que punteros dispersos comoRc<RefCell<Node>>destruyen la localidad de caché. - (d) Error típico de Junior: Creer que dos algoritmos con la misma complejidad asintótica O(N) tienen la misma velocidad en la práctica; recorrer un
Veccontiguo es hasta 10 veces más rápido que recorrer una lista enlazadaLinkedListdel mismo tamaño debido a las faltas de caché (cache misses).
2. Desglose Conceptual desde Primeros Principios (Sin Asumir Nada)#
El Protocolo de Auditoría Mental en 4 Pasos#
Cuando un ingeniero de nivel Staff se enfrenta a un problema algorítmico nuevo en una entrevista o en el diseño de un motor crítico, ejecuta sistemáticamente este protocolo mental:
- Clarificación y Casos de Frontera: - ¿El conjunto puede ser nulo o vacío? ¿Puede tener tamaño 1? - ¿Qué rango numérico tienen los enteros (riesgo de desbordamiento de 32 bits vs 64 bits)? - ¿Existen duplicados? ¿El orden relativo debe preservarse (estabilidad)?
- Definición Explícita de Invariantes:
- Enuncia en voz alta: "Defino que el puntero
lowsiempre delimita elementos estrictamente menores que el objetivo yhighdelimita elementos mayores o iguales". - Selección de Estructura por Jerarquía de Memoria:
- ¿Puedo resolverlo en memoria contigua en el Stack o con un
Vecpreasignado para maximizar líneas de caché L1? - ¿Se requieren estructuras de árbol balanceado (BTreeMap) o basta con particiones de montículo (BinaryHeap)? - Análisis de Fallos en Silicio: - ¿Qué operaciones matemáticas intermedias pueden provocar desbordamiento o imprecisión en coma flotante?
3. Visualización de Arquitectura y Memoria en Rust#
Diagrama Arquitectónico de Maestría#
Consulta el diagrama vectorial:
flowchart TD
Q["30 Preguntas Fundamentales de Active Recall"] --> DERIV["Derivación en Hoja en Blanco<br/>(Sin ayuda externa ni autocompletado)"]
DERIV --> AUDIT["Auditoría Rigurosa de Invariantes<br/>y Jerarquía de Memoria en Rust"]
AUDIT --> RETENTION["Retención Permanente en Silicio<br/>para Rondas Staff / Principal L6+"]
Visualización Cuadro por Cuadro de Memoria en Rust (Stack vs Heap)#
ESTADO DE MEMORIA DE UN INGENIERO STAFF:
Stack (Mínimo overhead, punteros de control y tipos primitivos):
- Variables de puntero: low: usize, high: usize, mid: usize
- Metadatos de vectores: ptr (8 bytes), cap (8 bytes), len (8 bytes) = 24 bytes por slice
- Tipos primitivos: i64, u128 para acumuladores inmunes a desbordamiento
Heap (Bloques contiguos con localidad de caché L1 garantizada):
- Vec<T> contiguo (sin dispersión de punteros individuales)
- Arena de Nodos: Vec<Node> indexada por enteros usize (cero Rc/RefCell)
- Tablas Hash preasignadas: HashMap::with_capacity(cap) (cero realocaciones dinámicas)
EL PRINCIPIO DE ORO:
"Los datos que se procesan juntos deben vivir contiguos en la memoria física".
4. Implementación Canónica en Rust (Compilable con rustc --test)#
Archivo de referencia: practica-rust/dia_69.rs
/// 1. Búsqueda Binaria de frontera izquierda segura contra desbordamientos
pub fn binary_search_left_boundary(arr: &[i32], target: i32) -> Option<usize> {
if arr.is_empty() { return None; }
let mut low = 0;
let mut high = arr.len() - 1;
let mut ans = None;
while low <= high {
let mid = low + (high - low) / 2;
if arr[mid] == target {
ans = Some(mid);
if mid == 0 { break; }
high = mid - 1;
} else if arr[mid] < target {
low = mid + 1;
} else {
if mid == 0 { break; }
high = mid - 1;
}
}
ans
}
/// 2. Pila Monótona para Siguiente Mayor Elemento en O(N)
pub fn next_greater_element(nums: &[i32]) -> Vec<i32> {
let mut result = vec![-1; nums.len()];
let mut stack: Vec<usize> = Vec::new();
for (i, &val) in nums.iter().enumerate() {
while let Some(&top_idx) = stack.last() {
if val > nums[top_idx] {
result[top_idx] = val;
stack.pop();
} else {
break;
}
}
stack.push(i);
}
result
}
/// 3. Fenwick Tree (Binary Indexed Tree) con idx & (-idx) en O(log N)
pub struct MasteryFenwickTree {
tree: Vec<i64>,
}
impl MasteryFenwickTree {
pub fn new(n: usize) -> Self {
Self { tree: vec![0; n + 1] }
}
pub fn add(&mut self, mut idx: usize, delta: i64) {
idx += 1;
while idx < self.tree.len() {
self.tree[idx] += delta;
idx += idx & (!idx + 1);
}
}
pub fn prefix_sum(&self, mut idx: usize) -> i64 {
idx += 1;
let mut sum = 0;
while idx > 0 {
sum += self.tree[idx];
idx -= idx & (!idx + 1);
}
sum
}
}
5. El Gran Examen de 30 Preguntas de Active Recall#
Cierra todas tus notas, toma tu hoja en blanco y responde a fondo cada una de las 30 preguntas antes de desplegar las respuestas.
Bloque I: Algoritmos Fundamentales, Arreglos y Búsqueda (Preguntas 1 a 6)#
Pregunta 1: En Búsqueda Binaria, ¿por qué la fórmula clásica mid = (low + high) / 2 es un bug de desbordamiento en enteros y cuál es la expresión matemáticamente segura en Rust y Python?
Respuesta:
- (a) Intuición: Si dos personas pesan 150 kilos cada una y las pones juntas en una báscula que solo aguanta hasta 200 kilos antes de romperse para calcular el promedio, la báscula se rompe en mil pedazos en cuanto ambas se suben, aunque el promedio real (150 kg) quepa perfectamente en la báscula.
- (b) Definición Técnica: Si
lowyhighson enteros de 32 bits con signo y su suma excede 2³¹ - 1 = 2,147,483,647, la suma desborda a un número negativo en complemento a dos, produciendo un índice negativo o un pánico de memoria. - (c) En Rust y Python:
- En Rust:
let mid = low + (high - low) / 2;(donde la restahigh - lownunca excede el rango). - En Python: Los enteros tienen precisión arbitraria automática, por lo que
(low + high) // 2no desborda en memoria, pero en entrevistas se exige escribirlow + (high - low) // 2para demostrar rigor de ingeniería de bajo nivel. - (d) Error de Junior: Escribir
(low + high) / 2en C++ o Java, provocando que el algoritmo más famoso del mundo falle catastróficamente en producción ante arreglos masivos de más de 10⁹ elementos (el célebre bug reportado por Joshua Bloch en Java en 2006 tras 9 años de pasar desapercibido).
Pregunta 2: ¿Cuál es la diferencia fundamental entre la técnica de Dos Punteros Convergentes y la técnica de Ventana Deslizante (Sliding Window)?
Respuesta:
- (a) Intuición: Dos punteros convergentes son como dos personas que caminan desde los extremos opuestos de un puente hacia el centro para encontrarse en medio. Una ventana deslizante es como una lupa o regla transparente que se desliza por una hoja de izquierda a derecha, expandiendo o contrayendo su tamaño según lo que veas a través de ella.
- (b) Definición Técnica:
- Dos Punteros Convergentes: Un puntero inicia en $0$ y el otro en N - 1. Avanzan uno hacia el otro en función de comparaciones de suma o simetría (ej. Two Sum en arreglos ordenados, verificación de palíndromos). Termina cuando low ≥ high.
- Ventana Deslizante: Dos punteros left y right que inician en $0$ y avanzan en la misma dirección. El puntero derecho expande la ventana para cumplir una condición y el izquierdo la contrae para restaurar invariantes o minimizar longitud.
- (c) En Rust: Ambos operan en el Stack con dos variables escalares
usize, logrando tiempo O(N) y espacio auxiliar estricto O(1). - (d) Error de Junior: Usar fuerza bruta con dos bucles anidados O(N²) para problemas de subarreglos continuos en lugar de una ventana deslizante amortizada O(N).
Pregunta 3: En una Pila Monótona (Monotonic Stack), ¿por qué el tiempo de ejecución para resolver el Siguiente Mayor Elemento es estrictamente O(N) a pesar de tener un bucle while dentro de un for?
Respuesta:
- (a) Intuición: Imagina que tienes 10 personas formadas en una fila y cada persona solo puede entrar al teatro una vez y salir del teatro una vez. Aunque en un minuto particular el portero saque a 5 personas de golpe a la calle, a lo largo de toda la tarde el portero no puede sacar más de 10 personas en total.
- (b) Definición Técnica (Análisis Amortizado):
- El bucle exterior
foritera N veces. Cada índice i se inserta en el stack mediantepushexactamente una vez (N operaciones). - El bucle interior
whileejecutapop()sobre elementos del stack. Un elemento solo puede ser extraído si previamente fue insertado. Por lo tanto, el número total de operacionespop()en toda la ejecución está estrictamente acotado por N. - Operaciones totales: N (pushes) + ≤ N (pops) ≤ 2N = O(N).
- (c) En Rust: Se almacena en un
Vec<usize>contiguo en el Heap con capacidad preasignada. - (d) Error de Junior: Multiplicar ingenuamente las iteraciones del
for(N) por las delwhile(N) y concluir erróneamente que la complejidad es O(N²).
Pregunta 4: ¿Cuáles son los 4 recorridos clásicos de un árbol binario y en qué situaciones de producción es indispensable usar cada uno?
Respuesta:
- (a) Intuición: Es como visitar las habitaciones de una casa en diferente orden: visitar la puerta antes que los cuartos, o limpiar los cuartos antes de cerrar la puerta con llave.
- (b) Definición Técnica:
- Preorder (Raíz → Izq → Der): Serialización y clonación de la estructura exacta del árbol.
- Inorder (Izq → Raíz → Der): En un Árbol Binario de Búsqueda (BST), visita los nodos en orden numérico estrictamente ascendente.
- Postorder (Izq → Der → Raíz): Liberación de memoria en C/C++ (destruir hijos antes que el padre), cálculo del tamaño/altura de directorios en sistemas de archivos.
- Level-Order (BFS por niveles): Impresión visual del árbol línea por línea, cálculo del camino más corto en árboles no ponderados.
- (c) En Rust: Preorder/Inorder/Postorder se implementan de forma recursiva o con una pila en el Stack; Level-Order requiere una cola FIFO
VecDeque. - (d) Error de Junior: Usar recursión profunda en árboles degenerados con 10⁶ nodos, provocando un desbordamiento del Call Stack (Stack Overflow); en producción se previene con iteración con pila explícita en el Heap.
Pregunta 5: ¿Por qué el algoritmo de Dijkstra falla si un grafo contiene aristas con pesos negativos y qué algoritmo alternativo se debe usar?
Respuesta:
- (a) Intuición: Dijkstra es un algoritmo codicioso (greedy): en cuanto extrae un nodo de su lista de prioridades, "sella" ese nodo creyendo con fe ciega que jamás existirá un camino más corto hacia él. Si después aparece un túnel con peso negativo (que te regala distancia), toda la suposición de Dijkstra se derrumba.
- (b) Definición Técnica: La demostración de corrección de Dijkstra depende del invariante de que añadir una arista a un camino nunca puede disminuir su longitud total: dist(u) + w(u, v) ≥ dist(u) si w ≥ 0. Con pesos negativos, este invariante se rompe.
- (c) Algoritmo Alternativo: Bellman-Ford en O(V · E), o el algoritmo SPFA (Shortest Path Faster Algorithm), los cuales además detectan la presencia de ciclos negativos infinitos.
- (d) Error de Junior: Sumar una constante gigantesca a todas las aristas negativas creyendo que eso "las hace positivas"; esto favorece erróneamente a los caminos con muchas aristas sobre los caminos con pocas aristas, arruinando la topología del camino más corto.
Pregunta 6: ¿Cuál es la diferencia fundamental en diseño y rendimiento entre Programación Dinámica Top-Down (Memoización) y Bottom-Up (Tabulación)?
Respuesta:
- (a) Intuición: Top-Down es como resolver un problema empezando por el final y preguntando solo las dudas que necesitas resolver sobre la marcha. Bottom-Up es como construir un edificio piso por piso desde los cimientos: pones el piso 1, luego el 2, luego el 3, sin hacer ninguna pregunta hacia atrás.
- (b) Definición Técnica:
- Top-Down (Memoización): Recursión natural que explora solo los estados alcanzables necesarios, almacenando resultados en una tabla Hash o arreglo de memoización. Costo: sobrecarga de llamadas en el Call Stack.
- Bottom-Up (Tabulación): Bucles iterativos que llenan una matriz ordenada de estados desde el caso base. Permite optimización de espacio descartando filas anteriores (space rolling) y maximiza la localidad de caché L1.
- (c) En Rust: Bottom-Up es universalmente preferido en sistemas de alto rendimiento porque elimina la sobrecarga de saltos del procesador y el riesgo de agotar el Call Stack.
- (d) Error de Junior: Usar Top-Down sin verificar los límites de profundidad de recursión del entorno de ejecución.
Bloque II: Bits, Particiones y Estructuras Especializadas (Preguntas 7 a 12)#
Pregunta 7: ¿Cómo funciona la optimización de espacio a 1D en el Problema de la Mochila 0/1 (0/1 Knapsack) y por qué es obligatorio iterar la capacidad en reversa?
Respuesta:
- (a) Intuición: En la tabla 2D original necesitas mirar el piso anterior. Si aplastas la tabla en una sola fila, cada vez que calculas una celda pisas el valor viejo. Si caminas de derecha a izquierda (en reversa), usas los valores viejos que todavía están intactos a tu izquierda antes de sobreescribirlos.
- (b) Definición Técnica: La recurrencia es dp[i][w] = max(dp[i-1][w], dp[i-1][w - wᵢ] + vᵢ). Al comprimir a un vector 1D dp[w], iterar w de W hacia abajo hasta wᵢ garantiza que el valor dp[w - wᵢ] que leemos corresponde a la fila i - 1 (el elemento actual aún no ha sido considerado en esa capacidad).
- (c) En Rust:
for w in (weight..=capacity).rev() { dp[w] = dp[w].max(dp[w - weight] + value); }. - (d) Error de Junior: Iterar hacia adelante (
for w in weight..=capacity); esto transforma accidentalmente el problema en la Mochila Sin Límite (Unbounded Knapsack), reutilizando el mismo objeto múltiples veces.
Pregunta 8: ¿Qué realiza exactamente la operación a nivel de bits de Brian Kernighan n & (n - 1) y por qué apaga el bit en 1 más a la derecha en O(1) de hardware?
Respuesta:
- (a) Intuición: Restarle 1 a un número binario es como pedir cambio: el último 1 se convierte en 0, y todos los ceros que estaban a su derecha se convierten en 1s. Al hacer un
ANDcon el número original, ese último 1 y todo lo que estaba a su derecha se borra por completo. - (b) Demostración Matemática: Sea n = … 1 0 0 … 0. Al restar 1, n - 1 = … 0 1 1 … 1. Los bits anteriores al último 1 permanecen idénticos, mientras que el último 1 y sus ceros subsecuentes se invierten. El operador AND produce:
- (c) En Rust: Permite contar bits encendidos en tiempo proporcional únicamente a la cantidad de bits en 1:
while n != 0 { n &= n - 1; count += 1; }. - (d) Error de Junior: Escribir un bucle de 32 o 64 iteraciones fijas para contar bits en números dispersos que solo tienen un bit encendido.
Pregunta 9: ¿Cuál es la diferencia entre la partición de Lomuto y la partición de Hoare en Quicksort y cuál previene degradación ante elementos duplicados masivos?
Respuesta:
- (a) Intuición: Lomuto usa dos punteros que caminan juntos hacia adelante barriendo elementos menores al pivote. Hoare usa dos punteros que arrancan en los extremos opuestos y se mueven hacia el centro intercambiando elementos desubicados.
- (b) Definición Técnica:
- Lomuto: Realiza aproximadamente 3N escrituras y sufre de degradación cuadrática O(N²) si todos los elementos del arreglo son idénticos (ej. un arreglo de puros números 5).
- Hoare: Realiza hasta 3 veces menos intercambios de memoria y divide el arreglo de elementos duplicados exactamente por la mitad, conservando la complejidad óptima O(N log N).
- (c) En Rust: La función estándar
slice::sort_unstableutiliza Pdqsort (Pattern-defeating quicksort), que combina Hoare con partición de 3 vías de Dijkstra (Fat Partition). - (d) Error de Junior: Implementar Lomuto en pruebas técnicas creyendo que es óptimo solo porque su código es 2 líneas más corto.
Pregunta 10: En DSU (Union-Find), ¿cómo colaboran la Compresión de Ruta y la Unión por Rango para dominar la complejidad casi constante O(α(N))?
Respuesta:
- (a) Intuición: La Unión por Rango evita que el árbol se vuelva alto pegando siempre el árbol más pequeño debajo de la raíz del árbol más grande. La Compresión de Ruta es un aplanamiento brutal: cada vez que buscas a alguien, conectas a todos los nodos del camino directamente al rey supremo.
- (b) Demostración Técnica: La Unión por Rango por sí sola garantiza que la altura del árbol nunca exceda O(log N). La Compresión de Ruta aplana los caminos subsecuentes. Juntas, garantizan que cualquier secuencia de M operaciones tome O(M · α(N)) pasos, donde α(N) ≤ 4.
- (c) En Rust: Se implementa con dos vectores planos
parentyrankcontiguos en el Heap sin punteros enlazados. - (d) Error de Junior: Implementar únicamente la compresión de ruta o únicamente el rango, perdiendo la cota formal de Ackermann.
Pregunta 11: ¿Por qué implementar un Trie mediante una Arena de Nodos (Vec<TrieNode>) es arquitectónicamente superior a usar árboles de punteros (Box<TrieNode>) en Rust?
Respuesta:
- (a) Intuición: Es la diferencia entre guardar 1,000 herramientas en 1,000 cajitas diminutas repartidas por toda tu casa (donde pasas más tiempo caminando que trabajando), contra guardarlas todas ordenadas en una sola gaveta metálica larga en tu mesa de trabajo.
- (b) Definición Técnica:
Box<TrieNode>invoca al asignador de memoria del sistema operativo para cada nodo individual, generando sobrecarga de encabezados (metadata headers) y dispersando los nodos en direcciones no contiguas del Heap. La Arena de Nodos aloja todos los nodos contiguos en un únicoVec<TrieNode>donde los punteros son simples enterosusize, eliminando el 100% de las faltas de página y maximizando la transferencia de líneas de caché L1. - (c) En Rust: Previene ciclos de referencias y elimina la necesidad de
Rc<RefCell<T>>. - (d) Error de Junior: Forzar árboles con punteros inseguros
*mutoRccreyendo que en Rust los árboles deben parecerse sintácticamente a los de C++.
Pregunta 12: En el Fenwick Tree (Binary Indexed Tree), ¿qué propiedad de silicio fundamenta el aislamiento del bit menos significativo con la expresión idx & (-idx)?
Respuesta:
- (a) Intuición: Es una trampa matemática perfecta basada en el reloj binario: la representación de los números negativos en tu computadora invierte todos los bits y suma 1, haciendo que coincida exactamente un único bit en 1 entre el número y su negativo.
- (b) Demostración Matemática en Silicio: En la arquitectura de hardware universal de Complemento a 2, el negativo de un número entero se define como:
Si x = a 1 0ᵏ (donde a son bits arbitrarios, $1$ es el bit menos significativo encendido y 0ᵏ son k ceros a la derecha):
Al aplicar el operador lógico AND:
Todos los bits de la izquierda se anulan (a & sim a = 0), aislando con precisión quirúrgica el bit de menor peso.
- (c) En Rust: En tipos sin signo
usize, se calcula de forma equivalente y segura conidx & (!idx + 1). - (d) Error de Junior: Usar un bucle manual con desplazamientos de bits para buscar la potencia de 2 en lugar de la instrucción atómica
idx & (-idx).
Bloque III: Rangos, Caché y Consultas Avanzadas (Preguntas 13 a 18)#
Pregunta 13: ¿Cuál es la diferencia crítica entre un Fenwick Tree y un Segment Tree y cuándo es obligatorio usar Segment Tree?
Respuesta:
- (a) Intuición: El Fenwick Tree es una navaja suiza diminuta, ultrarrápida y compacta que solo sirve para operaciones acumulativas reversibles (sumas o productos invertibles). El Segment Tree es un taller completo con maquinaria pesada: consume 4 veces más memoria, pero puede resolver cualquier consulta arbitraria (mínimos de rango, máximos, GCD, actualizaciones de rangos enteros con propagación perezosa).
- (b) Definición Técnica:
- Fenwick Tree: Requiere que la operación sea invertible (a + b - b = a). Consumo de memoria: exactamente N enteros.
- Segment Tree: Soporta cualquier semigrupo asociativo (incluso operaciones no invertibles como
min(L, R)omax(L, R)). Admite Lazy Propagation en O(log N) para actualizaciones sobre rangos continuos [l, r]. Consumo de memoria: 4N enteros. - (c) En Rust: Si el problema solo exige sumas acumulativas dinámicas, Fenwick Tree es 3x más rápido en silicio debido a menor huella de memoria. Si exige Range Minimum Query (RMQ) o Range Updates, Segment Tree es obligatorio.
- (d) Error de Junior: Intentar calcular Range Minimum Query con Fenwick Tree en arreglos donde se ejecutan actualizaciones dinámicas decrecientes.
Pregunta 14: ¿Cómo se generaliza el algoritmo de Kadane 1D para resolver el Problema del Subarreglo Circular Máximo en tiempo lineal O(N)?
Respuesta:
- (a) Intuición: Un subarreglo continuo en un círculo cerrado tiene dos opciones: o está confinado en el medio sin cruzar los extremos (Kadane normal), o se envuelve alrededor de los bordes. Si se envuelve por los bordes, significa que lo que quedó afuera en el centro es un subarreglo continuo con la suma mínima posible.
- (b) Formulación Matemática:
- Caso de Frontera Crítico: Si todos los números del arreglo son negativos (ej.
[-3, -2, -1]), laSumaTotal - KadaneMindaría $0$ (un subarreglo vacío), lo cual es ilegal. En ese caso especial, el resultado es simplemente KadaneMax (el número negativo menos desfavorable, -1). - (c) En Rust: Se computa en una única pasada de bucle manteniendo acumuladores locales y globales de mínimo y máximo en variables en el Stack.
- (d) Error de Junior: Duplicar el arreglo a tamaño 2N y ejecutar Kadane tradicional; esto permite erróneamente tomar subarreglos con más de N elementos, produciendo respuestas incorrectas.
Pregunta 15: En el diseño de una Caché LFU (Least Frequently Used) en O(1) estricto, ¿por qué es obligatoria la arquitectura de doble nivel con listas doblemente enlazadas?
Respuesta:
- (a) Intuición: No puedes ordenar las frecuencias con un Heap porque actualizar la frecuencia de una clave tomaría O(log N). En su lugar, creas casilleros numerados por frecuencia (casillero de 1 punto, casillero de 2 puntos, casillero de 3 puntos...). Dentro de cada casillero, las personas se forman en una fila ordenada por tiempo de llegada (LRU).
- (b) Demostración de O(1) Estricto:
- Nivel 1:
key_to_idx: HashMap<Key, Index>permite ubicar el nodo en memoria en O(1). - Nivel 2:
freq_to_list: HashMap<Freq, DoublyLinkedList>mantiene la cola de elementos de cada frecuencia. Desvincular un nodo de su lista actual e insertarlo al inicio de la lista f + 1 modifica exactamente 4 punteros adyacentes en O(1). - Variable
min_freq: Si la lista de frecuencia mínima queda vacía tras la promoción, min_freq simplemente se incrementa en +1 en O(1). - (c) En Rust: Se implementa sobre una arena contigua
Vec<Option<Node>>para eliminar punteros inseguros y fragmentación del Heap. - (d) Error de Junior: Proponer un Min-Heap ordenado por frecuencia creyendo que desaloja en O(1), ignorando que
get(key)tomaría O(N) o requeriría un heap indexado complejo.
Pregunta 16: ¿Cuál es la diferencia algorítmica fundamental entre Trapping Rain Water 1D (LeetCode 42) y Trapping Rain Water 2D (LeetCode 407)?
Respuesta:
- (a) Intuición: En 1D el agua está atrapada en un callejón recto: solo puede derramarse por la izquierda o por la derecha. En 2D estás en un campo abierto: el agua puede escaparse por cualquier curva, esquina o diagonal hacia el contorno exterior.
- (b) Definición Técnica:
- 1D: El agua sobre la celda i depende de min(max_{izq}, max_{der}) - hᵢ. Se resuelve en O(N) tiempo y O(1) espacio con Dos Punteros Convergentes.
- 2D: Es un problema de grafos donde la fuga depende del cuello de botella de menor elevación en cualquier camino cerrado hacia el borde perimetral exterior. Se resuelve en O(M · N log(M · N)) con un Min-Heap de contorno perimétrico y propagación BFS de tipo Dijkstra.
- (c) En Rust: En 2D se utiliza
BinaryHeap<Cell>con traitOrdinvertido para actuar como Min-Heap. - (d) Error de Junior: Intentar aplicar la solución de Dos Punteros 1D fila por fila o columna por columna en la matriz 2D.
Pregunta 17: En Sliding Window Maximum (LeetCode 239), ¿por qué una Priority Queue tradicional es subóptima comparada con un Monotonic Deque?
Respuesta:
- (a) Intuición: Una Priority Queue te cobra peaje logarítmico cada vez que entra un dato (O(log K)) y acumula datos muertos en su interior. Un Monotonic Deque expulsa a los candidatos inútiles de forma gratuita y te entrega el máximo en el frente en 0 pasos (O(1) directo).
- (b) Comparación Formal de Complejidad:
- Priority Queue / Heap: Inserción en O(log K). Al procesar N elementos: O(N log K) tiempo. Espacio O(K).
- Monotonic Deque: Cada elemento entra al deque como máximo una vez y sale como máximo una vez. Tiempo amortizado: estrictamente O(N) lineal. Espacio acotado a O(K).
- (c) En Rust: Se utiliza
VecDeque<usize>almacenando los índices en un búfer circular contiguo. - (d) Error de Junior: Almacenar los valores de los elementos en lugar de sus índices en el deque, perdiendo la capacidad de detectar en O(1) si un elemento caducó de la ventana activa.
Pregunta 18: En K-Nearest Neighbors (KNN), ¿por qué es indispensable aplicar Normalización Min-Max sobre las características numéricas y qué es la Maldición de la Dimensionalidad?
Respuesta:
- (a) Intuición: Si comparas personas evaluando su edad (entre 18 y 80) y su sueldo (entre $10,000 y $500,000), una diferencia insignificante de $2,000 en el sueldo equivale a 2000² = 4,000,000 en la fórmula de distancia, aplastando por completo a la edad como si no existiera. Normalizar es poner a todas las variables a competir en una misma escala justa de 0 a 1.
- (b) Fórmulas Matemáticas:
- Min-Max: x_{norm} = (x - min(X)) / (max(X) - min(X)) ∈ [0.0, 1.0].
- Maldición de la Dimensionalidad: En dimensión D alta, el volumen de un hipercubo unitario crece tan rápido que para capturar el 10% de los datos vecinos se necesita abarcar casi el 90% del rango de cada dimensión (e = 0.1^{1/D}). Todos los puntos se vuelven equidistantes entre sí, destruyendo el significado geométrico de "vecino cercano".
- (c) En Rust: Se calcula iterando columnas sobre slices contiguos de punto flotante
&[f64]. - (d) Error de Junior: Entrenar modelos de recomendación basados en distancia euclidiana directa sobre vectores de texto sin normalizar.
Bloque IV: Sistemas Distribuidos y Algoritmos Probabilísticos (Preguntas 19 a 24)#
Pregunta 19: ¿Por qué un Filtro de Bloom garantiza matemáticamente cero falsos negativos y cuál es la fórmula de su tasa de falsos positivos?
Respuesta:
- (a) Intuición: Si insertaste un nombre, sus 3 bombillas asociadas fueron encendidas con un interruptor que nadie puede apagar. Si alguien viene a consultar y al menos una de sus 3 bombillas está apagada, es una certeza matemática absoluta del 100% que ese nombre jamás fue insertado.
- (b) Demostración Formal:
- Al insertar x, los bits en h₁(x), …, hₖ(x) se colocan en 1. En un filtro de Bloom estándar, los bits nunca se apagan a 0. Por ende, si x fue insertado, una consulta siempre verá sus k bits en 1 ⟹ Imposible obtener falso negativo.
- Tasa de Falsos Positivos: p ≈ (1 - e^{-(k n) / (m)})ᵏ.
- Con k = (m) / (n) ln 2, se reduce a p ≈ 2⁻ᵏ. Con ~9.6 bits por elemento, p = 1%.
- (c) En Rust: Se modela sobre
Vec<u64>empaquetando 64 bits por palabra entera en memoria contigua. - (d) Error de Junior: Implementar borrados apagando bits individuales, lo que genera falsos negativos catastróficos para otros elementos que compartían esos mismos bits.
Pregunta 20: ¿Cómo estima HyperLogLog (HLL) la cardinalidad de miles de millones de elementos en apenas 1.5 KB de RAM y por qué utiliza la media armónica?
Respuesta:
- (a) Intuición: Es la analogía del récord de caras consecutivas al lanzar una moneda. Si un hash pseudoaleatorio produce una racha de 10 ceros consecutivos al inicio, infieres que el flujo vio alrededor de 2¹⁰ = 1024 elementos únicos.
- (b) Mecánica de Silicio:
- Un hash de 64 bits se parte en b bits para seleccionar el registro j ∈ [0, 2^b - 1], y los 64-b bits restantes se evalúan contando ceros líderes con la instrucción de CPU
LZCNT(leading_zeros()). - Cada registro M[j] guarda el récord máximo de ceros observado.
- La Media Armónica H = frac{m}{sum 2^{-M[j]}} es obligatoria porque neutraliza el impacto desproporcionado de rachas de suerte extremas (outliers) que inflarían el estimado a trillones bajo una media aritmética ordinaria.
- (c) En Rust: Con b = 11 (m = 2048 registros), se utiliza un vector
Vec<u8>de solo 2 KB en el Heap. - (d) Error de Junior: Guardar identificadores de usuario en un
HashSet<String>para métricas diarias en sistemas con cientos de millones de eventos, provocando fallos por falta de memoria RAM.
Pregunta 21: En motores de búsqueda como Elasticsearch o Lucene, ¿cómo se representa en memoria un Índice Invertido y cómo se calcula el ranking TF-IDF?
Respuesta:
- (a) Intuición: Es el índice temático al final de un libro: en lugar de buscar qué palabras tiene la página 50, buscas la palabra "árbol" y ves la lista de páginas donde aparece:
[12, 45, 99]. - (b) Definición Técnica:
- Estructura: Diccionario léxico que asocia cada término t con su lista contigua de ocurrencias (Posting List).
- Puntuación:
TF-IDF(t, d, D) = underbrace{(freq(t, d)) / (longitud(d))}_{TF (Relevancia local)} × underbrace{(ln((1 + |D|) / (1 + DF(t))) + 1)}_{IDF (Discriminación global)}
- (c) En Rust: Se implementa como
HashMap<String, Vec<Posting>>donde cadaPostingcontiene(doc_id, term_frequency)ordenado crecientemente para permitir intersecciones booleanas en O(L₁ + L₂) con dos punteros. - (d) Error de Junior: Hacer búsquedas textuales mediante escaneos de subcadenas con
LIKE '%texto%'en bases de datos relacionales, destruyendo el tiempo de respuesta.
Pregunta 22: ¿Cómo detecta el algoritmo Simhash de Moses Charikar casi-duplicados a escala web y cómo se calcula la distancia de Hamming a nivel de hardware?
Respuesta:
- (a) Intuición: Cada palabra de un documento vota +1 o -1 en un panel de 64 casillas según sus bits. Los votos se acumulan ponderados por la importancia de las palabras. Al final, las casillas que terminaron con saldo positivo se quedan con un 1, generando una huella digital de 64 bits. Si dos artículos difieren en apenas un par de palabras, sus huellas difieren en a lo sumo 3 bits.
- (b) Demostración Matemática: Simhash proyecta vectores sobre hiperplanos aleatorios gaussianos. La probabilidad de que un bit difiera entre dos documentos con ángulo θ es P(bitᵢ(u) ≠ bitᵢ(v)) = (θ) / (π).
- (c) En Silicio (Rust): La distancia de Hamming se computa como:
let dist = (fp1 ^ fp2).count_ones(); // Compila directamente a la instrucción x86 POPCNT en 1 ciclo
- (d) Error de Junior: Usar hashes criptográficos (MD5, SHA-256) para comparar similitud de texto; debido al efecto avalancha, un solo carácter cambiado altera el 50% de los bits.
Pregunta 23: ¿Qué afirma la Identidad de Bézout en teoría de números y cómo permite el Algoritmo de Euclides Extendido calcular inversos multiplicativos modulares?
Respuesta:
- (a) Intuición: Dados dos números enteros a y b, siempre puedes combinarlos multiplicándolos por dos enteros x e y (uno positivo y otro negativo) para que la resta dé exactamente su Máximo Común Divisor: a · x + b · y = gcd(a, b).
- (b) Aplicación al Inverso Modular:
- Deseamos hallar x tal que a · x ≡ 1 ±od m.
- Si gcd(a, m) = 1 (son coprimos), por Bézout existen enteros x, y tales que:
a · x + m · y = 1
- Aplicando módulo m a ambos lados:
(a · x) + 0 ≡ 1 ±od m ⟹ a · x ≡ 1 ±od m
- ¡El coeficiente de Bézout x (normalizado a positivo) es el inverso modular exacto!
- (c) En Rust: Se retorna como
Option<i64>. Sigcd != 1, retornaNone. - (d) Error de Junior: Intentar calcular inversos modulares mediante bucles de búsqueda lineal en O(m), lo que colapsa de inmediato para módulos criptográficos de 64 o 2048 bits.
Pregunta 24: ¿En qué principio algebraico se basa el protocolo de Intercambio de Claves Diffie-Hellman y qué problema computacional garantiza su seguridad?
Respuesta:
- (a) Intuición: Dos personas que hablan por un canal público pueden mezclar colores públicos con sus propios colores privados secretos y compartir las mezclas intermedias. Al final, ambos obtienen la misma mezcla secreta idéntica, mientras que un espía que ve los intercambios no puede separar los colores para deducirla.
- (b) Fundamento Matemático:
- Alicia elige a privado, publica A = g^a ±od p.
- Bob elige b privado, publica B = g^b ±od p.
- Alicia calcula S = B^a ±od p = (g^b)^a ±od p = g^{ab} ±od p.
- Bob calcula S = A^b ±od p = (g^a)^b ±od p = g^{ab} ±od p.
- Ambos obtienen el mismo secreto compartido g^{ab} ±od p.
- (c) Dureza Criptográfica: Se basa en la intratabilidad computacional del Problema del Logaritmo Discreto (DLP): dados g, p y g^a ±od p, es computacionalmente imposible deducir el exponente a en tiempo polinomial en computación clásica.
- (d) Error de Junior: Usar números primos pequeños o generadores que no generen un subgrupo cíclico de orden primo grande, permitiendo ataques de factorización o Pohlig-Hellman.
Bloque V: Escalabilidad, Resiliencia y Maestría de Diseño (Preguntas 25 a 30)#
Pregunta 25: ¿Por qué el algoritmo de Hashing Consistente con nodos virtuales (Vnodes) previene tormentas de caídas de caché (Cache Stampede) al redimensionar un clúster distribuido?
Respuesta:
- (a) Intuición: En el reparto ingenuo con módulo (hash % N), si tienes 10 servidores y agregas el número 11, el 91% de todas las claves de internet se cambian de servidor de golpe, vaciando las cachés y tumbando las bases de datos. En el anillo consistente, el nuevo servidor solo le quita una pequeña rebanada a su vecino inmediato; los otros 9 servidores conservan sus datos intactos.
- (b) Demostración Matemática:
- Hashing Módulo: fracción de claves reasignadas = (N) / (N+1) ≈ 90.9% para N=10.
- Consistent Hashing: fracción de claves reasignadas = (1) / (N+1) ≈ 9.1%.
- Los Vnodes asignan V réplicas virtuales por cada servidor físico (ej. V = 150), fragmentando el anillo y reduciendo la varianza en la carga de claves a O(1/√(V)) para evitar puntos calientes (hotspots).
- (c) En Rust: El anillo se mantiene ordenado en el Heap mediante un
BTreeMap<u64, String>, localizando el servidor en O(log V) con.range(hash..).next(). - (d) Error de Junior: Omitir los nodos virtuales, provocando que por pura varianza estocástica un solo servidor físico reciba el 70% de todo el tráfico del clúster.
Pregunta 26: ¿Cómo opera la reposición perezosa (Lazy Refill) en el algoritmo Token Bucket y por qué supera al uso de temporizadores o hilos de fondo?
Respuesta:
- (a) Intuición: En lugar de contratar a un empleado para que eche agua al balde segundo a segundo cuando nadie está mirando, simplemente dejas el balde solo. Cuando un cliente llega a pedir agua, miras tu reloj, calculas cuántos segundos pasaron desde la última visita y le agregas de golpe toda el agua correspondiente.
- (b) Mecánica de Silicio en O(1):
Cero hilos durmientes (sleep), cero temporizadores del sistema operativo, cero cambios de contexto de CPU (context switches).
- (c) En Rust: Operación matemática en el Stack con flotantes
f64, ejecutada en menos de 5 nanosegundos. - (d) Error de Junior: Crear un hilo o tarea
asyncperiódica por cada uno de los 500,000 clientes concurrentes de una API para reponer tokens.
Pregunta 27: En el problema Super Egg Drop (LeetCode 887), ¿cómo transforma la Inversión Dual la recurrencia para reducir la complejidad de O(K N²) a O(K · M)?
Respuesta:
- (a) Intuición: Cambias la pregunta de difícil a fácil. En lugar de preguntar: "Con K huevos y 10,000 pisos, ¿cuántos tiros necesito?", preguntas: "Si me das K huevos y me permites hacer M tiros, ¿cuál es el edificio más alto que puedo descifrar?".
- (b) Relación de Recurrencia Dual:
- Si el huevo se rompe en el tiro, podemos resolver dp[m-1][k-1] pisos hacia abajo.
- Si no se rompe, podemos resolver dp[m-1][k] pisos hacia arriba.
- Sumamos el piso actual de prueba (+1).
- La iteración se detiene en cuanto dp[m][k] ≥ N.
- (c) En Rust: Comprime el espacio a un vector 1D
Vec<usize>de tamaño K + 1 iterado en reversa, ejecutándose en menos de 2 milisegundos. - (d) Error de Junior: Intentar resolver N = 10,000 con la DP clásica de matriz N², recibiendo un veredicto de Time Limit Exceeded.
Pregunta 28: En Regular Expression Matching (LeetCode 10), ¿cómo modela la celda dp[i][j] la semántica del operador asterisco p[j-1] == '*'?
Respuesta:
- (a) Intuición: El asterisco te da dos opciones: o finges que no existe ignorando la letra previa (0 repeticiones, saltando 2 columnas atrás en el patrón), o si la letra actual de tu texto coincide con la letra del asterisco, consumes una letra de tu texto y te quedas esperando en el mismo patrón para ver si hay más letras iguales.
- (b) Ecuación de Transición de Estado:
- (c) En Rust: Requiere convertir
&straVec<char>para garantizar indexación O(1) sin violar los invariantes de UTF-8. - (d) Error de Junior: Confundir el operador de expresiones regulares (donde
*califica al carácter previo) con el wildcard de consola (donde*es un comodín independiente).
Pregunta 29: En Sliding Window Median (LeetCode 480), ¿por qué es obligatoria la técnica de eliminación perezosa (Lazy Removal) y cómo interactúa con el rebalanceo de los dos montículos?
Respuesta:
- (a) Intuición: En un montículo binario no puedes sacar a alguien que esté atrapado en las ramas del fondo sin romper la estructura. Simplemente anotas su nombre en una lista de espera de eliminaciones pendientes. Solo cuando ese elemento asoma su cabeza en la cima del montículo lo expulsas físicamente.
- (b) Mecánica de Control:
- Mantenemos contadores lógicos de tamaño real
small_sizeylarge_sizeque se decrementan en O(1) cuando un elemento caduca de la ventana. - Los rebalanceos de
balance()se rigen por los tamaños lógicos, no por la cantidad de nodos físicos que residen en los montículos. - La rutina
prune()purga las cimas de los montículos antes de extraer la mediana o transferir elementos. - (c) En Rust: El Max-Heap usa
BinaryHeap<i64>y el Min-Heap usaBinaryHeap<Reverse<i64>>. Los valores se promueven ai64para prevenir desbordamientos en la media(a + b) / 2.0. - (d) Error de Junior: Usar
BTreeSeten Rust creyendo que es un multiconjunto;BTreeSetcolapsa duplicados numéricos legítimos, destruyendo el cálculo de la mediana.
Pregunta 30: ¿Cuál es la rúbrica mental que distingue a un Ingeniero Staff / Principal L6+ frente a un Ingeniero Senior durante una ronda de diseño y optimización algorítmica?
Respuesta:
- (a) Intuición: Un desarrollador júnior o sénior se enfoca en hacer que el código compile y pase los tests. Un Ingeniero Staff ve el código como un componente de un sistema socio-técnico físico: evalúa el costo de la memoria caché del procesador, el impacto en la red, la predictibilidad de la latencia, la observabilidad, la resiliencia ante fallos catastróficos y la simplicidad arquitectónica para el equipo humano.
- (b) Los 5 Pilares de Excelencia Staff:
- Dominio de Invariantes y Cero Magia: Demuestra formalmente por qué la solución funciona antes de escribir una línea de código.
- Control Total de Recursos Físicos: Selecciona intencionalmente estructuras de memoria contigua en el Heap para maximizar líneas de caché L1/L2 y minimizar la fragmentación de la memoria virtual.
- Manejo Sistemático de Casos Extremos: Anticipa desbordamientos aritméticos (
u128,i64), nulos, colecciones vacías y ordenamiento estable sin depender de supuestos optimistas. - Honestidad Intelectual de Compensaciones (Trade-offs): Distingue con precisión entre complejidad en el peor caso, promedio y amortizada. Sabe cuándo renunciar al 100% de exactitud a cambio de estructuras probabilísticas (Bloom, HLL, Simhash) que reducen los costos de infraestructura en un 99%.
- Claridad y Liderazgo de Comunicación: Explica conceptos complejos desde primeros principios sin utilizar jerga vacía, guiando al entrevistador o al equipo de ingeniería con rigor y empatía técnica.