Perl + Math :: {ModInt, Polynomial, Prime :: Util}, punteggio ≤ 92819
$m=Math::Polynomial;sub l{($n,$b,$d)=@_;$n||$d||return;$n%$b,l($n/$b,$b,$d&&$d-1)}sub g{$p=$m->interpolate([grep ref$_[$_],0..$map{$p->evaluate($_)}0..$}sub p{prev_prime(128**$s)}sub e{($_,$r)=@_;length||return'';$s=$r+1;s/^[␀␁]/␁$&/;@l=map{mod($_,p$s)}l(Math::BigInt->from_bytes($_),p$s);$@l+$r>p($s)&&return e($_,$s);$a=0;join'',map{map{chr$_+$a}l($_->residue,128,$s,($a^=128))}g(@l)}sub d{@l=split/([␀-␡]+)/,$_[0];@l||return'';$s=vecmax map length,@l;@l=g map{length==$s&&mod($m->new(map{ord()%128}split//)->evaluate(128),p$s)}@l;$$_=$m->new(map{$_->residue}@l)->evaluate(p$s)->to_bytes;s/^␁//;$_}
Le immagini di controllo vengono utilizzate per rappresentare il carattere di controllo corrispondente (ad es. ␀
È un carattere letterale NUL). Non preoccuparti molto di provare a leggere il codice; c'è una versione più leggibile di seguito.
Corri con -Mbigint -MMath::ModInt=mod -MMath::Polynomial -MNtheory=:all
. -MMath::Bigint=lib,GMP
non è necessario (e quindi non è incluso nella partitura), ma se lo aggiungi prima delle altre librerie renderà il programma un po 'più veloce.
Calcolo del punteggio
L'algoritmo qui è in qualche modo migliorabile, ma sarebbe piuttosto difficile da scrivere (a causa del fatto che Perl non ha le librerie appropriate). Per questo motivo, ho fatto un paio di compromessi di dimensioni / efficienza nel codice, sulla base del fatto che dato che i byte possono essere salvati nella codifica, non ha senso cercare di eliminare ogni punto dal golf.
Il programma è composto da 600 byte di codice, più 78 byte di penalità per le opzioni della riga di comando, con una penalità di 678 punti. Il resto del punteggio è stato calcolato eseguendo il programma sulla stringa migliore e peggiore (in termini di lunghezza dell'uscita) per ogni lunghezza da 0 a 99 e ogni livello di radiazione da 0 a 9; il caso medio è da qualche parte nel mezzo, e questo dà dei limiti al punteggio. (Non vale la pena provare a calcolare il valore esatto a meno che non arrivi un'altra voce con un punteggio simile.)
Ciò significa quindi che il punteggio dall'efficienza di codifica è compreso tra 91100 e 92141 inclusi, quindi il punteggio finale è:
91100 + 600 + 78 = 91778 ≤ punteggio ≤ 92819 = 92141 + 600 + 78
Versione meno giocata, con commenti e codice di prova
Questo è il programma originale + nuove righe, rientri e commenti. (In realtà, la versione golf è stata prodotta rimuovendo newline / rientri / commenti da questa versione.)
use 5.010; # -M5.010; free
use Math::BigInt lib=>'GMP'; # not necessary, but makes things much faster
use bigint; # -Mbigint
use Math::ModInt 'mod'; # -MMath::ModInt=mod
use Math::Polynomial; # -MMath::Polynomial
use ntheory ':all'; # -Mntheory=:all
use warnings; # for testing; clearly not necessary
### Start of program
$m=Math::Polynomial; # store the module in a variable for golfiness
sub l{ # express a number $n in base $b with at least $d digits, LSdigit first
# Note: we can't use a builtin for this because the builtins I'm aware of
# assume that $b fits into an integer, which is not necessarily the case.
($n,$b,$d)=@_;
$n||$d||return;
$n%$b,l($n/$b,$b,$d&&$d-1)
}
sub g{ # replaces garbled blocks in the input with their actual values
# The basic idea here is to interpolate a polynomial through all the blocks,
# of the lowest possible degree. Unknown blocks then get the value that the
# polynomial evaluates to. (This is a special case of Reed-Solomon coding.)
# Clearly, if we have at least as many ungarbled blocks as we did original
# elements, we'll get the same polynomial, thus we can always reconstruct
# the input.
# Note (because it's confusing): @_ is the input, $_ is the current element
# in a loop, but @_ is written as $_ when using the [ or # operator (e.g.
# $_[0] is the first element of @_.
# We waste a few bytes of source for efficiency, storing the polynomial
# in a variable rather than recalculating it each time.
$p=$m->interpolate([grep ref$_[$_],0..$#_],[grep ref,@_]);
# Then we just evaluate the polynomial for each element of the input.
map{$p->evaluate($_)}0..$#_
}
sub p{ # determines maximum value of a block, given (radiation+1)
# We split the input up into blocks. Each block has a prime number of
# possibilities, and is stored using the top 7 bits of (radiation+1)
# consecutive bytes of the output. Work out the largest possible prime that
# satisfies this property.
prev_prime(128**$s)
}
sub e{ # encoder; arguments: input (bytestring), radiation (integer)
($_,$r)=@_; # Read the arguments into variables, $_ and $r respectively
length||return''; # special case for empty string
$s=$r+1; # Also store radiation+1; we use it a lot
# Ensure that the input doesn't start with NUL, via prepending SOH to it if
# it starts with NUL or SOH. This means that it can be converted to a number
# and back, roundtripping correctly.
s/^[␀␁]/␁$&/; #/# <- unconfuse Stack Exchange's syntax highlighting
# Convert the input to a bignum, then to digits in base p$s, to split it
# into blocks.
@l=map{mod($_,p$s)}l(Math::BigInt->from_bytes($_),p$s);
# Encoding can reuse code from decoding; we append $r "garbled blocks" to
# the blocks representing the input, and run the decoder, to figure out what
# values they should have.
$#l+=$r;
# Our degarbling algorithm can only handle at most p$s blocks in total. If
# that isn't the case, try a higher $r (which will cause a huge increase in
# $b and a reduction in @l).
@l+$r>p($s)&&return e($_,$s);
# Convert each block to a sequence of $s digits in base 128, adding 128 to
# alternating blocks; this way, deleting up to $r (i.e. less than $s) bytes
# will preserve the boundaries between each block; then convert that to a
# string
$a=0; # we must initialize $a to make this function deterministic
join'',map{map{chr$_+$a}l($_->residue,128,$s,($a^=128))}g(@l)
}
sub d{ # decoder: arguments; encdng (bytestring)
# Reconstruct the original blocks by looking at their top bits
@l=split/([␀-␡]+)/,$_[0];
@l||return''; # special case for empty string
# The length of the longest block is the radiation parameter plus 1 (i.e.
# $s). Use that to reconstruct the value of $s.
$s=vecmax map length,@l;
# Convert each block to a number, or to undef if it has the wrong length.
# Then work out the values for the undefs.
@l=g map{
# Convert blocks with the wrong length to undef.
length==$s&&
# Convert other blocks to numbers, via removing any +128 and then
# using Math::Polynomial to convert the digit list to a number.
mod($m->new(map{ord()%128}split// #/# <- fix syntax highlighting
)->evaluate(128),p$s)
}@l;
# Remove the redundant elements at the end; now that they've reconstructed
# the garbled elements they have no further use.
$#l-=$s-1;
# Convert @l to a single number (reversing the conversion into blocks.)
$_=$m->new(map{$_->residue}@l)->evaluate(p$s)
# Convert that number into a string.
->to_bytes;
# Delete a leading SOH.
s/^␁//; #/# <- unconfuse Stack Exchange's syntax highlighting
# Finally, return the string.
$_
}
### Testing code
use Encode qw/encode decode/;
# Express a string using control pictures + IBM437, to make binary strings
# easier for a human to parse
sub format_string {
($_)=@_;
$_ = decode("Latin-1", $_);
s/[\0-\x1f]/chr (0x2400 + ord $&)/eag;
s/\x7f/chr 0x2421/eag;
s/[ -~\x80-\xff]/decode("IBM437",$&)/eag;
encode("UTF-8","\x{ff62}$_\x{ff63}")
}
sub test {
my ($string, $radiation, $samples) = @_;
say "Input: ", format_string($string);
my $encoding = e($string, $radiation);
say "Encoding: ", format_string($encoding);
say "Input length ", length($string), ", encoding length ", length($encoding), ", radiation $radiation";
my $decoding = d($encoding);
$decoding eq $string or die "Mistake in output!";
say "Decoding: ", format_string($decoding), " from ",
format_string($encoding);
# Pseudo-randomly generate $samples radiation-damaged versions.
srand 1;
for my $i (1..$samples) {
my $encdng = $encoding;
for my $r (1..$radiation) {
substr $encdng, int(rand(length $encdng)), 1, "";
}
my $newdecoding = d($encdng);
say "Decoding: ", format_string($newdecoding), " from ",
format_string($encdng);
$newdecoding eq $string or die "Mistake in output!";
}
say "";
length $encoding;
}
test "abcdefghijklm", 1, 10;
test "abcdefghijklm", 2, 10;
test "abcdefghijklm", 5, 10;
test "abcdefghijklm", 10, 10;
test "\0\0\0\0\0", 1, 10;
test "\5\4\3\2\1", 2, 10;
test "a", 10, 10;
my %minlength = ();
my %maxlength = ();
for my $length (0..99) {
my ($min, $max) = ("", "");
$length and ($min, $max) =
("\2" . "\0" x ($length - 1), "\1" . "\377" x ($length - 1));
for my $radiation (0..9) {
$minlength{"$length-$radiation"} = test $min, $radiation, 1;
$maxlength{"$length-$radiation"} = test $max, $radiation, 1;
}
}
say "Minimum score: ", vecsum values %minlength;
say "Maximum score: ", vecsum values %maxlength;
Algoritmo
Semplificare il problema
L'idea di base è quella di ridurre questo problema di "codifica di cancellazione" (che non è ampiamente esplorato) in un problema di codifica di cancellazione (un'area della matematica esplorata in modo completo). L'idea alla base della codifica di cancellazione è che stai preparando i dati da inviare su un "canale di cancellazione", un canale che a volte sostituisce i caratteri che invia con un carattere "groviglio" che indica una posizione nota di un errore. (In altre parole, è sempre chiaro dove si è verificata la corruzione, anche se il personaggio originale è ancora sconosciuto.) L'idea alla base è piuttosto semplice: dividiamo l'input in blocchi di lunghezza ( radiazione+ 1) e usa sette degli otto bit in ciascun blocco per i dati, mentre il bit rimanente (in questa costruzione, MSB) si alterna tra l'impostazione di un intero blocco, azzera per l'intero blocco successivo, impostata per il blocco dopo quello e così via. Poiché i blocchi sono più lunghi del parametro di radiazione, almeno un carattere di ciascun blocco sopravvive nell'output; così prendendo sequenze di personaggi con lo stesso MSB, possiamo capire a quale blocco apparteneva ciascun personaggio. Anche il numero di blocchi è sempre maggiore del parametro di radiazione, quindi abbiamo sempre almeno un blocco intatto nell'encdng; sappiamo quindi che tutti i blocchi più lunghi o legati più a lungo sono integri, permettendoci di trattare eventuali blocchi più corti come danneggiati (quindi una garble). Possiamo anche dedurre il parametro di radiazione in questo modo (è '
Codifica di cancellazione
Per quanto riguarda la parte di codice di cancellazione del problema, questo utilizza un semplice caso speciale della costruzione Reed-Solomon. Questa è una costruzione sistematica: l'uscita (dell'algoritmo di codifica della cancellazione) è uguale all'ingresso più un numero di blocchi extra, pari al parametro di radiazione. Siamo in grado di calcolare i valori necessari per questi blocchi in modo semplice (e da golf!), Trattandoli come rifiuti, quindi eseguendo l'algoritmo di decodifica su di essi per "ricostruire" il loro valore.
L'idea reale alla base della costruzione è anche molto semplice: adattiamo un polinomio, il grado minimo possibile, a tutti i blocchi della codifica (con le garbature interpolate dagli altri elementi); se il polinomio è f , il primo blocco è f (0), il secondo è f (1) e così via. È chiaro che il grado del polinomio sarà uguale al numero di blocchi di input meno 1 (perché prima adattiamo un polinomio a quelli, quindi lo usiamo per costruire i blocchi extra "check"); e poiché d +1 punti definiscono in modo univoco un polinomio di grado d, confondendo un numero qualsiasi di blocchi (fino al parametro di radiazione) lascerà un numero di blocchi non danneggiati pari all'input originale, che è abbastanza informazioni per ricostruire lo stesso polinomio. (Non ci resta che valutare il polinomio come ungarble di un blocco.)
Conversione di base
L'ultima considerazione lasciata qui riguarda i valori effettivi presi dai blocchi; se eseguiamo interpolazione polinomiale sugli interi, i risultati possono essere numeri razionali (anziché interi), molto più grandi dei valori di input o altrimenti indesiderabili. Come tale, invece di usare gli interi, usiamo un campo finito; in questo programma, il campo finito utilizzato è il campo di numeri interi modulo p , dove p è il primo più grande inferiore a 128 radiazioni +1(ovvero il primo più grande per il quale possiamo adattare un numero di valori distinti pari a quel primo nella parte di dati di un blocco). Il grande vantaggio dei campi finiti è che la divisione (tranne per 0) è definita in modo univoco e produrrà sempre un valore all'interno di quel campo; pertanto, i valori interpolati dei polinomi si inseriranno in un blocco nello stesso modo dei valori di input.
Per convertire l'ingresso in una serie di dati di blocco, quindi, dobbiamo eseguire la conversione di base: convertire l'ingresso dalla base 256 in un numero, quindi convertire in base p (ad es. Per un parametro di radiazione di 1, abbiamo p= 16381). Ciò è stato in gran parte ostacolato dalla mancanza di routine di conversione delle basi di Perl (Math :: Prime :: Util ne ha alcune, ma non funzionano per le basi di bignum e alcuni dei numeri primi con cui lavoriamo qui sono incredibilmente grandi). Dato che stiamo già usando Math :: Polynomial per l'interpolazione polinomiale, sono stato in grado di riutilizzarlo come funzione "Converti da sequenza di cifre" (visualizzando le cifre come coefficienti di un polinomio e valutandolo), e questo funziona per i bignum va bene. Andando dall'altra parte, però, ho dovuto scrivere la funzione da solo. Fortunatamente, non è troppo difficile (o dettagliato) da scrivere. Sfortunatamente, questa conversione di base significa che l'input è in genere reso illeggibile. C'è anche un problema con gli zero iniziali;
Va notato che non possiamo avere più di p blocchi nell'output (altrimenti gli indici di due blocchi diventerebbero uguali e tuttavia potrebbe essere necessario produrre output diversi dal polinomio). Questo accade solo quando l'ingresso è estremamente grande. Questo programma risolve il problema in un modo molto semplice: aumentando radiazione (che rende i blocchi grandi e p molto più grande, che significa che possiamo sistemare più dati, e che chiaramente conduce a un risultato corretto).
Un altro punto degno di nota è che codifichiamo la stringa null su se stessa, perché altrimenti il programma come scritto andrebbe in crash. È anche chiaramente la migliore codifica possibile e funziona indipendentemente dal parametro di radiazione.
Potenziali miglioramenti
La principale inefficienza asintotica in questo programma ha a che fare con l'uso di modulo-prime come campi finiti in questione. Esistono campi finiti di dimensione 2 n (che è esattamente quello che vorremmo qui, perché le dimensioni del carico utile dei blocchi sono naturalmente una potenza di 128). Sfortunatamente, sono piuttosto più complessi di una semplice costruzione di moduli, il che significa che Math :: ModInt non lo taglierebbe (e non ho trovato alcuna libreria su CPAN per la gestione di campi finiti di dimensioni non prime); Dovrei scrivere un'intera classe con un'aritmetica sovraccarica affinché Math :: Polynomial sia in grado di gestirla, e a quel punto il costo in byte potrebbe potenzialmente superare la (molto piccola) perdita derivante dall'uso, ad esempio, 16381 anziché 16384.
Un altro vantaggio dell'utilizzo di dimensioni di potenza di 2 è che la conversione di base diventerebbe molto più semplice. Tuttavia, in entrambi i casi, sarebbe utile un metodo migliore per rappresentare la lunghezza dell'input; il metodo "anteporre un 1 in casi ambigui" è semplice ma dispendioso. La conversione della base biiettiva è un approccio plausibile qui (l'idea è che tu abbia la base come cifra e 0 come non una cifra, in modo che ogni numero corrisponda a una singola stringa).
Sebbene le prestazioni asintotiche di questa codifica siano molto buone (ad es. Per un input di lunghezza 99 e un parametro di radiazione di 3, la codifica è sempre lunga 128 byte, anziché i ~ 400 byte che otterrebbero gli approcci basati sulla ripetizione), le sue prestazioni è meno buono su input corti; la lunghezza della codifica è sempre almeno il quadrato del (parametro di radiazione + 1). Quindi per input molto brevi (lunghezza da 1 a 8) alla radiazione 9, la lunghezza dell'output è comunque 100. (Alla lunghezza 9, la lunghezza dell'output è talvolta 100 e talvolta 110.) Gli approcci basati sulla ripetizione battono chiaramente questa cancellazione -approccio basato sulla codifica su input molto piccoli; potrebbe valere la pena cambiare tra più algoritmi in base alla dimensione dell'input.
Infine, non si ottiene realmente nel calcolo del punteggio, ma con parametri di radiazione molto elevati, l'utilizzo di un bit di ogni byte (⅛ della dimensione dell'uscita) per delimitare i blocchi è dispendioso; sarebbe invece più economico usare delimitatori tra i blocchi. Ricostruire i blocchi dai delimitatori è piuttosto più difficile che con l'approccio a MSB alternato, ma ritengo sia possibile, almeno se i dati sono sufficientemente lunghi (con dati brevi, può essere difficile dedurre il parametro di radiazione dall'output) . Sarebbe qualcosa da considerare se si punta a un approccio asintoticamente ideale indipendentemente dai parametri.
(E ovviamente, potrebbe esserci un algoritmo completamente diverso che produce risultati migliori di questo!)