💡 Nota conceptual: compresión y entropía La compresión sin pérdida explota la redundancia estadística o estructural de los datos para reducir su tamaño, garantizando que la reconstrucción sea idéntica al original. El primer teorema de Shannon establece que la longitud promedio de cualquier código sin pérdida nunca puede ser menor que la entropía de la fuente: \( \bar{\ell} \geq H(X) \). Los algoritmos de esta semana (Huffman y Lempel-Ziv) buscan acercarse a ese límite desde distintas estrategias: Huffman aprovecha la redundancia estadística (símbolos más frecuentes → códigos más cortos); Lempel-Ziv aprovecha la redundancia estructural (patrones repetidos → referencias compactas).

Codificación Huffman Clásica

🔒 VaultSec Bank — Sistema de Auditoría de Transacciones
Ejercicio 1 Construcción del árbol de Huffman y análisis de eficiencia

VaultSec Bank registra eventos de auditoría en su plataforma de banca en línea. Cada evento se clasifica en una de cinco categorías. El equipo de TI ha analizado millones de registros y obtenido la siguiente distribución de probabilidades:

  • LOGIN_OK — Autenticación exitosa: p = 0.40
  • CONSULTA — Consulta de saldo o movimientos: p = 0.25
  • TRANSFER — Transferencia procesada: p = 0.20
  • SESION_EXP — Sesión expirada por inactividad: p = 0.10
  • LOGIN_FAIL — Autenticación fallida: p = 0.05

Actualmente cada evento se almacena con un código de longitud fija de 3 bits (necesarios para distinguir 5 categorías con código uniforme).

a) Construya el árbol de Huffman paso a paso y determine los códigos asignados.
b) Calcule la longitud promedio \(\bar{\ell}\) del código Huffman.
c) Calcule la entropía H(X) de la fuente y compare con \(\bar{\ell}\).
d) Calcule el ahorro en bits respecto al código de longitud fija de 3 bits.
e) ¿Cuál es la eficiencia del código Huffman obtenido?
Fórmulas aplicadas en este ejercicio
Longitud promedio del código
\[ \bar{\ell} = \sum_{x} p(x) \cdot \ell(x) \]

donde \(\ell(x)\) es la longitud en bits del código asignado al símbolo \(x\).

Entropía de la fuente (límite teórico inferior)
\[ H(X) = -\sum_{x} p(x)\,\log_2 p(x) \]
Eficiencia del código
\[ \eta = \frac{H(X)}{\bar{\ell}} \times 100\% \]
Primer Teorema de Shannon (desigualdad de Kraft)
Límite inferior: \( H(X) \leq \bar{\ell} < H(X) + 1 \)
Interpretación: Huffman siempre queda dentro de 1 bit por encima de la entropía

¿Qué busca el algoritmo de Huffman? Construir un código prefijo (ningún código es prefijo de otro, lo que garantiza decodificación única) donde los símbolos más frecuentes reciben los códigos más cortos y los menos frecuentes los más largos. El árbol binario que se construye de abajo hacia arriba es la estructura que garantiza esta propiedad de forma óptima.

Regla de construcción: en cada paso se fusionan los dos nodos con menor probabilidad acumulada, creando un nodo padre cuya probabilidad es la suma de ambos. Se repite hasta tener un único nodo raíz con probabilidad 1.0. Los códigos se leen desde la raíz hacia las hojas: rama izquierda = 0, rama derecha = 1.

Símbolo Probabilidad p(x) log₂ p(x) −p(x)·log₂p(x)
LOGIN_OK 0.40 −1.322 0.5288
CONSULTA 0.25 −2.000 0.5000
TRANSFER 0.20 −2.322 0.4644
SESION_EXP 0.10 −3.322 0.3322
LOGIN_FAIL 0.05 −4.322 0.2161
TOTAL 1.00 H(X) = 2.0415 bits

Nota: log₂(0.40) = ln(0.40)/ln(2) = −0.9163/0.6931 = −1.3219. La última columna muestra la contribución de cada símbolo a la entropía total.

1
Respuesta (a) — Ordenar los símbolos de menor a mayor probabilidad

El algoritmo de Huffman comienza con todos los símbolos como nodos hoja. Para facilitar la selección de los dos menores en cada iteración, conviene tenerlos ordenados. En la práctica (y en implementaciones computacionales), se usa una cola de prioridad mínima.

Lista inicial (orden ascendente de probabilidad):
LOGIN_FAIL = 0.05 ← menor
SESION_EXP = 0.10 ← segundo menor
TRANSFER = 0.20
CONSULTA = 0.25
LOGIN_OK = 0.40 ← mayor

Siempre se seleccionan los dos primeros de la lista (los de menor probabilidad). Nunca se fusionan símbolos al azar — el orden garantiza la optimalidad del código.

2
Iteración 1 — Fusionar LOGIN_FAIL (0.05) y SESION_EXP (0.10)

Se toman los dos nodos con menor probabilidad y se crea un nodo interno cuya probabilidad es la suma de ambos. Este nodo interno no representa ningún símbolo; es únicamente un punto de bifurcación en el árbol.

Nodo N₁ = LOGIN_FAIL + SESION_EXP
= 0.05 + 0.10 = 0.15
Lista actualizada (reinsertamos N₁, reordenamos):
N₁ (LOGIN_FAIL + SESION_EXP) = 0.15 ← ahora el segundo menor
TRANSFER = 0.20
CONSULTA = 0.25
LOGIN_OK = 0.40

El nodo fusionado N₁ = 0.15 se reinserta en la lista en su posición correcta por probabilidad. La lista ahora tiene 4 elementos (pasamos de 5 nodos a 4).

3
Iteración 2 — Fusionar N₁ (0.15) y TRANSFER (0.20)

Nuevamente tomamos los dos menores de la lista actualizada: N₁ = 0.15 y TRANSFER = 0.20.

Nodo N₂ = N₁ + TRANSFER
= 0.15 + 0.20 = 0.35
Lista actualizada:
CONSULTA = 0.25 ← menor
N₂ (N₁ + TRANSFER) = 0.35
LOGIN_OK = 0.40

Nótese que N₂ = 0.35 contiene en su interior a N₁ = 0.15, que a su vez contiene a LOGIN_FAIL y SESION_EXP. Vamos construyendo el árbol de hojas hacia la raíz.

4
Iteración 3 — Fusionar CONSULTA (0.25) y N₂ (0.35)

Los dos menores ahora son CONSULTA = 0.25 y N₂ = 0.35.

Nodo N₃ = CONSULTA + N₂
= 0.25 + 0.35 = 0.60
Lista actualizada:
LOGIN_OK = 0.40 ← menor
N₃ (CONSULTA + N₂) = 0.60

Solo quedan dos nodos en la lista. La próxima fusión producirá la raíz.

5
Iteración 4 — Fusionar LOGIN_OK (0.40) y N₃ (0.60) → Raíz

Esta es la última fusión. La raíz siempre tiene probabilidad 1.0 porque concentra todos los eventos posibles.

Raíz = LOGIN_OK + N₃
= 0.40 + 0.60 = 1.00
El árbol está completo. Número de fusiones = n − 1 = 5 − 1 = 4 ✓

Para un árbol con n hojas, siempre se realizan exactamente n − 1 fusiones. Con 5 símbolos → 4 fusiones.

6
Asignación de códigos binarios — recorrido del árbol

Se recorre el árbol desde la raíz hacia cada hoja. En cada bifurcación: rama izquierda = 0, rama derecha = 1. El código de cada símbolo es la secuencia de 0s y 1s acumulada al bajar desde la raíz hasta esa hoja.

Estructura del árbol (raíz = 1.00):
Raíz (1.00)
├── 0 → LOGIN_OK (0.40) → código: 0
└── 1 → N₃ (0.60)
├── 0 → CONSULTA (0.25) → código: 10
└── 1 → N₂ (0.35)
├── 0 → TRANSFER (0.20) → código: 110
└── 1 → N₁ (0.15)
├── 0 → SESION_EXP (0.10) → código: 1110
└── 1 → LOGIN_FAIL (0.05) → código: 11110

Verificación del código prefijo: ningún código es prefijo de otro (ej. "0" no es inicio de "10", "110", etc.). Esto garantiza que la decodificación sea siempre única y sin ambigüedad.

Símbolo p(x) Código Huffman Longitud ℓ(x) p(x) · ℓ(x)
LOGIN_OK 0.40 0 1 bit 0.40
CONSULTA 0.25 10 2 bits 0.50
TRANSFER 0.20 110 3 bits 0.60
SESION_EXP 0.10 1110 4 bits 0.40
LOGIN_FAIL 0.05 11110 5 bits 0.25
Longitud promedio \(\bar{\ell}\) = Σ p(x)·ℓ(x) 2.15 bits/símbolo
7
Respuesta (b) — Longitud promedio \(\bar{\ell}\)

La longitud promedio pondera la longitud de cada código por la frecuencia con que aparece ese símbolo. Los símbolos muy frecuentes (LOGIN_OK con p=0.40, código de solo 1 bit) contribuyen poco al total; los raros contribuyen más en longitud pero compensan porque su ponderación es pequeña.

ℓ̄ = p(LOGIN_OK)·ℓ(LOGIN_OK) + p(CONSULTA)·ℓ(CONSULTA) + ...
= 0.40·1 + 0.25·2 + 0.20·3 + 0.10·4 + 0.05·5
= 0.40 + 0.50 + 0.60 + 0.40 + 0.25
ℓ̄ = 2.15 bits/símbolo

Esto significa que, en promedio, cada evento de auditoría de VaultSec Bank se codifica con 2.15 bits usando Huffman.

8
Respuesta (c) — Entropía H(X) y comparación con \(\bar{\ell}\)

La entropía H(X) es el límite teórico mínimo que ningún código sin pérdida puede cruzar. Ya fue calculada en la tabla de datos; aquí verificamos el cálculo completo término por término.

H(X) = −[0.40·log₂(0.40) + 0.25·log₂(0.25) + 0.20·log₂(0.20)
+ 0.10·log₂(0.10) + 0.05·log₂(0.05)]
= −[0.40·(−1.322) + 0.25·(−2.000) + 0.20·(−2.322)
+ 0.10·(−3.322) + 0.05·(−4.322)]
= −[−0.5288 − 0.5000 − 0.4644 − 0.3322 − 0.2161]
H(X) = 2.0415 bits/símbolo
Comparación con la desigualdad de Shannon:
H(X) = 2.0415 bits ← límite teórico mínimo
ℓ̄ (Huffman) = 2.1500 bits ← longitud promedio obtenida
H(X) + 1 = 3.0415 bits ← límite superior garantizado
✓ H(X) ≤ ℓ̄ < H(X)+1 → 2.0415 ≤ 2.15 < 3.0415

El código Huffman cumple la garantía del Primer Teorema de Shannon: queda a solo 0.1085 bits por encima de la entropía, es decir, es casi óptimo.

9
Respuesta (d) — Ahorro respecto al código de longitud fija de 3 bits

Un código de longitud fija asigna a todos los símbolos el mismo número de bits. Para distinguir 5 categorías se necesitan ⌈log₂(5)⌉ = 3 bits (con 3 bits se representan hasta 8 valores distintos). Huffman aprovecha la desigualdad de probabilidades para reducir ese promedio.

Código longitud fija: ℓ_fijo = 3 bits/símbolo (para 5 categorías)
Código Huffman: ℓ̄ = 2.15 bits/símbolo
Ahorro absoluto = 3.00 − 2.15 = 0.85 bits/símbolo
Ahorro relativo = 0.85 / 3.00 = 28.3%

Si VaultSec Bank genera 10 millones de eventos por día, Huffman ahorra 0.85 × 10⁷ = 8.5 millones de bits diarios (~1.06 MB/día) solo en el log de auditoría.

10
Respuesta (e) — Eficiencia del código Huffman

La eficiencia mide qué tan cerca está la longitud promedio obtenida del mínimo teórico posible (la entropía). Una eficiencia del 100% significaría que se ha alcanzado exactamente la entropía, lo cual solo ocurre cuando todas las probabilidades son potencias de 2 (p.ej., 0.5, 0.25, 0.125...).

η = H(X) / ℓ̄ × 100%
= 2.0415 / 2.1500 × 100%
η = 94.95% ≈ 95%

El código Huffman obtenido usa en promedio solo un 5% más de bits que el mínimo teórico posible. Para la mayoría de aplicaciones prácticas, esta eficiencia es excelente.

H(X) — límite de Shannon
2.04
2.0415 bits
ℓ̄ Huffman
2.15
2.1500 bits
Código fijo (3 bits)
3.00
3.0000 bits

La barra verde es el mínimo teórico (entropía). La barra azul oscuro es Huffman — muy cercana al mínimo. La barra roja es el código de longitud fija actual.

Conclusión para VaultSec Bank El código Huffman para los eventos de auditoría logra una longitud promedio de 2.15 bits/símbolo, frente a 3 bits del esquema actual. Esto representa un ahorro del 28.3% y una eficiencia del 95% respecto a la entropía teórica. Para la empresa, esto se traduce directamente en menor almacenamiento, menor ancho de banda de transmisión y menor latencia en el sistema de auditoría en tiempo real — sin pérdida de ningún evento.

Codificación Huffman Avanzada · Adaptativo y por Bloques

🛡 CipherWatch S.A. — Monitor de Alertas en Tiempo Real
Ejercicio 2 Huffman adaptativo vs. Huffman por bloques en flujo de alertas

CipherWatch S.A. recibe un flujo continuo de alertas de seguridad desde sensores distribuidos. No se conocen de antemano las probabilidades de cada tipo de alerta. El sistema registra la siguiente secuencia de eventos durante un intervalo de monitoreo:

A   B   A   C   A   B   A   C   A   B

Donde: A = ACCESO_NORMAL · B = ANOMALIA_LEVE · C = ANOMALIA_CRITICA

a) Aplique Huffman adaptativo: actualice las frecuencias símbolo a símbolo y calcule los códigos finales.
b) Calcule la longitud total codificada con Huffman adaptativo.
c) Aplique Huffman por bloques de tamaño 2: identifique los bigramas, asigne códigos y calcule la longitud total.
d) Compare los tres métodos (longitud fija, adaptativo, por bloques) y explique por qué el método por bloques comprime más.
Fórmulas y conceptos aplicados en este ejercicio
Longitud total de la cadena codificada
\[ L_{total} = \sum_{x} f(x) \cdot \ell(x) \]

donde \(f(x)\) es la frecuencia absoluta del símbolo \(x\) y \(\ell(x)\) su longitud en bits.

Entropía de bloques
Principio: Al agrupar símbolos en bloques de tamaño \(k\), la entropía por símbolo se reduce si hay dependencia entre ellos: \(H(\text{bloque})/k \leq H(X)\)
Ahorro por bloques
\[ \text{Ahorro} = \frac{L_{sin} - L_{con}}{L_{sin}} \times 100\% \]

¿Por qué Huffman adaptativo? El Huffman clásico requiere conocer las probabilidades antes de codificar. En flujos de red en tiempo real, esas probabilidades no están disponibles de antemano. El Huffman adaptativo resuelve esto aprendiendo la distribución mientras codifica: comienza con un árbol vacío y lo actualiza tras cada símbolo recibido.

¿Por qué Huffman por bloques? Al agrupar símbolos en pares o secuencias más largas, el algoritmo puede capturar redundancia contextual: la tendencia de ciertos símbolos a aparecer juntos. Si el par AB aparece frecuentemente, tratarlo como una unidad y asignarle un código corto es más eficiente que codificar A y B por separado.

Huffman Adaptativo
Huffman por Bloques
1
Estado inicial — árbol vacío (NYT: Not Yet Transmitted)

El árbol comienza con un único nodo especial llamado NYT (Not Yet Transmitted). Cuando aparece un símbolo nuevo, se emite primero el código del nodo NYT (que indica "símbolo nuevo sigue") más el símbolo en binario crudo. Cuando aparece un símbolo ya conocido, se emite directamente su código Huffman actual.

Estado inicial:
Árbol = {NYT} Tabla de frecuencias: vacía
2
Lectura símbolo a símbolo y actualización de frecuencias

En el Huffman adaptativo simplificado que aplicamos aquí, procesamos toda la cadena y registramos cómo evoluciona la tabla de frecuencias. En la práctica, el árbol se reconstruiría tras cada símbolo, pero el resultado final del código viene de la distribución completa aprendida.

Posición Símbolo leído Acción f(A) f(B) f(C)
1A Símbolo nuevo — añadir al árbol 100
2B Símbolo nuevo — añadir al árbol 110
3A Ya existe — incrementar frecuencia 210
4C Símbolo nuevo — añadir al árbol 211
5A Ya existe — incrementar frecuencia 311
6B Ya existe — incrementar frecuencia 321
7A Ya existe — incrementar frecuencia 421
8C Ya existe — incrementar frecuencia 422
9A Ya existe — incrementar frecuencia 522
10B Ya existe — incrementar frecuencia 532
3
Distribución final aprendida — construcción del árbol Huffman

Tras procesar los 10 símbolos, las frecuencias y probabilidades aprendidas son:

Símbolo A: f=5 → p = 5/10 = 0.50
Símbolo B: f=3 → p = 3/10 = 0.30
Símbolo C: f=2 → p = 2/10 = 0.20
Construcción del árbol (los dos menores primero: C y B):
Iter 1: fusionar C(0.20) + B(0.30) → N₁ = 0.50
Lista: N₁(0.50), A(0.50)
Iter 2: fusionar N₁(0.50) + A(0.50) → Raíz = 1.00
Árbol final:
Raíz (1.00)
├── 0 → A (0.50) → código: 0
└── 1 → N₁ (0.50)
├── 0 → B (0.30) → código: 10
└── 1 → C (0.20) → código: 11
4
Respuesta (b) — Longitud total con Huffman adaptativo

Con los códigos obtenidos de la distribución aprendida, codificamos la cadena completa A B A C A B A C A B:

Cadena: A B A C A B A C A B
Código: 0 10 0 11 0 10 0 11 0 10
Longitud por símbolo:
A: aparece 5 veces × 1 bit = 5 bits
B: aparece 3 veces × 2 bits = 6 bits
C: aparece 2 veces × 2 bits = 4 bits
L_total (Huffman adaptativo) = 5 + 6 + 4 = 15 bits

Con código de longitud fija (2 bits para 3 símbolos): 10 símbolos × 2 bits = 20 bits. Ahorro: 5 bits (25%).

1
Respuesta (c) — Paso 1: Segmentación en bigramas (bloques de 2)

En lugar de codificar cada símbolo individualmente, agrupamos la cadena en pares consecutivos no solapados. La cadena A B A C A B A C A B tiene 10 símbolos → 5 bigramas.

Cadena original: A B A C A B A C A B
Bigramas: [A B][A C][A B][A C][A B]
AB AC AB AC AB

Al segmentar, el "alfabeto" pasa de ser {A, B, C} a ser el conjunto de bigramas observados: {AB, AC}. Si hubieran aparecido combinaciones como AA, BC, CB, etc., también se incluirían.

2
Paso 2: Frecuencias y probabilidades de los bigramas

Contamos cuántas veces aparece cada bigrama en la segmentación:

BigramaFrecuenciaProbabilidadInterpretación
AB 3 3/5 = 0.60 Acceso normal seguido de anomalía leve
AC 2 2/5 = 0.40 Acceso normal seguido de anomalía crítica

El bigrama AB aparece 3 de 5 veces → es el patrón dominante. El algoritmo le asignará el código más corto.

3
Paso 3: Construcción del árbol Huffman sobre bigramas

Con solo 2 bigramas, el árbol tiene una única fusión posible. La raíz = 0.60 + 0.40 = 1.00. El bigrama más frecuente (AB = 0.60) queda en la rama izquierda (código 0) y el menos frecuente (AC = 0.40) en la derecha (código 1).

Árbol de bigramas:
Raíz (1.00)
├── 0 → AB (0.60) → código: 0 (1 bit por bigrama = 0.5 bits/símbolo)
└── 1 → AC (0.40) → código: 1 (1 bit por bigrama = 0.5 bits/símbolo)
Longitud promedio del código de bigramas:
ℓ̄_bloque = 0.60·1 + 0.40·1 = 1.00 bit/bigrama
ℓ̄_bloque por símbolo = 1.00 / 2 = 0.50 bits/símbolo

Esto es extraordinariamente eficiente: solo 0.5 bits por símbolo. Ocurre porque hay solo 2 bigramas distintos (distribución casi binaria) y la codificación binaria de 2 elementos requiere exactamente 1 bit.

4
Paso 4: Codificación completa de la cadena por bloques
Cadena segmentada: [AB] [AC] [AB] [AC] [AB]
Código de bigramas: 0 1 0 1 0
Cadena codificada: 0 1 0 1 0 → L_total = 5 bits
5
Respuesta (d) — Comparación de los tres métodos

¿Por qué el método por bloques comprime tanto más? Porque detecta que en esta cadena, A siempre aparece seguido de B o de C. Al tratar los pares como unidades, el código captura esa dependencia contextual. El Huffman símbolo a símbolo no puede ver que A predice que el siguiente símbolo será B o C; el Huffman por bloques sí.

Método Bits totales Bits/símbolo Ahorro vs. fijo Diagnóstico
Longitud fija (2 bits) 20 bits 2.00 Referencia
Huffman adaptativo 15 bits 1.50 25.0% Bueno
Huffman por bloques (k=2) 5 bits 0.50 75.0% Óptimo aquí
📌
¿Por qué el método por bloques es tan superior en este caso? La clave está en la redundancia contextual: en esta secuencia, A siempre aparece en primer lugar de cada par. Eso significa que el símbolo A es perfectamente predecible dado el contexto (posición impar de la cadena). Al codificar por bloques, esa predictibilidad se elimina: solo necesitamos 1 bit para saber si el segundo símbolo es B o C. Sin bloques, A y B se codifican como si fueran independientes y gastamos bits en información que ya conocíamos.
Conclusión para CipherWatch S.A. Para flujos de alertas en tiempo real donde las probabilidades no se conocen de antemano, el Huffman adaptativo es la solución apropiada: aprende la distribución sobre la marcha y logra un 25% de ahorro. Si el análisis posterior revela patrones contextuales (como que ciertos eventos siempre se suceden en pares), el Huffman por bloques puede llevar ese ahorro hasta el 75%. La elección del método depende de si se prioriza latencia (adaptativo, procesa símbolo a símbolo) o eficiencia máxima (bloques, requiere agrupar datos primero).

Algoritmo LZ77 · Ventana Deslizante

🔐 FortiLog Systems — Compresión de Tráfico de Red
Ejercicio 3 Compresión de una cadena de eventos de red con LZ77

FortiLog Systems captura secuencias de eventos de tráfico de red y necesita comprimirlas para su almacenamiento eficiente en logs históricos. Una secuencia típica tiene la siguiente forma (cada letra representa un tipo de paquete):

A B C A B C A B C

Se utilizará el algoritmo LZ77 con una ventana de búsqueda de 6 caracteres y un buffer de anticipación de 4 caracteres.

a) Aplique LZ77 paso a paso y genere la secuencia de tokens (offset, longitud, símbolo siguiente).
b) Interprete cada token: ¿qué dice exactamente al decodificador?
c) Calcule el ahorro en caracteres respecto a la representación original.
d) Explique la diferencia conceptual entre LZ77 y Huffman: ¿qué tipo de redundancia explota cada uno?
Estructura y conceptos de LZ77
Formato de cada token LZ77
\[ \text{token} = (\,\text{offset},\; \text{longitud},\; \text{símbolo\_siguiente}\,) \]

offset: cuántas posiciones hacia atrás (en la ventana de búsqueda) comienza la coincidencia  |  longitud: cuántos caracteres coinciden  |  símbolo_siguiente: el carácter que sigue inmediatamente después de la coincidencia

Caso especial: símbolo nuevo (sin coincidencia)
Sin coincidencia: \( (\,0,\; 0,\; \text{símbolo}\,) \)

Cuando no hay ninguna coincidencia en la ventana, se emite offset=0, longitud=0 y el símbolo en crudo.

Dos zonas de LZ77
Ventana de búsqueda: contiene los últimos W caracteres ya procesados (el "pasado")
Buffer de anticipación: contiene los próximos B caracteres aún no procesados (el "futuro")

Intuición de LZ77: Imagine que está redactando un documento y necesita escribir una frase que ya aparece más arriba. En lugar de reescribirla, pondría una nota como "ver párrafo 3, líneas 4-7". LZ77 hace exactamente eso: cuando detecta que una subcadena ya apareció antes en la ventana de búsqueda, emite una "referencia" (offset, longitud) en lugar de repetir los caracteres.

Ventana deslizante: La ventana de búsqueda se desplaza con el cursor de lectura. Contiene solo los últimos W caracteres procesados, no toda la cadena. Esto limita la memoria necesaria y hace el algoritmo eficiente en flujos continuos como el tráfico de red.

🪟 Representación de la ventana deslizante
En cada paso, la posición del cursor divide la cadena en dos zonas:

| ← ventana de búsqueda (máx 6 chars) → | ← buffer anticipación (máx 4 chars) → | resto |

El algoritmo busca en la ventana la coincidencia más larga posible con el inicio del buffer. Siempre se busca la coincidencia más larga para maximizar la compresión.
1
Posición 1 — Cursor en 'A' (ventana vacía)

Al inicio, la ventana de búsqueda está vacía porque no hay ningún carácter previamente procesado. No puede haber coincidencia. Se emite el símbolo directamente con un token de "sin coincidencia".

Ventana de búsqueda: [ vacía ]
Buffer de anticip.: [ A B C A ] ← máximo 4 chars
Búsqueda de 'A' en la ventana: SIN coincidencia
Token 1: (0, 0, 'A') → "símbolo nuevo: A"

El cursor avanza 1 posición (longitud 0 + 1 símbolo siguiente). Ahora la ventana contiene: [A].

2
Posición 2 — Cursor en 'B'

La ventana tiene [A]. El buffer comienza con 'B'. Buscamos 'B' en la ventana: no aparece.

Ventana de búsqueda: [ A ]
Buffer de anticip.: [ B C A B ]
Búsqueda de 'B' en [A]: SIN coincidencia
Token 2: (0, 0, 'B') → "símbolo nuevo: B"

Cursor avanza 1. Ventana: [A B].

3
Posición 3 — Cursor en 'C'

La ventana tiene [A B]. Buscamos 'C': no aparece en la ventana.

Ventana de búsqueda: [ A B ]
Buffer de anticip.: [ C A B C ]
Búsqueda de 'C' en [A B]: SIN coincidencia
Token 3: (0, 0, 'C') → "símbolo nuevo: C"

Cursor avanza 1. Ventana: [A B C].

4
Posición 4 — Cursor en 'A' (primera coincidencia)

La ventana tiene [A B C]. El buffer comienza con 'A B C A'. Buscamos la coincidencia más larga que empiece por 'A' en la ventana. 'A' está en la ventana en la posición 3 hacia atrás (offset=3). ¿Cuántos caracteres coinciden desde ahí? La ventana tiene A en pos −3, luego B en −2, luego C en −1. El buffer tiene A, B, C, A. Coinciden: A·B·C = 3 caracteres. El siguiente carácter del buffer después de la coincidencia es 'A'.

Ventana de búsqueda: [ A B C ] ← posiciones -3, -2, -1 respecto al cursor
Buffer de anticip.: [ A B C A ]
Coincidencia: ventana[-3] = A ✓, ventana[-2] = B ✓, ventana[-1] = C ✓
Longitud de coincidencia = 3
Siguiente símbolo (tras la coincidencia): 'A'
Token 4: (3, 3, 'A') → "retrocede 3, copia 3 chars, luego 'A'"

Este token representa 4 caracteres (3 copiados + 1 símbolo siguiente). El cursor avanza 4 posiciones. Ventana ahora: [A B C A B C] (últimos 6 chars procesados).

5
Posición 8 — Cursor en 'B' (segunda coincidencia)

Cursor está ahora en la posición 8. Ventana contiene los 6 caracteres anteriores: [A B C A B C]. Buffer de anticipación: [B C] (solo quedan 2 chars). Buscamos la coincidencia más larga que empiece por 'B'.

Ventana de búsqueda: [ A B C A B C ] ← posiciones -6...-1
Buffer de anticip.: [ B C ]
Buscamos 'B' en la ventana:
- Posición -5 (B): coincide 'B' → luego 'C' en -4: ¿coincide con buffer[1]='C'? Sí
- Longitud de coincidencia = 2 (B C)
- El buffer se agota después de la coincidencia
- No hay símbolo siguiente (fin de cadena)
Token 5: (5, 2, '') → "retrocede 5, copia 2 chars (BC), fin de cadena"

Nota: cuando se llega al final de la cadena sin un símbolo siguiente, se emite el token sin carácter adicional. Algunos libros lo escriben como (5, 2, ε) o simplemente (5, 2).

# Token Tipo Decodificación Chars representados
1 (0, 0, 'A') Nuevo símbolo Emitir 'A' directamente A
2 (0, 0, 'B') Nuevo símbolo Emitir 'B' directamente B
3 (0, 0, 'C') Nuevo símbolo Emitir 'C' directamente C
4 (3, 3, 'A') Referencia Retroceder 3, copiar 3 chars (ABC), luego 'A' A B C A
5 (5, 2, '') Referencia Retroceder 5, copiar 2 chars (BC) B C
6
Respuesta (b) — Verificación: decodificación de los tokens

La decodificación reconstruye exactamente la cadena original. Para cada token de referencia, el decodificador aplica: "ir offset posiciones hacia atrás en lo ya decodificado, copiar longitud caracteres".

Token 1: (0,0,'A') → emite 'A' Salida: A
Token 2: (0,0,'B') → emite 'B' Salida: A B
Token 3: (0,0,'C') → emite 'C' Salida: A B C
Token 4: (3,3,'A') → retrocede 3 → pos de 'A'
copia 3: A, B, C
emite 'A' Salida: A B C A B C A
Token 5: (5,2,'') → retrocede 5 → pos de 'B'
copia 2: B, C Salida: A B C A B C A B C
Cadena reconstruida: A B C A B C A B C ✓ Idéntica al original
7
Respuesta (c) — Ahorro en representación

Comparamos el número de tokens necesarios con el número de caracteres originales. En la práctica, cada token requiere bits para codificar offset, longitud y símbolo; aquí analizamos la reducción en unidades lógicas.

Cadena original: 9 caracteres
Representación LZ77: 5 tokens
Reducción de tokens: (9 − 5) / 9 × 100% = 44.4%
Nota: los tokens 1, 2 y 3 son "ineficientes" (representan solo 1 char c/u)
Los tokens 4 y 5 son eficientes: representan 4 y 2 chars en 1 token
En cadenas largas con patrones repetitivos, LZ77 se vuelve mucho más eficiente

El algoritmo gasta tokens "caros" al inicio (cuando la ventana está vacía) pero se vuelve cada vez más eficiente a medida que la ventana se llena con el historial de la cadena.

8
Respuesta (d) — LZ77 vs. Huffman: tipos de redundancia

Estos dos algoritmos operan sobre fundamentos completamente distintos:

Huffman — Redundancia estadística

Explota que ciertos símbolos ocurren con mayor frecuencia. Si LOGIN_OK aparece el 40% del tiempo, se le da un código corto. No importa en qué posición de la cadena aparece; solo importa cuántas veces. Requiere conocer (o aprender) las probabilidades de los símbolos.

LZ77 — Redundancia estructural

Explota que ciertos patrones se repiten dentro de la cadena. No analiza probabilidades: detecta subcadenas que ya aparecieron y las reemplaza por referencias. No necesita conocer la distribución de la fuente; aprende los patrones al vuelo. Ideal para datos con estructura repetitiva (código fuente, logs, ADN).

📌
Sinergia en la práctica Algoritmos como DEFLATE (usado en ZIP, PNG, HTTP/2) combinan ambos enfoques: primero aplican LZ77 para eliminar patrones repetidos, luego aplican Huffman sobre los tokens resultantes para reducir la redundancia estadística restante. El resultado es más eficiente que cada método por separado.
Conclusión para FortiLog Systems LZ77 comprime la secuencia A B C A B C A B C de 9 caracteres a 5 tokens, logrando una reducción del 44.4% en unidades lógicas. Para logs de tráfico de red reales, donde los mismos tipos de paquetes se repiten continuamente en secuencias largas, esta reducción es significativamente mayor. La ventana deslizante garantiza que el algoritmo puede procesar flujos continuos con memoria acotada — clave en sistemas de captura de paquetes en tiempo real.

Algoritmo LZ78 · Diccionario Dinámico

💾 NetShield Consulting — Compresión de Registros de Acceso
Ejercicio 4 Construcción del diccionario dinámico LZ78 en un log de acceso

NetShield Consulting necesita comprimir registros de acceso que contienen secuencias de comandos de sistema. Una muestra del log tiene la siguiente forma (cada letra representa un tipo de comando):

A B A A B A B A

a) Aplique LZ78 paso a paso: construya el diccionario y genere la secuencia de tokens (índice, símbolo).
b) Muestre el estado del diccionario al final del proceso.
c) Verifique que la decodificación reconstruye exactamente la cadena original.
d) Compare LZ77 y LZ78: ventajas, desventajas y casos de uso en ciberseguridad.
Estructura y conceptos de LZ78
Formato de cada token LZ78
\[ \text{token} = (\,\text{índice\_previo},\; \text{símbolo\_nuevo}\,) \]

índice_previo: el índice en el diccionario de la secuencia previa más larga ya conocida (0 si no hay antecedente)  |  símbolo_nuevo: el carácter que extiende esa secuencia conocida

Proceso de construcción del diccionario
Paso 1: Leer la cadena de izquierda a derecha
Paso 2: Encontrar la secuencia más larga que ya esté en el diccionario
Paso 3: Tomar esa secuencia + el siguiente símbolo → nueva entrada del diccionario
Paso 4: Emitir el token (índice de la secuencia previa, símbolo nuevo)
Diferencia clave con LZ77
LZ77: Usa una ventana deslizante de tamaño fijo W — memoria acotada, no acumula todo el pasado
LZ78: Construye un diccionario explícito que crece con la cadena — puede volverse grande en textos largos

Intuición de LZ78: Imagine que tiene un glosario al que va añadiendo términos mientras lee un documento. La primera vez que ve una palabra, la añade al glosario. La siguiente vez que la ve, no la transcribe completa: escribe "ver glosario, entrada 3". Si luego ve esa misma palabra seguida de otra nueva, crea una nueva entrada en el glosario para el par. El diccionario de LZ78 funciona así: crece con cada paso, construyendo frases cada vez más largas que puede referenciar eficientemente.

Ventaja sobre LZ77: LZ78 no tiene límite de ventana — puede referenciar un patrón que apareciera al principio de la cadena, sin importar cuánto tiempo atrás. La desventaja es que el diccionario puede crecer indefinidamente (en la práctica se limita su tamaño y se reinicia cuando llega a un máximo).

1
Estado inicial — Diccionario vacío

El diccionario comienza completamente vacío. El índice 0 es un índice especial que significa "sin prefijo previo" — se usa cuando el símbolo que se va a codificar no tiene ningún antecedente en el diccionario. Los índices reales empiezan en 1.

Diccionario — Estado inicial
0:(vacío — símbolo raíz)
Cadena a procesar: A B A A B A B A
Posición cursor: ^
2
Paso 1 — Leer 'A'

Leemos 'A'. Buscamos 'A' en el diccionario: no existe. Como no hay prefijo previo en el diccionario, el índice_previo es 0. Creamos la entrada 1: "A". Emitimos el token (0, A).

Símbolo leído: 'A'
¿'A' en diccionario? NO
Prefijo previo más largo: (ninguno) → índice = 0
Nueva entrada: 1 → "A"
Token emitido: (0, A) → "no hay prefijo, símbolo nuevo: A"
Diccionario — Tras Paso 1
0:(vacío)
1:A ← nueva entrada
3
Paso 2 — Leer 'B'

Leemos 'B'. No está en el diccionario. Prefijo previo: ninguno. Nueva entrada: 2 → "B". Emitimos (0, B).

Símbolo leído: 'B'
¿'B' en diccionario? NO
Nueva entrada: 2 → "B"
Token emitido: (0, B) → "no hay prefijo, símbolo nuevo: B"
Diccionario — Tras Paso 2
0:(vacío)
1:A
2:B ← nueva entrada
4
Paso 3 — Leer 'A', luego 'A' (secuencia "AA")

Leemos 'A'. Ahora 'A' SÍ está en el diccionario (entrada 1). Extendemos: leemos el siguiente símbolo 'A'. Buscamos "AA" en el diccionario: no existe. La secuencia conocida más larga es solo "A" (entrada 1). El símbolo nuevo es el segundo 'A'. Creamos la entrada 3: "AA". Emitimos (1, A).

Leemos: 'A' → 'A' existe en diccionario (entrada 1)
Extendemos: leemos siguiente = 'A'
¿'AA' en diccionario? NO
Prefijo previo más largo: "A" → índice = 1
Símbolo nuevo: 'A'
Nueva entrada: 3 → "AA"
Token emitido: (1, A) → "prefijo 'A' (entrada 1), luego 'A'"
Diccionario — Tras Paso 3
1:A
2:B
3:AA ← nueva entrada (A + A)

El cursor avanza 2 posiciones (la 'A' conocida + la 'A' nueva). Nos hemos consumido A B A A — quedan: B A B A.

5
Paso 4 — Leer 'B', luego 'A' (secuencia "BA")

Leemos 'B'. 'B' existe (entrada 2). Extendemos: leemos el siguiente = 'A'. Buscamos "BA" en el diccionario: no existe. Prefijo "B" → índice 2. Nueva entrada: 4 → "BA". Emitimos (2, A).

Leemos: 'B' → 'B' existe (entrada 2)
Extendemos: leemos siguiente = 'A'
¿'BA' en diccionario? NO
Nueva entrada: 4 → "BA"
Token emitido: (2, A) → "prefijo 'B' (entrada 2), luego 'A'"
Diccionario — Tras Paso 4
1:A
2:B
3:AA
4:BA ← nueva entrada (B + A)

Consumido hasta ahora: A B A A B A. Quedan: B A.

6
Paso 5 — Leer 'B', luego 'A' — coincidencia con entrada 4

Leemos 'B'. Existe (entrada 2). Extendemos: leemos 'A'. Buscamos "BA": SÍ existe en el diccionario (entrada 4). Intentamos extender más: ¿hay más caracteres? No — la cadena se termina después de este 'A'. No podemos leer otro símbolo para crear "BAx". Emitimos (4, ε) — algunas implementaciones usan (2, A) para este caso; aquí indicamos que "BA" es el prefijo con índice 4 y no hay símbolo siguiente.

Leemos: 'B' → existe (entrada 2)
Extendemos: leemos 'A'
¿'BA' en diccionario? SÍ → entrada 4
Extendemos más: fin de cadena
Secuencia conocida más larga: "BA" → índice 4
Token emitido: (4, ε) → "prefijo 'BA' (entrada 4), sin símbolo adicional"

Nota: en muchas implementaciones este último token se trata de forma especial (indicador de fin de cadena) o simplemente no se añade una nueva entrada al diccionario porque no hay símbolo nuevo que añadir.

7
Respuesta (b) — Estado final del diccionario
Diccionario LZ78 — Estado final
1:A← primer símbolo observado
2:B← segundo símbolo observado
3:AA← A seguido de A
4:BA← B seguido de A
Paso Token Cadena representada Nueva entrada diccionario
1 (0, A) A 1 → A
2 (0, B) B 2 → B
3 (1, A) A A 3 → AA
4 (2, A) B A 4 → BA
5 (4, ε) B A — (fin de cadena)
8
Respuesta (c) — Verificación de decodificación

Para decodificar, el receptor reconstruye el diccionario exactamente en el mismo orden. Cada token (índice, símbolo) le dice: "toma la cadena del diccionario en esa entrada, luego añade el símbolo nuevo".

Token (0, A): prefijo=nada, símbolo=A → emite "A" Reconstruido: A
Token (0, B): prefijo=nada, símbolo=B → emite "B" Reconstruido: A B
Token (1, A): prefijo=dict[1]="A", símbolo=A → emite "AA" Reconstruido: A B A A
Token (2, A): prefijo=dict[2]="B", símbolo=A → emite "BA" Reconstruido: A B A A B A
Token (4, ε): prefijo=dict[4]="BA", sin símbolo → emite "BA" Reconstruido: A B A A B A B A
Cadena reconstruida: A B A A B A B A ✓ Idéntica al original

El decodificador puede hacer esto sin recibir el diccionario por separado: lo reconstruye de forma local a medida que decodifica los tokens, exactamente igual que lo hizo el codificador.

9
Respuesta (d) — Comparación LZ77 vs. LZ78 en ciberseguridad
Criterio LZ77 LZ78
Estructura de memoria Ventana deslizante (tamaño fijo W) Diccionario dinámico (crece con la cadena)
Alcance hacia atrás Limitado al tamaño de la ventana W Ilimitado (toda la cadena procesada)
Memoria requerida Acotada y predecible (O(W)) Variable, puede crecer mucho (O(n))
Tipo de token (offset, longitud, símbolo) (índice diccionario, símbolo)
Rendimiento en datos cortos Menor (ventana vacía al inicio) Menor (diccionario vacío al inicio)
Rendimiento en datos largos Bueno, especialmente con W grande Muy bueno si los patrones se repiten
Variantes conocidas DEFLATE, gzip, zlib, PNG LZW (GIF, TIFF, PDF antiguo), LZC
Uso en ciberseguridad Compresión de capturas de paquetes (pcap), logs de flujos de red Compresión de bases de datos de firmas, registros de auditoría estructurados
⚠️
Nota de seguridad — CRIME y BREACH En contextos de ciberseguridad, la compresión basada en diccionarios (variantes de LZ) dentro de conexiones TLS puede ser explotada. Los ataques CRIME (2012) y BREACH (2013) demostraron que si un atacante puede observar el tamaño del texto cifrado comprimido, puede inferir fragmentos del contenido secreto (como tokens de sesión) insertando texto conocido y observando si el tamaño se reduce (indicando que el patrón coincidió con un secreto). Por eso la compresión TLS se deshabilitó en navegadores modernos.
Conclusión para NetShield Consulting LZ78 procesa la cadena A B A A B A B A (8 caracteres) en 5 tokens, construyendo un diccionario de 4 entradas. El diccionario dinámico le permite reconocer y referenciar patrones como "BA" que se repiten, sin necesidad de una ventana de tamaño predefinido. Para registros de auditoría con vocabulario limitado pero patrones repetitivos complejos (secuencias de comandos, combinaciones de eventos), LZ78 y su variante LZW son especialmente eficientes.

Tabla comparativa — Métodos de compresión sin pérdida

Método Tipo de redundancia Requiere dist. previa Acercamiento a H(X) Uso típico en ciberseguridad
Huffman clásico Estadística H(X) ≤ ℓ̄ < H(X)+1 Compresión de logs con distribución conocida
Huffman adaptativo Estadística No Se acerca conforme aprende Flujos de alertas en tiempo real
Huffman por bloques Estadística + contextual Parcialmente Mejor que símbolo a símbolo Logs con patrones de eventos correlacionados
LZ77 Estructural (ventana) No Asintóticamente óptimo Capturas pcap, tráfico de red (gzip, DEFLATE)
LZ78 / LZW Estructural (diccionario) No Asintóticamente óptimo Bases de firmas, registros estructurados

Relaciones fundamentales

Huffman: H(X) ≤ ℓ̄ < H(X)+1
Eficiencia: η = H(X)/ℓ̄ × 100%
LZ77 token: (offset, longitud, símbolo)
LZ78 token: (índice, símbolo_nuevo)
DEFLATE = LZ77 + Huffman (combinados)

Casos extremos de compresión

Dist. uniforme → R=0, no compresible
Un solo símbolo → R=1, compresión total
Huffman efic. → p(x) = 2^{−k} para todo x
LZ óptimo → cadena con muchos patrones
LZ ineficiente → cadena sin repeticiones