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
вище.) Див. Нижче, як створити цей файл.
Програма використовує комбінацію моделей Маркова, по суті, як у цій відповіді від користувача2699 , але з кількома невеликими модифікаціями. Це створює розподіл для наступного символу. Ми використовуємо теорію інформації для того, щоб вирішити, чи приймати помилку або витрачати шматочки зберігання на B
підказки кодування (і якщо так, то як). Ми використовуємо арифметичне кодування для оптимального зберігання дробових бітів у моделі.
Програма має 582 байти (включаючи непотрібний остаточний рядок), а двійковий файл B
- 69942 байт, тому згідно з правилами скорингу декількох файлів ми L
оцінюємо як 582 + 69942 + 1 = 70525.
Програма майже напевно вимагає 64-бітової (мало-ендіанської?) Архітектури. Для запуску 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 кілобайт «підказки», які безпосередньо допомагають їй відгадати персонажів; він не керується просто коротким фрагментом коду вище.
Створення натяків
Наступна програма приймає точний код подання вище (на стандартному введенні) та генерує точний 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. Зверніть увагу, що бажаний вихід - це розподіл , а не лише одна здогадка для найбільш ймовірного персонажа.
У нашому випадку ми, по суті, використовуємо модель у цій відповіді від користувача2699 . Ми не використали модель з відповіді найвищої оцінки (крім нашої власної) Андерса Касеорга саме тому, що нам не вдалося витягнути розподіл, а не єдину найкращу здогадку. Теоретично ця відповідь обчислює середньозважене геометричне значення, але ми отримали дещо погані результати, коли інтерпретували це занадто буквально. Ми "вкрали" модель з іншої відповіді, тому що наш "секретний соус" - це не модель, а загальний підхід. Якщо хтось має "кращу" модель, то він повинен мати можливість отримати кращі результати, використовуючи решту наших методик.
Як зауваження, більшість методів стиснення, такі як Лемпель-Зів, можна розглядати як "мовну модель" таким чином, хоча, можливо, доведеться трохи косити. (Це особливо хитро для чогось, що перетворює Берроуса-Уілера!) Також зауважте, що модель користувачем2699 є модифікацією моделі Маркова; по суті, ніщо інше не є конкурентоспроможним для цього завдання або, можливо, навіть моделювання тексту загалом.
Загальна архітектура
Для розуміння приємно розбити загальну архітектуру на кілька частин. З точки зору вищого рівня, має бути трохи коду управління державою. Це не особливо цікаво, але для повноти ми хочемо підкреслити, що в кожен момент програми запитують наступну здогадку, вона має у своєму розпорядженні правильний приставку Мобі Діка. Ми жодним чином не використовуємо наші помилкові здогадки. З метою ефективності мовна модель, ймовірно, може повторно використовувати свій стан з перших 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, а загальна швидкість передачі коду - ентропія Шеннона . Це означає, що символ з імовірністю біля 1/2 займає приблизно один біт для кодування, тоді як один з ймовірністю 1/1000 займає близько 10 біт (оскільки 2 ^ 10 приблизно 1000).
Але показник оцінки цього виклику був правильно підібраний, щоб уникнути стиснення як оптимальної стратегії. Доведеться розібратися, як зробити деякі помилки як компроміс для отримання більш короткого файлу підказок. Наприклад, одна стратегія, яку можна спробувати, - це проста стратегія розгалуження: ми, як правило, намагаємось використовувати арифметичне кодування, коли можемо, але якщо розподіл ймовірностей із моделі "поганий", ми просто здогадуємось про найімовірнішого символу і не робимо " не спробуйте його кодувати.
Навіщо робити помилки?
Проаналізуємо приклад раніше, щоб мотивувати, чому ми могли б хотіти помилки "навмисно". Якщо ми використовуємо арифметичне кодування для кодування правильного символу, ми витрачаємо приблизно один біт у випадку з E
або T
, але приблизно десять біт у випадку ан 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
повністю ігнорувати . Аналізуючи в кінці попереднього розділу, ця схема робить деякі помилки, але зменшує загальний бал, не кодуючи жодну з A
s. У меншому ефекті, він на самому ділі економить на довжину натяків файлу , а також, тому що ми в кінцевому підсумку , використовуючи рівно один біт для кожного E
і T
, замість того , щоб трохи більше , ніж трохи.
Трохи теореми
Як ми вирішуємо, коли зробити помилку? Припустимо, наша модель дає нам розподіл ймовірностей P для наступного символу. Ми розділимо можливі символи на два класи: закодовані та не закодовані . Якщо правильний символ не кодується, ми в кінцевому підсумку скористаємося механізмом "перемотування назад", щоб прийняти помилку без витрат. Якщо правильний символ зашифрований, то ми будемо використовувати якийсь інший розподіл Q для його кодування за допомогою арифметичного кодування.
Але який розподіл Q ми повинні вибрати? Не надто важко бачити, що всі закодовані символи повинні мати більш високу ймовірність (в P), ніж не кодовані символи. Також розподіл Q повинен включати лише закодовані символи; зрештою, ми не кодуємо інші, тому ми не повинні "витрачати" на них ентропію. Трохи складніше бачити, що розподіл ймовірностей Q повинен бути пропорційним P на кодованих символах. Якщо скласти ці спостереження разом, це означає, що ми повинні кодувати найбільш вірогідні символи, але, можливо, не менш вірогідні символи, і що Q просто P пересчитано на закодовані символи.
Крім того, виявляється, що існує класна теорема щодо того, яке "відсічення" слід вибрати для кодуючих символів: ви повинні кодувати символ до тих пір, поки це принаймні 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.
- Оновіть модель (Маркова) тексту.
- Секретний соус : Якщо попереднє припущення було невірним, перемотати стан арифметичного декодера його стан перед попередньої здогадкою!
- Попросіть модель Маркова передбачити розподіл ймовірностей P для наступного символу.
- Перетворіть P на Q, використовуючи підпрограму з попереднього розділу.
- Попросіть арифметичний декодер для декодування символу з залишку файлу підказки, відповідно до розподілу Q.
- Відгадайте отриманий персонаж.
Кодування файлу підказок працює аналогічно. У такому випадку програма знає, що є правильним наступним символом. Якщо це символ, який слід закодувати, то, звичайно, слід використовувати арифметичний кодер на ньому; але якщо це не кодований символ, він просто не оновлює стан арифметичного кодера.
Якщо ви розумієте інформаційно-теоретичну основу, як розподіл ймовірностей, ентропія, стиснення та арифметичне кодування, але намагалися та не зрозуміли цю посаду (за винятком того, чому теорема справжня), дайте нам знати, і ми можемо спробувати прояснити речі. Дякуємо за прочитане!