Perl +数学:: {ModInt,多项式,素数:: 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提取字符,我们可以算出每个字符属于哪个块。块的数量也总是大于辐射参数,因此在封装中我们总是至少有一个未损坏的块。因此,我们知道所有最长或捆绑时间最长的块均未损坏,从而使我们可以将任何较短的块视为已损坏(因此为乱码)。我们也可以像这样推导辐射参数
擦除编码
至于问题的纠删编码部分,这使用了Reed-Solomon结构的一个简单特殊情况。这是系统的结构:(纠删编码算法的)输出等于输入加上许多额外的块,等于辐射参数。我们可以通过将它们视为乱码,然后通过对它们运行解码算法来“重构”它们的值,从而以一种简单的方式(并且容易理解)计算这些块所需的值。
构造背后的实际思想也非常简单:我们将最小可能程度的多项式拟合到编码中的所有块(从其他元素中插入乱码);如果多项式为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进行多项式插值,因此我能够将其重新用作“从数字序列转换”功能(通过将数字视为多项式的系数并对其进行求值),并且该方法适用于bignums正好。但是,相反,我必须自己编写函数。幸运的是,编写起来并不难(或冗长)。不幸的是,这种基本转换意味着输入通常变得不可读。前导零还存在一个问题。
应当注意,输出中不能有p个以上的块(否则两个块的索引将相等,但是可能需要从多项式中产生不同的输出)。仅当输入非常大时才会发生这种情况。这个方案解决了一个非常简单的方法问题:增加辐射(这使得块大和p大得多,这意味着我们可以在更多的数据,并明确导致适合一个正确的结果)。
值得一提的另一点是,我们将空字符串编码为其自身,因为编写的程序否则将在其上崩溃。显然,它也是最佳的编码方式,无论辐射参数是什么,都可以正常工作。
潜在的改进
该程序中主要的渐近效率低下与使用模素数作为所讨论的有限域有关。存在大小为2 n的有限字段(这正是我们在这里想要的,因为块的有效载荷大小自然是128的幂)。不幸的是,它们比简单的模构造要复杂得多,这意味着Math :: ModInt不会削减它(而且我在CPAN上找不到任何库来处理非素数大小的有限字段);我必须用Math :: Polynomial编写一个带有重载算术的整个类才能处理它,到那时,字节开销可能会超过使用(例如16381)而不是(16384)所造成的(很小)损失。
使用2的幂的大小的另一个优点是基本转换将变得更加容易。但是,在任何一种情况下,代表输入长度的更好方法都是有用的。“在模棱两可的情况下添加1”是一种简单但浪费的方法。在这里,双射基数转换是一种可行的方法(想法是您将基数作为数字,而将0作为非数字,因此每个数字都对应一个字符串)。
尽管此编码的渐近性能非常好(例如,对于长度为99的输入和3的辐射参数,该编码始终为128字节长,而不是基于重复的方法将获得的〜400字节),但其性能输入短时效果较差;编码长度始终至少为(辐射参数+ 1)的平方。因此,对于辐射9处非常短的输入(长度1到8),输出的长度仍然是100。(在长度9处,输出的长度有时是100,有时是110。)基于重复的方法显然可以克服这种擦除基于编码的非常小的输入方法;根据输入的大小,可能需要在多种算法之间进行更改。
最后,它并没有真正出现在评分中,但是在辐射参数非常高的情况下,使用每个字节的一点(输出大小的三分之一)来分隔块是浪费的;而是在块之间使用定界符会更便宜。从定界符重构块比使用交替MSB方法要困难得多,但是我相信,至少在数据足够长的情况下才有可能(对于短数据,可能很难从输出中推断出辐射参数)。 。如果目标是渐近理想的方法而不考虑参数,那将是一件值得关注的事情。
(当然,可能有一种完全不同的算法可以产生比该算法更好的结果!)