Perl, 2 · 70525 + 326508 = 467558
Dự đoán
$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}
Để chạy chương trình này, bạn cần tệp này ở đây , phải được đặt tên B
. (Bạn có thể thay đổi tên tệp này trong trường hợp thứ hai của ký tự B
ở trên.) Xem bên dưới để biết cách tạo tệp này.
Chương trình sử dụng kết hợp các mô hình Markov về cơ bản như trong câu trả lời này của user2699 , nhưng với một vài sửa đổi nhỏ. Điều này tạo ra một phân phối cho các nhân vật tiếp theo. Chúng tôi sử dụng lý thuyết thông tin để quyết định chấp nhận lỗi hay dành bit lưu trữ trong B
gợi ý mã hóa (và nếu có, làm thế nào). Chúng tôi sử dụng mã hóa số học để lưu trữ tối ưu các bit phân đoạn từ mô hình.
Chương trình dài 582 byte (bao gồm một dòng mới cuối cùng không cần thiết) và tệp nhị phân B
dài 69942 byte, vì vậy theo quy tắc cho điểm nhiều tệp , chúng tôi ghi điểm L
là 582 + 69942 + 1 = 70525.
Chương trình gần như chắc chắn đòi hỏi kiến trúc 64 bit (chút cuối?). Mất khoảng 2,5 phút để chạy trên một m5.large
phiên bản trên Amazon EC2.
Mã kiểm tra
# 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
Khai thác thử nghiệm giả định việc gửi trong tệp submission.pl
, nhưng điều này có thể dễ dàng thay đổi trong dòng thứ hai.
So sánh văn bản
"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
Mẫu này (được chọn trong câu trả lời khác ) xảy ra khá muộn trong văn bản, vì vậy mô hình này khá phát triển vào thời điểm này. Hãy nhớ rằng mô hình được tăng thêm 70 kilobyte "gợi ý" trực tiếp giúp nó đoán các ký tự; nó không bị điều khiển đơn giản bởi đoạn mã ngắn ở trên.
Tạo gợi ý
Chương trình sau đây chấp nhận mã gửi chính xác ở trên (trên đầu vào tiêu chuẩn) và tạo B
tệp chính xác ở trên (trên đầu ra tiêu chuẩn):
@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],'}}'
Phải mất khoảng thời gian dài để chạy như trình, vì nó thực hiện các tính toán tương tự.
Giải trình
Trong phần này, chúng tôi sẽ cố gắng mô tả những gì giải pháp này thực hiện đủ chi tiết để bạn có thể "tự thử tại nhà". Kỹ thuật chính phân biệt câu trả lời này với các câu hỏi khác là một vài phần dưới dạng cơ chế "tua lại", nhưng trước khi chúng ta đến đó, chúng ta cần thiết lập các điều cơ bản.
Mô hình
Thành phần cơ bản của giải pháp là một mô hình ngôn ngữ. Đối với mục đích của chúng tôi, một mô hình là một cái gì đó lấy một lượng văn bản tiếng Anh và trả về phân phối xác suất cho ký tự tiếp theo. Khi chúng tôi sử dụng mô hình, văn bản tiếng Anh sẽ là một số tiền tố (chính xác) của Moby Dick. Xin lưu ý rằng đầu ra mong muốn là một bản phân phối và không chỉ là một phỏng đoán duy nhất cho nhân vật có khả năng nhất.
Trong trường hợp của chúng tôi, về cơ bản, chúng tôi sử dụng mô hình trong câu trả lời này bởi user2699 . Chúng tôi không sử dụng mô hình từ các câu trả lời đạt điểm cao nhất (trừ riêng của chúng tôi) bởi Anders Kaseorg chính vì chúng tôi không thể trích xuất một phân phối chứ không phải là một đoán tốt nhất duy nhất. Về lý thuyết, câu trả lời đó tính toán một ý nghĩa hình học có trọng số, nhưng chúng tôi đã nhận được một số kết quả kém khi chúng tôi giải thích điều đó theo nghĩa đen. Chúng tôi "đánh cắp" một mô hình từ một câu trả lời khác vì "nước sốt bí mật" của chúng tôi không phải là mô hình mà là cách tiếp cận tổng thể. Nếu ai đó có mô hình "tốt hơn", thì họ sẽ có thể có kết quả tốt hơn bằng cách sử dụng các kỹ thuật còn lại của chúng tôi.
Một lưu ý, hầu hết các phương pháp nén như Lempel-Ziv có thể được coi là một "mô hình ngôn ngữ" theo cách này, mặc dù người ta có thể phải nheo mắt một chút. (Điều này đặc biệt khó khăn đối với thứ gì đó thực hiện chuyển đổi Burrows-Wheeler!) Ngoài ra, lưu ý rằng mô hình của user2699 là một sửa đổi của mô hình Markov; về cơ bản không có gì khác cạnh tranh cho thách thức này hoặc thậm chí có thể mô hình hóa văn bản nói chung.
Kiến trúc tổng thể
Với mục đích hiểu biết, thật tuyệt khi chia kiến trúc tổng thể thành nhiều phần. Từ quan điểm cấp cao nhất, cần có một chút mã quản lý nhà nước. Điều này không đặc biệt thú vị, nhưng để hoàn thiện, chúng tôi muốn nhấn mạnh rằng tại mọi thời điểm, chương trình được yêu cầu đoán tiếp theo, nó có sẵn tiền tố chính xác của Moby Dick. Chúng tôi không sử dụng những dự đoán không chính xác trong quá khứ theo bất kỳ cách nào. Vì mục đích hiệu quả, mô hình ngôn ngữ có thể sử dụng lại trạng thái của nó từ N ký tự đầu tiên để tính trạng thái của nó cho các ký tự (N + 1) đầu tiên, nhưng về nguyên tắc, nó có thể tính toán lại mọi thứ từ đầu mỗi khi được gọi.
Chúng ta hãy đặt "trình điều khiển" cơ bản của chương trình sang một bên và lén nhìn vào phần đoán nhân vật tiếp theo. Về mặt khái niệm, nó giúp tách ba phần: mô hình ngôn ngữ (đã thảo luận ở trên), tệp "gợi ý" và "trình thông dịch". Ở mỗi bước, trình thông dịch sẽ yêu cầu mô hình ngôn ngữ phân phối cho ký tự tiếp theo và có thể đọc một số thông tin từ tệp gợi ý. Sau đó, nó sẽ kết hợp những phần này thành một dự đoán. Chính xác những thông tin trong tập tin gợi ý cũng như cách sử dụng nó sẽ được giải thích sau, nhưng bây giờ nó giúp giữ cho các phần này tách biệt về mặt tinh thần. Lưu ý rằng triển khai khôn ngoan, tệp gợi ý đúng nghĩa là một tệp (nhị phân) riêng biệt nhưng nó có thể là một chuỗi hoặc một cái gì đó được lưu trữ bên trong chương trình. Như một xấp xỉ,
Nếu một người đang sử dụng một phương pháp nén tiêu chuẩn, chẳng hạn như bzip2 như trong câu trả lời này , tệp "gợi ý" tương ứng với tệp nén. "Trình thông dịch" tương ứng với bộ giải nén, trong khi "mô hình ngôn ngữ" hơi ẩn (như đã đề cập ở trên).
Tại sao sử dụng một tập tin gợi ý?
Hãy chọn một ví dụ đơn giản để phân tích thêm. Giả sử rằng văn bản N
dài các ký tự và được xấp xỉ bằng một mô hình trong đó mỗi ký tự là (độc lập) chữ cái E
có xác suất nhỏ hơn một nửa, T
tương tự với xác suất hơi nhỏ hơn một nửa và A
với xác suất 1/1000 = 0,1%. Giả sử không có nhân vật nào khác là có thể; trong mọi trường hợp, A
nó khá giống với trường hợp của một nhân vật chưa từng thấy trước đây ra khỏi màu xanh.
Nếu chúng tôi hoạt động theo chế độ L 0 (hầu hết, nhưng không phải tất cả, các câu trả lời khác cho câu hỏi này), sẽ không có chiến lược nào tốt hơn cho người phiên dịch hơn là chọn một trong E
và T
. Trung bình, nó sẽ nhận được khoảng một nửa số ký tự chính xác. Vậy E N / 2 và điểm N / 2 cũng vậy. Tuy nhiên, nếu chúng ta sử dụng chiến lược nén, thì chúng ta có thể nén đến ít hơn một bit cho mỗi ký tự. Vì L được tính bằng byte, chúng tôi nhận được L ≈ N / 8 và do đó điểm N / 4, tốt gấp đôi so với chiến lược trước đó.
Đạt được tỷ lệ này ít hơn một bit cho mỗi ký tự cho mô hình này là không cần thiết, nhưng một phương pháp là mã hóa số học.
Mã hóa số học
Như thường được biết đến, mã hóa là một cách biểu diễn một số dữ liệu bằng cách sử dụng bit / byte. Ví dụ, ASCII là mã hóa 7 bit / ký tự của văn bản tiếng Anh và các ký tự liên quan và nó là mã hóa của tệp Moby Dick gốc đang được xem xét. Nếu một số chữ cái phổ biến hơn các chữ cái khác, thì mã hóa có chiều rộng cố định như ASCII là không tối ưu. Trong tình huống như vậy, nhiều người tiếp cận với tiền mã hóa Huffman . Điều này là tối ưu nếu bạn muốn một mã cố định (không có tiền tố) với số bit nguyên cho mỗi ký tự.
Tuy nhiên, mã hóa số học thậm chí còn tốt hơn. Nói một cách đơn giản, nó có thể sử dụng các bit "phân đoạn" để mã hóa thông tin. Có rất nhiều hướng dẫn để mã hóa số học có sẵn trực tuyến. Chúng ta sẽ bỏ qua các chi tiết ở đây (đặc biệt là việc triển khai thực tế, có thể hơi rắc rối từ góc độ lập trình) vì các tài nguyên khác có sẵn trực tuyến, nhưng nếu ai đó phàn nàn, có lẽ phần này có thể được bổ sung thêm.
Nếu một người có văn bản thực sự được tạo bởi một mô hình ngôn ngữ đã biết, thì mã hóa số học cung cấp một mã hóa văn bản tối ưu về cơ bản từ mô hình đó. Theo một nghĩa nào đó, điều này "giải quyết" vấn đề nén cho mô hình đó. (Vì vậy, trong thực tế, vấn đề chính là mô hình không được biết đến và một số mô hình tốt hơn các mô hình khác về mô hình văn bản của con người.) Nếu nó không được phép mắc lỗi trong cuộc thi này, thì bằng ngôn ngữ của phần trước , một cách để tạo ra giải pháp cho thách thức này là sử dụng bộ mã hóa số học để tạo tệp "gợi ý" từ mô hình ngôn ngữ và sau đó sử dụng bộ giải mã số học làm "trình thông dịch".
Trong mã hóa cơ bản tối ưu này, cuối cùng chúng ta chi bit -log_2 (p) cho một ký tự có xác suất p và tốc độ bit tổng thể của mã hóa là entropy của Shannon . Điều này có nghĩa là một ký tự có xác suất gần 1/2 mất khoảng một bit để mã hóa, trong khi một ký tự có xác suất 1/1000 mất khoảng 10 bit (vì 2 ^ 10 là khoảng 1000).
Nhưng số liệu chấm điểm cho thử thách này được lựa chọn tốt để tránh nén là chiến lược tối ưu. Chúng ta sẽ phải tìm ra một số cách để tạo ra một số lỗi như một sự đánh đổi để có được một tệp gợi ý ngắn hơn. Ví dụ: một chiến lược mà người ta có thể thử là một chiến lược phân nhánh đơn giản: chúng ta thường cố gắng sử dụng mã hóa số học khi có thể, nhưng nếu phân phối xác suất từ mô hình là "xấu" theo cách nào đó chúng ta chỉ đoán được ký tự có khả năng nhất và không ' Hãy thử mã hóa nó.
Tại sao mắc lỗi?
Hãy phân tích ví dụ từ trước để thúc đẩy lý do tại sao chúng ta có thể muốn mắc lỗi "cố ý". Nếu chúng ta sử dụng mã hóa số học để mã hóa ký tự chính xác, chúng ta sẽ dành khoảng một bit trong trường hợp E
hoặc T
, nhưng khoảng mười bit trong trường hợp của một A
.
Nhìn chung, đây là một mã hóa khá tốt, chi tiêu hơn một chút cho mỗi ký tự mặc dù có ba khả năng; về cơ bản, điều A
này khá khó xảy ra và cuối cùng chúng ta không chi tiêu mười bit tương ứng của nó. Tuy nhiên, liệu có tốt không nếu chúng ta chỉ có thể gây ra lỗi thay vì trong trường hợp A
? Xét cho cùng, số liệu cho vấn đề coi 1 byte = 8 bit có độ dài tương đương với 2 lỗi; do đó, có vẻ như người ta nên thích một lỗi thay vì chi nhiều hơn 8/2 = 4 bit cho một ký tự. Chi nhiều hơn một byte để lưu một lỗi chắc chắn nghe có vẻ không tối ưu!
Cơ chế "tua lại"
Phần này mô tả khía cạnh thông minh chính của giải pháp này, đây là một cách để xử lý các dự đoán không chính xác mà không mất chi phí.
Đối với ví dụ đơn giản mà chúng tôi đã phân tích, cơ chế tua lại đặc biệt đơn giản. Trình thông dịch đọc một bit từ tệp gợi ý. Nếu nó là 0, nó đoán E
. Nếu nó là 1, nó đoán T
. Lần sau khi được gọi, nó sẽ thấy ký tự chính xác là gì. Nếu tệp gợi ý được thiết lập tốt, chúng tôi có thể đảm bảo rằng trong trường hợp E
hoặc T
, trình thông dịch đoán chính xác. Nhưng còn cái gì A
? Ý tưởng của cơ chế tua lại đơn giản là không mã hóa A
gì cả . Chính xác hơn, nếu sau đó người phiên dịch biết rằng ký tự chính xác là một A
, nó ẩn dụ " tua lại cuộn băng": nó trả về bit mà nó đã đọc trước đó. Bit nó đọc có ý định mã E
hoặcT
, nhưng không phải bây giờ; nó sẽ được sử dụng sau Trong ví dụ đơn giản này, về cơ bản, điều này có nghĩa là nó tiếp tục đoán cùng một nhân vật ( E
hoặc T
) cho đến khi nó đúng; sau đó nó đọc một bit khác và tiếp tục đi.
Mã hóa cho tệp gợi ý này rất đơn giản: biến tất cả các E
s thành 0 bit và T
s thành 1 bit, trong khi bỏ qua A
hoàn toàn s. Bằng cách phân tích ở cuối phần trước, sơ đồ này tạo ra một số lỗi nhưng làm giảm điểm tổng thể bằng cách không mã hóa bất kỳ của A
s. Là một hiệu ứng nhỏ hơn, nó thực sự cũng tiết kiệm độ dài của tệp gợi ý, vì cuối cùng chúng tôi sử dụng chính xác một bit cho mỗi E
và T
thay vì hơi nhiều hơn một chút.
Một định lý nhỏ
Làm thế nào để chúng ta quyết định khi nào gây ra lỗi? Giả sử mô hình của chúng tôi cung cấp cho chúng tôi phân phối xác suất P cho ký tự tiếp theo. Chúng tôi sẽ tách các ký tự có thể thành hai lớp: được mã hóa và không được mã hóa . Nếu ký tự chính xác không được mã hóa, thì cuối cùng chúng ta sẽ sử dụng cơ chế "tua lại" để chấp nhận lỗi mà không mất phí. Nếu ký tự đúng được mã hóa, thì chúng ta sẽ sử dụng một số phân phối Q khác để mã hóa nó bằng mã hóa số học.
Nhưng chúng ta nên chọn phân phối nào? Không quá khó để thấy rằng tất cả các ký tự được mã hóa đều có xác suất (tính bằng P) cao hơn các ký tự không được mã hóa. Ngoài ra, phân phối Q chỉ nên bao gồm các ký tự được mã hóa; Rốt cuộc, chúng tôi không mã hóa những cái khác, vì vậy chúng tôi không nên "chi tiêu" entropy cho chúng. Sẽ khó hơn một chút khi thấy rằng phân phối xác suất Q phải tỷ lệ thuận với P trên các ký tự được mã hóa. Đặt các quan sát này lại với nhau có nghĩa là chúng ta nên mã hóa các ký tự có khả năng nhất nhưng có thể không phải là các ký tự ít có khả năng hơn và Q chỉ đơn giản là P được định cỡ lại trên các ký tự được mã hóa.
Ngoài ra, nó còn chỉ ra rằng có một định lý hay về việc "cắt" nên chọn các ký tự mã hóa: bạn nên mã hóa một ký tự miễn là ít nhất là 1 / 5.393 như các ký tự được mã hóa khác kết hợp. Điều này "giải thích" sự xuất hiện của hằng số dường như ngẫu nhiên ở 5.393
gần cuối chương trình ở trên. Số 1 / 5.393 0.18542 là giải pháp cho phương trình -p log (16) - p log p + (1 + p) log (1 + p) = 0 .
Có lẽ đó là một ý tưởng hợp lý để viết ra thủ tục này trong mã. Đoạn mã này có trong 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;
}
Để tất cả chúng cùng nhau
Phần trước không may là một chút kỹ thuật, nhưng nếu chúng ta đặt tất cả các phần khác lại với nhau, cấu trúc như sau. Bất cứ khi nào chương trình được yêu cầu dự đoán nhân vật tiếp theo sau một ký tự chính xác:
- Thêm ký tự chính xác vào tiền tố chính xác đã biết của Moby Dick.
- Cập nhật mô hình (Markov) của văn bản.
- Các nước sốt bí mật : Nếu đoán trước đây là không chính xác, tua lại trạng thái của bộ giải mã số học để trạng thái của nó trước khi đoán trước!
- Yêu cầu mô hình Markov dự đoán phân phối xác suất P cho ký tự tiếp theo.
- Chuyển đổi P thành Q bằng chương trình con từ phần trước.
- Yêu cầu bộ giải mã số học giải mã một ký tự từ phần còn lại của tệp gợi ý, theo phân phối Q.
- Đoán nhân vật kết quả.
Mã hóa của tập tin gợi ý hoạt động tương tự. Trong trường hợp đó, chương trình biết nhân vật tiếp theo chính xác là gì. Nếu đó là một ký tự nên được mã hóa, thì dĩ nhiên người ta nên sử dụng bộ mã hóa số học trên nó; nhưng nếu nó không phải là ký tự không được mã hóa, thì nó sẽ không cập nhật trạng thái của bộ mã hóa số học.
Nếu bạn hiểu nền tảng lý thuyết thông tin như phân phối xác suất, entropy, nén và mã hóa số học nhưng đã cố gắng và không hiểu bài này (ngoại trừ lý do tại sao định lý là đúng), hãy cho chúng tôi biết và chúng tôi có thể cố gắng làm sáng tỏ mọi thứ. Cảm ơn vì đã đọc!