Saltar a contenido

LZ78

Definición

El algoritmo LZ78 es un método de compresión sin pérdida desarrollado por Abraham Lempel y Jacob Ziv en 1978. Es uno de los algoritmos de compresión más famosos y ha servido de base para múltiples formatos y aplicaciones, como el compresor compress de Unix y el formato de imagen GIF. El algoritmo es parte de la familia de compresión Lempel-Ziv y es conocido por su estructura regular, lo que lo hace adecuado para representaciones comprimidas que permiten acceso óptimo y para índices de texto comprimido que soportan búsqueda de patrones.

Funciona construyendo un diccionario dinámico de subcadenas a medida que procesa la secuencia de entrada. La idea principal es identificar patrones repetidos y sustituirlos por referencias a un diccionario. Implementaciones comunes implican el uso de un trie (LZ Trie) o diccionarios (hashmap).

Complejidades

  • Espacio: \(O(z \log n)\), donde \(z\) es el tamaño del diccionario.
  • Tiempo: \(O(n \log \sigma)\), siendo \(n\) la longitud del texto y \(\sigma\) el tamaño del alfabeto.

Compresión

El algoritmo LZ78 comprime un texto \(T[1...n]\) segmentándolo en una secuencia de frases únicas. Consideremos la comprensión del texto \(T\) donde se ha procesado \(T[1...i-1]\) en \(r\) frases \(B_0,B_1,B_2,...,B_{r-1}\), donde \(B_0\) representa la cadena vacía.

Para determinar la siguiente frase \(B_r\):

  1. Encontrar el prefijo más largo \(T[i...j-1]\) de \(T\) que coincida con alguna frase previa \(B_q\) donde \(q < r\).
  2. Definir \(B_r = B_q\times T_j\) donde \(T[j]\) es el siguiente símbolo en el texto.
  3. Representar \(B_r\) como el par \((q,T[j])\).
  4. Avanzar en el texto desde la posición \(j+1\).

Descompresión

  1. Crear un diccionario \(D\) vacío donde \(D[0]\) representa la cadena vacía.
  2. Procesar cada par \((q,c)\) en la secuencia de entrada:
  3. Si \(q=0\), c es una frase nueva y se añade como \(D[i]=c\), donde \(i\) es el índice actual.
  4. Si \(q \neq 0\), esto indica que \(D[i]=D[q]+c\), donde \(D[q]\) es la frase previa y \(c\) el carácter.
  5. Añadir \(D[r]\) al diccionario y actualizar el índice \(i\).
  6. Concatenar el texto \(D[r]\)
  7. Repetir hasta completar todos los pares en la entrada comprimida.

Ejemplo de uso

\(T = aababcaabbac\)

Compresión

\(T = a|ab|abc|aa|b|ba|c\)

\(i\) \(\text{Factor}\) \(\text{Encoding}\)
\(1\) \(a\) \((0, a)\)
\(2\) \(ab\) \((1, b)\)
\(3\) \(abc\) \((2, c)\)
\(4\) \(aa\) \((1, a)\)
\(5\) \(b\) \((0, b)\)
\(6\) \(ba\) \((5, a)\)
\(7\) \(c\) \((0, c)\)

\(S=\{(0, a),(1, b),(2, c) , (1, a) , (0, b) , (5, a) , (0, c)\}\)

LZ78
\(\text{LZ Trie para } T = aababcaabbac\)

Descompresión

\(i\) \(\text{Encoding}\) \(\text{Descompresión}\) \(\text{String}\)
\(1\) \((0, a)\) \(D[1] = a\) \(a\)
\(2\) \((1, b)\) \(D[2] = D[1] + b = ab\) \(aab\)
\(3\) \((2, c)\) \(D[3] = D[2] + c = abc\) \(aababc\)
\(4\) \((1, a)\) \(D[4] = D[1] + a = aa\) \(aababcaa\)
\(5\) \((0, b)\) \(D[5] = b\) \(aababcaab\)
\(6\) \((5, a)\) \(D[6] = D[5] + a = ba\) \(aababcaabba\)
\(7\) \((0, c)\) \(D[7] = c\) \(aababcaabbac\)

Implementación

class LZ78Compressor:
    def __init__(self):
        self.dictionary = {0: ""}
        self.next_index = 1

    def compress(self, input_string):
        output = []
        current_prefix = ""

        for char in input_string:
            if (current_prefix + char) in self.dictionary:
                current_prefix += char
            else:
                output.append((self.dictionary.get(current_prefix, 0), char))
                self.dictionary[current_prefix + char] = self.next_index
                self.next_index += 1
                current_prefix = ""

        if current_prefix:
            output.append((self.dictionary.get(current_prefix, 0), ""))

        return output

    def decompress(self, compressed_data):
        self.dictionary = {0: ""}
        self.next_index = 1
        output = ""

        for index, char in compressed_data:
            current_string = self.dictionary[index] + char
            output += current_string

            self.dictionary[self.next_index] = current_string
            self.next_index += 1

        return output

compressor = LZ78Compressor()
compressed_data = compressor.compress("aababcaabbac")
print(compressed_data)

decompressed_data = compressor.decompress(compressed_data)
print(decompressed_data)

Referencias

  • Ziv, J., Lempel, A. (1977). A Universal Algorithm for Sequential Data Compression. IEEE Transactions on Information Theory.
  • Ziv, J., Lempel, A. (1978). Compression of Individual Sequences via Variable-Rate Coding. IEEE Transactions on Information Theory.
  • Arroyuelo, D., Cánovas, R., Navarro, G., & Raman, R. (2017). LZ78 Compression in Low Main Memory Space. In Lecture Notes in Computer Science (pp. 38–50). Springer International Publishing.
  • Arroyuelo, D., Cánovas, R., Fischer, J., Köppl, D., Löbel, M., Navarro, G., & Raman, R. (2021). Engineering Practical Lempel-Ziv Tries. In ACM Journal of Experimental Algorithmics (Vol. 26, pp. 1–47). Association for Computing Machinery (ACM).
  • Zhao, F. & Muthusamy, N. (2012). 5418 Parallel Computer Architecture and Programming Project Report: Implementation and Comparison of Parallel LZ77 and LZ78 Algorithms.