Perl,2·70525 + 326508 = 467558
预测变量
$m=($u=1<<32)-1;open B,B;@e=unpack"C*",join"",<B>;$e=2903392593;sub u{int($_[0]+($_[1]-$_[0])*pop)}sub o{$m&(pop()<<8)+pop}sub g{($h,%m,@b,$s,$E)=@_;if($d eq$h){($l,$u)=(u($l,$u,$L),u($l,$u,$U));$u=o(256,$u-1),$l=o($l),$e=o(shift@e,$e)until($l^($u-1))>>24}$M{"@c"}{$h}++-++$C{"@c"}-pop@c for@p=($h,@c=@p);@p=@p[0..19]if@p>20;@c=@p;for(@p,$L=0){$c="@c";last if" "ne pop@c and@c<2 and$E>99;$m{$_}+=$M{$c}{$_}/$C{$c}for sort keys%{$M{$c}};$E+=$C{$c}}$s>5.393*$m{$_}or($s+=$m{$_},push@b,$_)for sort{$m{$b}<=>$m{$a}}sort keys%m;$e>=u($l,$u,$U=$L+$m{$_}/$s)?$L=$U:return$d=$_ for sort@b}
要运行该程序,您需要在此处将此文件命名为B
。(您可以在上面字符的第二个实例中更改此文件名B
。)有关如何生成此文件的信息,请参见下文。
该程序使用了Markov模型的组合,本质上与user2699的答案相同,但做了一些小的修改。这将产生下一个字符的分布。我们使用信息论来决定是接受错误还是在B
编码提示中花费一些存储空间(如果可以,如何使用)。我们使用算术编码来最佳存储模型中的小数位。
该程序的长度为582个字节(包括不必要的最终换行符),二进制文件的B
长度为69942个字节,因此在对多个文件进行评分的规则下,我们的得分L
为582 + 69942 + 1 = 70525。
该程序几乎肯定需要64位(little-endian?)体系结构。m5.large
在Amazon EC2 上的实例上运行大约需要2.5分钟。
测试码
# Golfed submission
require "submission.pl";
use strict; use warnings; use autodie;
# Scoring length of multiple files adds 1 penalty
my $length = (-s "submission.pl") + (-s "B") + 1;
# Read input
open my $IN, "<", "whale2.txt";
my $input = do { local $/; <$IN> };
# Run test harness
my $errors = 0;
for my $i ( 0 .. length($input)-2 ) {
my $current = substr $input, $i, 1;
my $decoded = g( $current );
my $correct = substr $input, $i+1, 1;
my $error_here = 0 + ($correct ne $decoded);
$errors += $error_here;
}
# Output score
my $score = 2 * $length + $errors;
print <<EOF;
length $length
errors $errors
score $score
EOF
测试工具假定提交位于文件中submission.pl
,但是可以在第二行中轻松更改。
文字比较
"And did none of ye see it before?" cried Ahab, hailing the perched men all around him.\\"I saw him almost that same instant, sir, that Captain
"And wid note of te fee bt seaore cried Ahab, aasling the turshed aen inl atound him. \"' daw him wsoost thot some instant, wer, that Saptain
"And _id no_e of _e _ee _t _e_ore__ cried Ahab, _a_ling the __r_hed _en __l a_ound him._\"_ _aw him ___ost th_t s_me instant, __r, that _aptain
Ahab did, and I cried out," said Tashtego.\\"Not the same instant; not the same--no, the doubloon is mine, Fate reserved the doubloon for me. I
Ahab aid ind I woued tut, said tashtego, \"No, the same instant, tot the same -tow nhe woubloon ws mane. alte ieserved the seubloon ior te, I
Ahab _id_ _nd I ___ed _ut,_ said _ashtego__\"No_ the same instant_ _ot the same_-_o_ _he _oubloon _s m_ne_ __te _eserved the __ubloon _or _e_ I
only; none of ye could have raised the White Whale first. There she blows!--there she blows!--there she blows! There again!--there again!" he cr
gnly towe of ye sould have tersed the shite Whale aisst Ihere ihe blows! -there she blows! -there she blows! Ahere arains -mhere again! ce cr
_nly_ _o_e of ye _ould have ___sed the _hite Whale _i_st_ _here _he blows!_-there she blows!_-there she blows! _here a_ain__-_here again!_ _e cr
该示例(在另一个答案中选择)在文本中出现得很晚,因此到那时为止,该模型已经相当完善。请记住,该模型增加了70 KB的“提示”,可直接帮助其猜测字符。它不仅仅是由上面的简短代码片段驱动的。
产生提示
以下程序接受上面的确切提交代码(在标准输入上)并生成B
上面的确切文件(在标准输出上):
@S=split"",join"",<>;eval join"",@S[0..15,64..122],'open W,"whale2.txt";($n,@W)=split"",join"",<W>;for$X(0..@W){($h,$n,%m,@b,$s,$E)=($n,$W[$X]);',@S[256..338],'U=0)',@S[343..522],'for(sort@b){$U=($L=$U)+$m{$_}/$s;if($_ eq$n)',@S[160..195],'X<128||print(pack C,$l>>24),',@S[195..217,235..255],'}}'
由于它执行类似的计算,因此与提交运行所需的时间大约相同。
说明
在本节中,我们将尝试详细描述此解决方案的作用,以便您可以自己“在家尝试”。将此答案与其他答案区分开的主要技术是“倒带”机制,但在到达此处之前,我们需要建立基础知识。
模型
解决方案的基本要素是语言模型。就我们的目的而言,模型是一种需要一定数量的英文文本并在下一个字符处返回概率分布的事物。当我们使用模型时,英文文本将是Moby Dick的一些(正确)前缀。请注意,所需的输出是一个distribution,而不仅仅是对最可能出现的字符的一次猜测。
在我们的案例中,我们实际上是通过user2699在此答案中使用模型。我们之所以没有使用Anders Kaseorg得分最高的答案(不是我们自己的答案)中的模型,恰恰是因为我们无法提取分布而不是单个最佳猜测。从理论上讲,该答案计算的是加权几何平均值,但是当我们从字面上解释时,得出的结果有些差。我们从另一个答案中“窃取”一个模型,因为我们的“秘密调味料”不是模型,而是整体方法。如果某人拥有“更好”的模型,那么他们应该能够使用我们的其余技术获得更好的结果。
值得一提的是,大多数压缩方法(例如Lempel-Ziv)可以被视为是这种“语言模型”,尽管可能需要斜视一下。(对于执行Burrows-Wheeler转换的操作特别棘手!)此外,请注意user2699的模型是对Markov模型的修改。基本上,没有什么比这个挑战甚至对文本建模更具竞争力。
整体架构
为了便于理解,将整个体系结构分解为几部分是很不错的。从最高级别的角度来看,需要一些状态管理代码。这并不是特别有趣,但是为了完整起见,我们要强调的是,程序在每个点都被要求进行下一个猜测,它可以使用正确的Moby Dick前缀。我们不会以任何方式使用过去的错误猜测。为了提高效率,语言模型可以重用前N个字符的状态来计算前(N + 1)个字符的状态,但是原则上,每次调用它时,它都可以从头开始重新计算。
让我们将程序的基本“驱动程序”放在一边,并在猜测下一个字符的部分内进行浏览。从概念上讲,它有助于分离三个部分:语言模型(如上所述),“提示”文件和“解释器”。在每个步骤中,解释器都会向语言模型询问下一个字符的分布,并可能从提示文件中读取一些信息。然后将这些部分组合成一个猜测。提示文件中的确切信息以及使用方法将在稍后进行解释,但是目前它有助于在精神上将这些部分分开。请注意,在实现方面,提示文件实际上是一个单独的(二进制)文件,但它可能是字符串或程序中存储的内容。作为一个近似值,
如果在此答案中使用的是诸如bzip2之类的标准压缩方法,则“提示”文件对应于压缩文件。“解释器”对应于解压缩器,而“语言模型”则有点隐式(如上所述)。
为什么要使用提示文件?
让我们选择一个简单的示例进行进一步分析。假设文本是一个N
字符长的字符,并且由一个模型很好地近似,其中每个字符(独立地)是字母E
,概率略小于一半,T
类似地,概率略小于一半,且A
概率为1/1000 = 0.1%。假设没有其他字符可以使用;在任何情况下,它A
都与以前看不见的字符突然消失的情况非常相似。
如果我们在L 0体制下进行操作(与该问题的大多数其他答案一样,但不是全部),对于口译员而言,没有比选择E
和更好的策略了T
。平均而言,它将获得大约一半的正确字符。因此,E≈N/ 2,分数也≈N/ 2。但是,如果使用压缩策略,则每个字符可以压缩到多于一位。因为L以字节为单位,所以我们得到L≈N / 8,因此得分≈N / 4,是以前策略的两倍。
对于此模型,每个字符要达到一个多于一位的速率是不平凡的,但是一种方法是算术编码。
算术编码
众所周知,编码是一种使用位/字节表示某些数据的方式。例如,ASCII是英语文本和相关字符的7位/字符编码,它是所考虑的原始Moby Dick文件的编码。如果某些字母比其他字母更常见,那么像ASCII这样的固定宽度编码不是最佳的。在这种情况下,许多人开始使用霍夫曼编码。如果您想要一个固定(无前缀)代码且每个字符的位数为整数,则这是最佳选择。
但是,算术编码甚至更好。粗略地说,它能够使用“小数”位对信息进行编码。在线有许多算术编码指南。由于在线上有其他可用资源,我们将在这里跳过详细信息(尤其是实际实现,从编程角度来看可能有些棘手),但是如果有人抱怨,也许可以进一步完善本节。
如果一个人的文本实际上是由一种已知的语言模型生成的,则算术编码将提供该模型中文本的本质上最优的编码。从某种意义上说,这“解决”了该模型的压缩问题。(因此,在实践中,主要的问题是该模型不为人所知,有些模型在建模人工文本方面比其他模型要好。)如果不允许在比赛中犯错,则使用上一节的语言。 ,一种解决此问题的方法是使用算术编码器从语言模型生成“提示”文件,然后将算术解码器用作“解释器”。
在这种本质上最优的编码中,我们最终花费了-log_2(p)位用于概率为p的字符,并且编码的总体位速率为Shannon熵。这意味着一个概率接近1/2的字符需要大约一位进行编码,而概率为1/1000的一个字符需要大约10位(因为2 ^ 10大约为1000)。
但是,针对此挑战的评分标准是经过精心选择的,可以避免将压缩作为最佳策略。我们必须找出一些方法来犯一些错误,以作为获取较短提示文件的折衷方案。例如,一种可能尝试的策略是一种简单的分支策略:通常,我们会尽可能尝试使用算术编码,但是如果模型中的概率分布在某种程度上“不好”,我们只会猜测最可能的特征,而不会请尝试对其进行编码。
为什么会出错?
让我们从以前开始分析示例,以激发为什么我们可能要“有意地”犯错误。如果我们使用算术编码来编码正确的字符,则在E
或的情况下,我们将花费大约一位T
,而在的情况下,将花费约10位A
。
总体而言,这是一种非常不错的编码,即使存在三种可能性,每个字符也会花费一点点;基本上,这A
不太可能,而且我们最终不会花费太多相应的十位。但是,如果只发生错误而不是发生错误,那不是很好A
吗?毕竟,问题的度量标准认为1字节= 8位长度等于2个错误;因此,似乎应该更喜欢一种错误,而不是在一个字符上花费超过8/2 = 4位。花费多于一个字节来保存一个错误,听起来绝对不是最佳选择!
“倒带”机制
本节描述了此解决方案的主要巧妙方面,这是一种无需花费太多时间即可处理错误猜测的方法。
对于我们一直在分析的简单示例,倒带机制特别简单。解释器从提示文件中读取一位。如果为0,则猜测为E
。如果为1,则猜测为T
。下次调用它时,它将看到正确的字符。如果提示文件设置正确,我们可以确保在E
或的情况下T
,解释器可以正确猜测。但是呢A
?倒带机制的想法是根本不代码A
在所有。更准确地讲,如果解释器后来得知正确的字符是A
,则隐喻地“ 倒带 ”:它返回先前读取的位。它读取的位确实打算进行编码E
或T
, 但是不是现在; 稍后将使用。在这个简单的示例中,这基本上意味着它一直猜测相同的字符(E
或T
),直到正确为止。然后它再读一点,然后继续前进。
此提示文件的编码非常简单:将所有E
s都转换为0位,将T
s转换为1位,而全部都A
完全忽略s。通过上一节末尾的分析,该方案会产生一些错误,但由于不对任何A
s进行编码,因此总体上降低了得分。作为较小的效果,它实际上也节省了提示文件的长度,因为我们最终对E
和和T
仅仅使用了一位,而不是稍微多一点。
一点定理
我们如何确定何时出错?假设我们的模型为我们提供了下一个字符的概率分布P。我们将可能的字符分为两类:已编码和未编码。如果没有编码正确的字符,那么我们将最终使用“倒带”机制免费接受错误。如果编码了正确的字符,那么我们将使用其他分布Q通过算术编码对其进行编码。
但是,我们应该选择哪种分布Q?不难发现编码字符都应比未编码字符具有更高的概率(以P为单位)。同样,分布Q应该只包括编码字符;毕竟,我们不编码其他编码,所以我们不应该在它们上“花费”熵。看到概率分布Q应该与编码字符上的P成比例有点棘手。将这些观察结果放在一起意味着我们应该对最可能的字符进行编码,而可能对不太可能的字符进行编码,并且对编码的字符简单地重新缩放Q。
此外,事实证明,存在一个很酷的定理,其中一个编码字符应选择“截断”:您应编码一个字符,只要它与其他编码字符组合的可能性至少为1 / 5.393。这“解释”了5.393
接近上面程序结尾处的看似随机常数的外观。1 / 5.393≈0.18542是方程-p log(16)-p log p +(1 + p)log(1 + p)= 0的解。
用代码写出此过程也许是一个合理的想法。此代码段在C ++中:
// Assume the model is computed elsewhere.
unordered_map<char, double> model;
// Transform p to q
unordered_map<char, double> code;
priority_queue<pair<double,char>> pq;
for( char c : CHARS )
pq.push( make_pair(model[c], c) );
double s = 0, p;
while( 1 ) {
char c = pq.top().second;
pq.pop();
p = model[c];
if( s > 5.393*p )
break;
code[c] = p;
s += p;
}
for( auto& kv : code ) {
char c = kv.first;
code[c] /= s;
}
全部放在一起
不幸的是,上一节的内容有点技术性,但是如果将所有其他部分放在一起,其结构如下。每当要求程序预测给定正确字符后的下一个字符时:
- 将正确的字符添加到Moby Dick的已知正确前缀中。
- 更新文本的(Markov)模型。
- 的秘诀:如果先前的猜测是不正确的,倒退的算术解码器的状态,其状态之前的猜测面前!
- 要求马尔可夫模型预测下一个字符的概率分布P。
- 使用上一部分中的子例程将P转换为Q。
- 根据分布Q,要求算术解码器从提示文件的其余部分解码字符。
- 猜猜结果字符。
提示文件的编码操作类似。在这种情况下,程序将知道正确的下一个字符是什么。如果它是一个应该被编码的字符,那么当然应该在其上使用算术编码器。但是,如果它是未编码的字符,则不会更新算术编码器的状态。
如果您了解诸如概率分布,熵,压缩和算术编码之类的信息理论背景,但尝试并未能理解本文(除了定理为何成立),请告诉我们,我们可以尝试解决问题。谢谢阅读!