Compresión cuadrada latina

31

Un cuadrado latino es un cuadrado que tiene símbolos en las filas o columnas sin repetida: .

13420
21304
32041
04213
40132

Y como muchos jugadores de Sudoku saben, no necesitas todos los números para deducir los números restantes.

Su desafío es comprimir un cuadrado latino en la menor cantidad de bytes posible. Debe proporcionar uno o dos programas que comprimen / descomprimen.

Diversa información:

  • Los números utilizados siempre serán 0..N-1, donde Nes la longitud del borde del cuadrado, yN<=25
  • En la descompresión, el cuadrado latino debe ser idéntico a la entrada.
  • Sus programas deberían poder (des) comprimir cualquier cuadrado latino (dentro del tamaño cuadrado máximo), no solo los que he proporcionado. Las relaciones de compresión también deberían ser similares.
  • En realidad, debe ejecutar la compresión y el descompresor para obtener su puntaje (sin tiempos de ejecución de fin de universo)

Los casos de prueba se pueden encontrar en github . Su puntaje es el tamaño total de los casos de prueba comprimidos.

EDITAR: A partir de las 20:07 del 7 de julio, actualicé los casos de prueba (para solucionar un problema de generación). Vuelva a ejecutar su programa en los nuevos casos de prueba. Gracias Anders Kaseorg .

Nathan Merrill
fuente
1
Pues bien, por definición, cualquier símbolo podría ser utilizado, pero mis casos de prueba acaba de suceder a utilizar 0aunque n-1:)
Nathan Merrill
3
@NathanMerrill bueno, solo se permitía al punto usar ndiferentes símbolos. : P
Martin Ender
1
@DavidC No debería importar, ya que el tamaño se mide en bytes .
flawr
2
19 de sus 25 casos de prueba (todos menos 4, 6, 8, 10, 12, 14) se generaron permutando las filas y columnas del cuadrado latino trivial cuya entrada ( i , j ) es i + j mod n . Esto los hace muy fáciles de comprimir mucho más que un cuadrado latino aleatorio. Aunque sus reglas dicen que deberíamos tener proporciones de compresión similares para todos los cuadrados latinos, esto podría ser fácil de romper por accidente. Los casos de prueba deberían ser más representativos.
Anders Kaseorg

Respuestas:

10

Python, 1281.375 1268.625 bytes

Codificamos el cuadrado latino "decisión" a la vez, donde cada decisión es de una de estas tres formas:

  • qué número va en la fila i , columna j ;
  • en la fila i , en qué columna va el número k ;
  • en la columna j , en la que entra el número k .

En cada paso, hacemos todas las inferencias lógicas que podemos basándonos en decisiones anteriores, luego tomamos la decisión con el menor número de opciones posibles, que por lo tanto toman el menor número de bits para representar.

Las opciones son proporcionadas por un decodificador aritmético simple (div / mod por el número de opciones). Pero eso deja algo de redundancia en la codificación: si k decodifica a un cuadrado donde el producto de todos los números de opciones era m , entonces k + m , k + 2⋅ m , k + 3⋅ m , ... decodifica al mismo cuadrado con algún estado sobrante al final.

Aprovechamos esta redundancia para evitar codificar explícitamente el tamaño del cuadrado. El descompresor comienza tratando de decodificar un cuadrado de tamaño 1. Cada vez que el decodificador termina con el estado restante, arroja ese resultado, resta m del número original, aumenta el tamaño en 1 e intenta nuevamente.

import numpy as np

class Latin(object):
    def __init__(self, size):
        self.size = size
        self.possible = np.full((size, size, size), True, dtype=bool)
        self.count = np.full((3, size, size), size, dtype=int)
        self.chosen = np.full((3, size, size), -1, dtype=int)

    def decision(self):
        axis, u, v = np.unravel_index(np.where(self.chosen == -1, self.count, self.size).argmin(), self.count.shape)
        if self.chosen[axis, u, v] == -1:
            ws, = np.rollaxis(self.possible, axis)[:, u, v].nonzero()
            return axis, u, v, list(ws)
        else:
            return None, None, None, None

    def choose(self, axis, u, v, w):
        t = [u, v]
        t[axis:axis] = [w]
        i, j, k = t
        assert self.possible[i, j, k]
        assert self.chosen[0, j, k] == self.chosen[1, i, k] == self.chosen[2, i, j] == -1

        self.count[1, :, k] -= self.possible[:, j, k]
        self.count[2, :, j] -= self.possible[:, j, k]
        self.count[0, :, k] -= self.possible[i, :, k]
        self.count[2, i, :] -= self.possible[i, :, k]
        self.count[0, j, :] -= self.possible[i, j, :]
        self.count[1, i, :] -= self.possible[i, j, :]
        self.count[0, j, k] = self.count[1, i, k] = self.count[2, i, j] = 1
        self.possible[i, j, :] = self.possible[i, :, k] = self.possible[:, j, k] = False
        self.possible[i, j, k] = True
        self.chosen[0, j, k] = i
        self.chosen[1, i, k] = j
        self.chosen[2, i, j] = k

def encode_sized(size, square):
    square = np.array(square, dtype=int)
    latin = Latin(size)
    chosen = np.array([np.argmax(square[:, :, np.newaxis] == np.arange(size)[np.newaxis, np.newaxis, :], axis=axis) for axis in range(3)])
    num, denom = 0, 1
    while True:
        axis, u, v, ws = latin.decision()
        if axis is None:
            break
        w = chosen[axis, u, v]
        num += ws.index(w)*denom
        denom *= len(ws)
        latin.choose(axis, u, v, w)
    return num

def decode_sized(size, num):
    latin = Latin(size)
    denom = 1
    while True:
        axis, u, v, ws = latin.decision()
        if axis is None:
            break
        if not ws:
            return None, 0
        latin.choose(axis, u, v, ws[num % len(ws)])
        num //= len(ws)
        denom *= len(ws)
    return latin.chosen[2].tolist(), denom

def compress(square):
    size = len(square)
    assert size > 0
    num = encode_sized(size, square)
    while size > 1:
        size -= 1
        square, denom = decode_sized(size, num)
        num += denom
    return '{:b}'.format(num + 1)[1:]

def decompress(bits):
    num = int('1' + bits, 2) - 1
    size = 1
    while True:
        square, denom = decode_sized(size, num)
        num -= denom
        if num < 0:
            return square
        size += 1

total = 0
with open('latin_squares.txt') as f:
    while True:
        square = [list(map(int, l.split(','))) for l in iter(lambda: next(f), '\n')]
        if not square:
            break

        bits = compress(square)
        assert set(bits) <= {'0', '1'}
        assert square == decompress(bits)
        print('Square {}: {} bits'.format(len(square), len(bits)))
        total += len(bits)

print('Total: {} bits = {} bytes'.format(total, total/8.0))

Salida:

Square 1: 0 bits
Square 2: 1 bits
Square 3: 3 bits
Square 4: 8 bits
Square 5: 12 bits
Square 6: 29 bits
Square 7: 43 bits
Square 8: 66 bits
Square 9: 94 bits
Square 10: 122 bits
Square 11: 153 bits
Square 12: 198 bits
Square 13: 250 bits
Square 14: 305 bits
Square 15: 363 bits
Square 16: 436 bits
Square 17: 506 bits
Square 18: 584 bits
Square 19: 674 bits
Square 20: 763 bits
Square 21: 877 bits
Square 22: 978 bits
Square 23: 1097 bits
Square 24: 1230 bits
Square 25: 1357 bits
Total: 10149 bits = 1268.625 bytes
Anders Kaseorg
fuente
Estoy probando este código en ideone, pero solo da errores de tiempo de ejecución. Lo modifiqué usando stdin en lugar del archivo f. ideone.com/fKGSQd
edc65
@ edc65 No funciona porque NumPy de Ideone está desactualizado.
Dennis
@ edc65 Ideone tiene NumPy 1.8.2 que es demasiado viejo para np.stack(). En este caso, se puede reemplazar con np.array([…]), y lo he hecho en la versión actual.
Anders Kaseorg
hmmm ¿están todos los cuadrados almacenados en un flujo de bytes? ¿también se almacena la información sobre su tamaño, o el decodificador supone que son de tamaño 1,2,3, ... etc.?
Sarge Borsch
@SargeBorsch Cada cuadrado se comprime en un flujo de bits separado. El descompresor recupera el tamaño cuadrado inequívocamente del flujo de bits, utilizando el algoritmo que describí. No se utiliza suposición.
Anders Kaseorg
7

MATLAB, 3'062.5 2'888.125 bytes

Este enfoque simplemente abandona la última fila y la última columna del cuadrado, y convierte cada entrada en palabras de cierta profundidad de bits. La profundidad de bits se elige mínima para el cuadrado de tamaño dado. (Sugerencia de @KarlNapf) Estas palabras se agregan entre sí. La descompresión es justo lo contrario.

La suma para todos los casos de prueba es 23'105 bits o 2'888.125 bytes. (Todavía se mantiene para los casos de prueba actualizados, ya que el tamaño de mis salidas solo depende del tamaño de la entrada).

function bin=compress(a)
%get rid of last row and column:
s=a(1:end-1,1:end-1);
s = s(:)';
bin = [];
%choose bit depth:
bitDepth = ceil(log2(numel(a(:,1))));
for x=s;
    bin = [bin, dec2bin(x,bitDepth)];
end
end

function a=decompress(bin)
%determine bit depth
N=0;
f=@(n)ceil(log2(n)).*(n-1).^2;
while f(N)~= numel(bin)
    N=N+1; 
end
bitDepth = ceil(log2(N));
%binary to decimal:
assert(mod(numel(bin),bitDepth)==0,'invalid input length')
a=[];
for k=1:numel(bin)/bitDepth;
    number = bin2dec([bin(bitDepth*(k-1) + (1:bitDepth)),' ']);
    a = [a,number];    
end
n = sqrt(numel(a));
a = reshape(a,n,n);
disp(a)
%reconstruct last row/column:
n=size(a,1)+1;
a(n,n)=0;%resize
%complete rows:
v = 0:n-1;
for k=1:n
    a(k,n) = setdiff(v,a(k,1:n-1));
    a(n,k) = setdiff(v,a(1:n-1,k));
end
end
falla
fuente
Puede comprimir un poco más utilizando una tasa de bits variable, como para n=9..164 bits son suficientes.
Karl Napf
@KarlNapf ¿Cómo discriminas las palabras de diferente longitud? Por lo que sé, entonces necesitas prefijos adicionales, ¿no?
flawr
No es variable dentro de una compresión, más bien depende del tamaño del cuadrado. Si n> 16, use 5 bits fijos, si 8 <n <= 16 use 4 bits fijos y así sucesivamente.
Karl Napf
Oh cierto, esto tiene sentido, ¡gracias!
flawr
3
Por la misma razón que lo haces al revés, es probablemente la forma en que estás acostumbrado. =)
flawr
7

Python 3, 10772 bits (1346.5 bytes)

def compress(rows):
    columns = list(zip(*rows))
    size = len(rows)
    symbols = range(size)
    output = size - 1
    weight = 25
    for i in symbols:
        for j in symbols:
            choices = set(rows[i][j:]) & set(columns[j][i:])
            output += weight * sorted(choices).index(rows[i][j])
            weight *= len(choices)
    return bin(output + 1)[3:]

def decompress(bitstring):
    number = int('1' + bitstring, 2) - 1
    number, size = divmod(number, 25)
    size += 1
    symbols = range(size)
    rows = [[None] * size for _ in symbols]
    columns = [list(column) for column in zip(*rows)]
    for i in symbols:
        for j in symbols:
            choices = set(symbols) - set(rows[i]) - set(columns[j])
            number, index = divmod(number, len(choices))
            rows[i][j] = columns[j][i] = sorted(choices)[index]
    return rows

Toma 0.1 segundos para comprimir y descomprimir los casos de prueba combinados.

Verifique el puntaje en Ideone .

Dennis
fuente
Woah, ¿quieres explicarlo?
Nathan Merrill
1
En pocas palabras, el compresor viaja a través del cuadrado en orden de lectura, haciendo un seguimiento de los símbolos que ya aparecieron en esa fila y columna, y codificando aritméticamente el índice del símbolo en la lista ascendente de posibles símbolos. Agregaré una explicación detallada después de limpiar un poco mi código y probar si la base biyectiva 256 guarda algún byte.
Dennis
No estoy completamente seguro de lo que está haciendo su código, pero ¿no es posible dejar la última línea y resolverla mientras se descomprime?
Yytsi
@TuukkaX Cuando solo hay un símbolo posible len(possible)es 1 y possible.index(rows[i][j])es 0 , por lo que ese símbolo se codifica sin costo.
Dennis
Sí, los nuevos casos de prueba ahorraron 6 bits. :)
Dennis
3

J , 2444 bytes

Se basa en la construcción A.para convertir ay desde una permutación de enteros [0, n) y un índice de permutación.

Comprimir, 36 bytes

({:a.)joinstring<@(a.{~255&#.inv)@A.

La entrada es una matriz 2D que representa el cuadrado latino. Cada fila se convierte en un índice de permutación, y ese índice se convierte en una lista de 255 dígitos base y se reemplaza con un valor ASCII. Cada cadena se une con el carácter ASCII en 255.

Descomprimir, 45 bytes

[:(A.i.@#)[:(_&,(255&#.&x:);._1~1,255&=)u:inv

Divide la cadena de entrada en cada valor ASCII de 255 y analiza cada grupo como 255 dígitos base. Luego, usando el número de grupos, cree una lista de enteros [0, longitud) y permútala de acuerdo con cada índice y devuélvala como una matriz 2d.

millas
fuente
2

Python, 6052 4521 3556 bytes

compresstoma el cuadrado como una cadena multilínea, al igual que los ejemplos y devuelve una cadena binaria, mientras que decompresshace lo contrario.

import bz2
import math

def compress(L):
 if L=="0": 
  C = []
 else:
  #split elements
  elems=[l.split(',') for l in L.split('\n')]
  n=len(elems)
  #remove last row and col
  cropd=[e[:-1] for e in elems][:-1]
  C = [int(c) for d in cropd for c in d]

 #turn to string
 B=map(chr,C)
 B=''.join(B)

 #compress if needed
 if len(B) > 36:
  BZ=bz2.BZ2Compressor(9)
  BZ.compress(B)
  B=BZ.flush()

 return B

def decompress(C):

 #decompress if needed
 if len(C) > 40:
  BZ=bz2.BZ2Decompressor()
  C=BZ.decompress(C)

 #return to int and determine length
 C = map(ord,C)
 n = int(math.sqrt(len(C)))
 if n==0: return "0"

 #reshape to list of lists
 elems = [C[i:i+n] for i in xrange(0, len(C), n)]

 #determine target length
 n = len(elems[0])+1
 L = []
 #restore last column
 for i in xrange(n-1):
  S = {j for j in range(n)}
  L.append([])
  for e in elems[i]:
   L[i].append(e)
   S.remove(e)
  L[i].append(S.pop())
 #restore last row
 L.append([])
 for col in xrange(n):
  S = {j for j in range(n)}
  for row in xrange(n-1):
   S.remove(L[row][col])
  L[-1].append(S.pop())
 #merge elements
 orig='\n'.join([','.join([str(e) for e in l]) for l in L])
 return orig

Retire la última fila + columna y cierre el resto.

  • Edit1: bien base64no parece necesario
  • Edit2: ahora convierte la tabla cortada en una cadena binaria y comprime solo si es necesario
Karl Napf
fuente
2

Python 3, 1955 bytes

Otro más que usa índices de permutación ...

from math import factorial

test_data_name = 'latin_squares.txt'

def grid_reader(fname):
    ''' Read CSV number grids; grids are separated by empty lines '''
    grid = []
    with open(fname) as f:
        for line in f:
            line = line.strip()
            if line:
                grid.append([int(u) for u in line.split(',') if u])
            elif grid:
                yield grid
                grid = []
    if grid:
        yield grid

def show(grid):
    a = [','.join([str(u) for u in row]) for row in grid]
    print('\n'.join(a), end='\n\n')

def perm(seq, base, k):
    ''' Build kth ordered permutation of seq '''
    seq = seq[:]
    p = []
    for j in range(len(seq) - 1, 0, -1):
        q, k = divmod(k, base)
        p.append(seq.pop(q))
        base //= j
    p.append(seq[0])
    return p

def index(p):
    ''' Calculate index number of sequence p,
        which is a permutation of range(len(p))
    '''
    #Generate factorial base code
    fcode = [sum(u < v for u in p[i+1:]) for i, v in enumerate(p[:-1])]

    #Convert factorial base code to integer
    k, base = 0, 1
    for j, v in enumerate(reversed(fcode), 2):
        k += v * base
        base *= j
    return k

def encode_latin(grid):
    num = len(grid)
    fbase = factorial(num)

    #Encode grid rows by their permutation index,
    #in reverse order, starting from the 2nd-last row
    codenum = 0
    for row in grid[-2::-1]:
        codenum = codenum * fbase + index(row)
    return codenum

def decode_latin(num, codenum):
    seq = list(range(num))
    sbase = factorial(num - 1)
    fbase = sbase * num

    #Extract rows
    grid = []
    for i in range(num - 1):
        codenum, k = divmod(codenum, fbase)
        grid.append(perm(seq, sbase, k))

    #Build the last row from the missing element of each column
    allnums = set(seq)
    grid.append([allnums.difference(t).pop() for t in zip(*grid)])
    return grid

byteorder = 'little'

def compress(grid):
    num = len(grid)
    codenum = encode_latin(grid)
    length = -(-codenum.bit_length() // 8)
    numbytes = num.to_bytes(1, byteorder)
    codebytes = codenum.to_bytes(length, byteorder)
    return numbytes + codebytes

def decompress(codebytes):
    numbytes, codebytes= codebytes[:1], codebytes[1:]
    num = int.from_bytes(numbytes, byteorder)
    if num == 1:
        return [[0]]
    else:
        codenum = int.from_bytes(codebytes, byteorder)
        return decode_latin(num, codenum)

total = 0
for i, grid in enumerate(grid_reader(test_data_name), 1):
    #show(grid)
    codebytes = compress(grid)
    length = len(codebytes)
    total += length
    newgrid = decompress(codebytes)
    ok = newgrid == grid
    print('{:>2}: Length = {:>3}, {}'.format(i, length, ok))
    #print('Code:', codebytes)
    #show(newgrid)

print('Total bytes: {}'.format(total))

salida

 1: Length =   1, True
 2: Length =   1, True
 3: Length =   2, True
 4: Length =   3, True
 5: Length =   5, True
 6: Length =   7, True
 7: Length =  11, True
 8: Length =  14, True
 9: Length =  20, True
10: Length =  26, True
11: Length =  33, True
12: Length =  41, True
13: Length =  50, True
14: Length =  61, True
15: Length =  72, True
16: Length =  84, True
17: Length =  98, True
18: Length = 113, True
19: Length = 129, True
20: Length = 147, True
21: Length = 165, True
22: Length = 185, True
23: Length = 206, True
24: Length = 229, True
25: Length = 252, True
Total bytes: 1955
PM 2Ring
fuente
2

Python3 - 3,572 3,581 bytes

from itertools import *
from math import *

def int2base(x,b,alphabet='0123456789abcdefghijklmnopqrstuvwxyz'):
    if isinstance(x,complex):
        return (int2base(x.real,b,alphabet) , int2base(x.imag,b,alphabet))
    if x<=0:
        if x==0:return alphabet[0]
        else:return  '-' + int2base(-x,b,alphabet)
    rets=''
    while x>0:
        x,idx = divmod(x,b)
        rets = alphabet[idx] + rets
    return rets

def lexicographic_index(p):
    result = 0
    for j in range(len(p)):
        k = sum(1 for i in p[j + 1:] if i < p[j])
        result += k * factorial(len(p) - j - 1)
    return result

def getPermutationByindex(sequence, index):
    S = list(sequence)
    permutation = []
    while S != []:
        f = factorial(len(S) - 1)
        i = int(floor(index / f))
        x = S[i]
        index %= f
        permutation.append(x)
        del S[i]
    return tuple(permutation)

alphabet = "abcdefghijklmnopqrstuvwxyz"

def dataCompress(lst):
    n = len(lst[0])

    output = alphabet[n-1]+"|"

    for line in lst:
        output += "%s|" % int2base(lexicographic_index(line), 36)

    return output[:len(output) - 1]

def dataDeCompress(data):
    indexes = data.split("|")
    n = alphabet.index(indexes[0]) + 1
    del indexes[0]

    lst = []

    for index in indexes:
        if index != '':
            lst.append(getPermutationByindex(range(n), int(index, 36)))

    return lst

dataCompress toma una lista de tuplas de enteros y devuelve una cadena.

dateDeCompress toma una cadena y devuelve una lista de tuplas de enteros.

En resumen, para cada línea, este programa toma ese índice de permutación de líneas y lo guarda en la base 36. La descompresión lleva mucho tiempo con grandes entradas, pero la compresión es realmente rápida incluso en grandes entradas.

Uso:

dataCompress([(2,0,1),(1,2,0),(0,1,2)])

resultado: c|4|3|0

dataDeCompress("c|4|3|0")

resultado: [(2, 0, 1), (1, 2, 0), (0, 1, 2)]

Yytsi
fuente
2
Probablemente obtendrá un tiempo de ejecución mucho mejor si no ajusta sus permutationsllamadas en listllamadas: permutationsdevuelve un generador, que genera perezosamente todas las permutaciones, pero si intenta convertirlo en un list, genera ansiosamente todas las permutaciones, lo que toma Un largo tiempo.
Mego
¿Podría explicar un poco mejor cómo usar su código?
Mego
@Mego Claro, tal vez también implemente la evaluación perezosa, aunque todavía es bastante indiscutible.
Yytsi
1

Java, 2310 bytes

Convertimos cada fila del cuadrado a un número que represente qué permutación lexicográfica está usando números factorados, también conocido como sistema de números factoriales , que es útil para numerar permutaciones.

Escribimos el cuadrado en un archivo binario donde el primer byte es el tamaño del cuadrado, y luego cada fila tiene un byte para el número de bytes en la representación binaria de un BigInteger de Java, seguido de los bytes de ese BigInteger.

Para invertir el proceso y descomprimir el cuadrado, leemos el tamaño y luego cada BigInteger, y usamos ese número para generar cada fila del cuadrado.

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class Latin {
    public static void main(String[] args) {
        if (args.length != 3) {
            System.out.println("java Latin {-c | -d} infile outfile");
        } else if (args[0].equals("-c")) {
            compress(args[1], args[2]);
        } else if (args[0].equals("-d")) {
            decompress(args[1], args[2]);
        } else {
            throw new IllegalArgumentException(
                "Invalid mode: " + args[0] + ", not -c or -d");
        }
    }

    public static void compress(String filename, String outname) {
        try (BufferedReader br = Files.newBufferedReader(Paths.get(filename))) {
            try (OutputStream os =
                    new BufferedOutputStream(new FileOutputStream(outname))) {
                String line = br.readLine();
                if (line == null) return;
                int size = line.split(",").length;
                if (size > 127) throw new ArithmeticException(
                    "Overflow: square too large");
                Permutor perm = new Permutor(size);
                os.write((byte) size); // write size of square

                do {
                    List<Integer> nums = Arrays.stream(line.split(","))
                        .map(Integer::new)
                        .collect(Collectors.toList());
                    byte[] bits = perm.which(nums).toByteArray();
                    os.write((byte) bits.length); // write length of bigint
                    os.write(bits); // write bits of bigint
                } while ((line = br.readLine()) != null);
            }
        } catch (IOException e) {
            System.out.println("Error compressing " + filename);
            e.printStackTrace();
        }
    }

    public static void decompress(String filename, String outname) {
        try (BufferedInputStream is =
                new BufferedInputStream(new FileInputStream(filename))) {
            try (BufferedWriter bw =
                    Files.newBufferedWriter(Paths.get(outname))) {
                int size = is.read(); // size of latin square
                Permutor perm = new Permutor(size);
                for (int i = 0; i < size; ++i) {
                    int num = is.read(); // number of bytes in bigint
                    if (num == -1) {
                        throw new IOException(
                            "Unexpected end of file reading " + filename);
                    }
                    byte[] buf = new byte[num];
                    int read = is.read(buf); // read bits of bigint into buf
                    if (read != num) {
                        throw new IOException(
                            "Unexpected end of file reading " + filename);
                    }
                    String row = perm.nth(new BigInteger(buf)).stream()
                        .map(Object::toString)
                        .collect(Collectors.joining(","));
                    bw.write(row);
                    bw.newLine();
                }
            }
        } catch (IOException e) {
            System.out.println("Error reading " + filename);
            e.printStackTrace();
        }
    }
}

Permutor está adaptado de una clase que escribí hace unos años para trabajar con permutaciones:

import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import java.math.BigInteger;
import static java.math.BigInteger.ZERO;
import static java.math.BigInteger.ONE;

public class Permutor {
    private final List<Integer> items;

    public Permutor(int n) {
        items = new ArrayList<>();
        for (int i = 0; i < n; ++i) items.add(i);
    }

    public BigInteger size() {
        return factorial(items.size());
    }

    private BigInteger factorial(int x) {
        BigInteger f = ONE;
        for (int i = 2; i <= x; ++i) {
            f = f.multiply(BigInteger.valueOf(i));
        }
        return f;
    }

    public List<Integer> nth(long n) {
        return nth(BigInteger.valueOf(n));
    }

    public List<Integer> nth(BigInteger n) {
        if (n.compareTo(size()) > 0) {
            throw new IllegalArgumentException("too high");
        }
        n = n.subtract(ONE);
        List<Integer> perm = new ArrayList<>(items);
        int offset = 0, size = perm.size() - 1;
        while (n.compareTo(ZERO) > 0) {
            BigInteger fact = factorial(size);
            BigInteger mult = n.divide(fact);
            n = n.subtract(mult.multiply(fact));
            int pos = mult.intValue();
            Integer t = perm.get(offset + pos);
            perm.remove((int) (offset + pos));
            perm.add(offset, t);
            --size;
            ++offset;
        }
        return perm;
    }

    public BigInteger which(List<Integer> perm) {
        BigInteger n = ONE;
        List<Integer> copy = new ArrayList<>(items);
        int size = copy.size() - 1;
        for (Integer t : perm) {
            int pos = copy.indexOf(t);
            if (pos < 0) throw new IllegalArgumentException("invalid");
            n = n.add(factorial(size).multiply(BigInteger.valueOf(pos)));
            copy.remove((int) pos);
            --size;
        }
        return n;
    }
}

Uso:

Con un cuadrado latino adentro latin.txt, comprímalo:

java Latin -c latin.txt latin.compressed

Y descomprimirlo:

java Latin -d latin.compressed latin.decompressed
David Conrad
fuente