Perl + Math :: {ModInt, Polynomial, Prime :: Util}, оценка ≤ 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/^␁//;$_}
Управляющие изображения используются для представления соответствующего управляющего символа (например ␀
, это буквенный символ NUL). Не беспокойтесь о попытке прочитать код; Ниже приведена более читаемая версия.
Беги с -Mbigint -MMath::ModInt=mod -MMath::Polynomial -MNtheory=:all
. -MMath::Bigint=lib,GMP
не является необходимым (и поэтому не включается в счет), но если вы добавите его раньше других библиотек, это заставит программу работать несколько быстрее.
Расчет балла
Алгоритм здесь несколько улучшен, но его будет сложнее написать (из-за того, что в Perl нет соответствующих библиотек). Из-за этого я сделал пару компромиссов между размером и эффективностью в коде, исходя из того, что, учитывая, что байты могут быть сохранены в кодировке, нет смысла пытаться сбить каждую точку с игры в гольф.
Программа состоит из 600 байтов кода плюс 78 байтов штрафов за параметры командной строки, что дает штраф в 678 баллов. Оставшаяся часть балла была рассчитана путем запуска программы для строки наилучшего и наихудшего (с точки зрения длины вывода) значений для каждой длины от 0 до 99 и каждого уровня излучения от 0 до 9; средний случай находится где-то посередине, и это дает оценки на счет. (Не стоит пытаться вычислить точное значение, если не появится другая запись с аналогичным счетом.)
Следовательно, это означает, что оценка эффективности кодирования находится в диапазоне от 91100 до 92141 включительно, поэтому окончательная оценка составляет:
91100 + 600 + 78 = 91778 ≤ оценка ≤ 92819 = 92141 + 600 + 78
Версия для гольфа с комментариями и тестовым кодом
Это оригинальная программа + переводы строк, отступы и комментарии. (На самом деле, версия для гольфа была создана путем удаления новых строк / отступов / комментариев из этой версии.)
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;
Алгоритм
Упрощение проблемы
Основная идея состоит в том, чтобы свести эту проблему «кодирования удаления» (которая не является широко исследованной) к проблеме кодирования стирания (всесторонне изученной области математики). Идея кодирования стирания заключается в том, что вы готовите данные для отправки по «каналу стирания», каналу, который иногда заменяет отправляемые символы «искаженным» символом, который указывает на известную позицию ошибки. (Другими словами, всегда ясно, где произошло искажение, хотя первоначальный персонаж все еще неизвестен.) Идея, лежащая в основе этого, довольно проста: мы разделяем входные данные на блоки длины ( излучение+ 1) и использовать семь из восьми битов в каждом блоке для данных, в то время как оставшийся бит (в этой конструкции, MSB) чередуется между установленным для всего блока, очищенным для всего следующего блока, установленным для блока после этого и так далее. Поскольку блоки длиннее, чем параметр излучения, по крайней мере один символ из каждого блока остается на выходе; поэтому, взяв серии символов с одним и тем же MSB, мы можем определить, к какому блоку принадлежит каждый символ. Количество блоков также всегда больше, чем параметр излучения, поэтому у нас всегда есть хотя бы один неповрежденный блок в encdng; таким образом, мы знаем, что все блоки, которые являются самыми длинными или связаны с самыми длинными, не имеют повреждений, что позволяет нам рассматривать любые более короткие блоки как поврежденные (таким образом, мусор). Мы также можем вывести параметр излучения следующим образом:
Стирание кодирования
Что касается части проблемы, связанной с кодированием стирания, то здесь используется простой частный случай конструкции Рида-Соломона. Это систематическая конструкция: выходной сигнал (алгоритма кодирования стирания) равен входному значению плюс количество дополнительных блоков, равных параметру излучения. Мы можем вычислить значения, необходимые для этих блоков, простым (и гольфистским!) Способом, обрабатывая их как искаженные объекты, а затем запуская алгоритм декодирования для них, чтобы «восстановить» их значение.
Реальная идея построения также очень проста: мы подгоняем полином минимально возможной степени ко всем блокам кодирования (с искажением, интерполированным из других элементов); если многочлен f , первый блок f (0), второй f (1) и т. д. Понятно, что степень многочлена будет равна количеству входных блоков минус 1 (потому что сначала мы подгоняем полином к ним, а затем используем его для построения дополнительных «проверочных» блоков); и потому, что d +1 баллов однозначно определяют многочлен степени dпри искажении любого количества блоков (вплоть до параметра излучения) количество неповрежденных блоков останется равным исходному вводу, что достаточно для восстановления одного и того же полинома. (Тогда мы просто должны оценить многочлен, чтобы разгрузить блок.)
Базовая конверсия
Последнее оставленное здесь соображение связано с фактическими значениями, взятыми блоками; если мы выполняем полиномиальную интерполяцию по целым числам, результаты могут быть рациональными числами (а не целыми числами), намного большими, чем входные значения, или иным образом нежелательными. Таким образом, вместо использования целых чисел мы используем конечное поле; в этой программе используемое конечное поле представляет собой поле целых чисел по модулю p , где p - наибольшее простое число, меньшее 128 излучения +1(т. е. наибольшее простое число, для которого мы можем поместить несколько различных значений, равных этому простому числу, в часть данных блока). Большим преимуществом конечных полей является то, что деление (кроме 0) определяется однозначно и всегда будет давать значение в этом поле; таким образом, интерполированные значения полиномов будут помещаться в блок точно так же, как и входные значения.
Чтобы преобразовать входные данные в серию блочных данных, нам нужно выполнить базовое преобразование: преобразовать входные данные из базового 256 в число, а затем преобразовать в базовое p (например, для параметра излучения 1 мы имеем p= 16381). В основном это было вызвано отсутствием в Perl подпрограмм преобразования баз (у Math :: Prime :: Util есть некоторые, но они не работают для баз bignum, и некоторые простые числа, с которыми мы здесь работаем, невероятно велики). Поскольку мы уже используем Math :: Polynomial для полиномиальной интерполяции, я смог использовать его как функцию «преобразования из последовательности цифр» (просматривая цифры как коэффициенты полинома и оценивая его), и это работает для больших чисел просто хорошо. Если пойти по другому пути, мне пришлось написать эту функцию самому. К счастью, это не слишком сложно (или многословно), чтобы написать. К сожалению, это базовое преобразование означает, что входные данные обычно отображаются нечитаемыми. Есть также проблема с ведущими нулями;
Следует отметить, что у нас не может быть более p блоков в выходных данных (в противном случае индексы двух блоков станут равными, и, тем не менее, возможно, потребуется создать различные выходные данные из полинома). Это происходит только тогда, когда ввод очень велик. Эта программа решает проблему очень простым способом: увеличивая излучение (что делает блоки больше и p намного больше, что означает, что мы можем разместить гораздо больше данных, и которое явно приводит к правильному результату).
Еще одно замечание, которое стоит отметить, - это то, что мы кодируем пустую строку для себя, потому что написанная программа в противном случае вылетит на ней. Это также, несомненно, наилучшее из возможных кодировок, и оно работает независимо от того, какой параметр излучения используется.
Потенциальные улучшения
Основная асимптотическая неэффективность в этой программе связана с использованием по модулю простого числа в качестве рассматриваемых конечных полей. Существуют конечные поля размером 2 n (это именно то, что мы хотели бы здесь, потому что размеры полезной нагрузки блоков, естественно, имеют степень 128). К сожалению, они скорее более сложны, чем простая конструкция по модулю, а это означает, что Math :: ModInt не будет его резать (и я не смог найти никаких библиотек на CPAN для обработки конечных полей не простых размеров); Мне нужно было бы написать целый класс с перегруженной арифметикой для Math :: Polynomial, чтобы иметь возможность его обрабатывать, и в этот момент стоимость байтов может потенциально перевесить (очень малую) потерю от использования, например, 16381, а не 16384.
Другое преимущество использования размеров степени 2 состоит в том, что базовое преобразование станет намного проще. Однако в любом случае был бы полезен лучший способ представления длины входных данных; метод «добавить 1 в неоднозначных случаях» прост, но расточителен. Биективное преобразование базы - один из возможных подходов (идея состоит в том, что у вас есть база в виде цифры, а 0 - не цифра, так что каждое число соответствует одной строке).
Хотя асимптотическая производительность этого кодирования очень хорошая (например, для входа длиной 99 и параметра излучения 3, кодирование всегда имеет длину 128 байтов, а не ~ 400 байтов, которые могут получить подходы, основанные на повторениях), его производительность менее хорош на коротких входах; длина кодирования всегда равна как минимум квадрату (параметр излучения + 1). Таким образом, для очень коротких входов (длина от 1 до 8) в излучении 9 длина выхода, тем не менее, равна 100. (При длине 9 длина выходного сигнала иногда составляет 100, а иногда и 110.) Подходы, основанные на повторениях, явно превосходят это стирание. -подход на основе кодирования на очень маленьких входах; возможно, стоило бы переключаться между несколькими алгоритмами в зависимости от размера ввода.
Наконец, это не совсем подходит для оценки, но при очень высоких параметрах излучения использование каждого байта (size выходного размера) для разделения блоков является расточительным; вместо этого было бы дешевле использовать разделители между блоками. Реконструировать блоки из разделителей довольно сложно, чем при использовании метода чередующихся MSB, но я считаю, что это возможно, по крайней мере, если данные достаточно длинные (при коротких данных может быть сложно вывести параметр излучения из выходного сигнала) , Это было бы на что посмотреть, если мы стремимся к асимптотически идеальному подходу независимо от параметров.
(И, конечно, может быть совершенно другой алгоритм, который дает лучшие результаты, чем этот!)