<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"
    xmlns:dc="http://purl.org/dc/elements/1.1/">
    <channel>
        <title>Debiania</title>
        <link>https://blog.debiania.in.ua</link>
        <description><![CDATA[Посты на русском]]></description>
        <atom:link href="https://blog.debiania.in.ua/feeds/russian.rss" rel="self"
                   type="application/rss+xml" />
        <lastBuildDate>Sun, 16 Jul 2023 00:00:00 UT</lastBuildDate>
        <item>
    <title>ICFPC 2023</title>
    <link>https://blog.debiania.in.ua/posts/2023-07-16-icfpc-2023.html</link>
    <description><![CDATA[<p>Снова лето, снова моё любимое соревнование по программированию! В этот раз оно
стартовало в обед пятницы, а я опять не взял отпуск, поэтому с трудом дождался
вечера и побежал читать <a href="https://www.icfpcontest.com/specification_v3.pdf" title="ICFP Programming Contest 2023 v3">задание</a> (<a href="https://blog.debiania.in.ua/misc/icfpc-2023-spec-v3.pdf" title="ICFP Programming Contest 2023 v3">копия</a>). Начиналось оно
так:</p>
<blockquote>
<p>After a successful stint as painters, the denizens of Lambda land are now
turning to different artistic endeavors, and starting a new career in music.</p>
</blockquote>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/the-office-no-god-please-no.webp" width="1155px" height="687px" alt="Сцена из сериала «Офис». Майкл Скотт (которого играет Стив Карелл) кричит: Noo God! No. God. Please. No. No!!! No!!! NOOOOOOOO!!!" class="bleed" /></p>
</div>
<p>Только не Lambda land! Это был сеттинг <a href="https://blog.debiania.in.ua/posts/2017-09-03-icfpc-2017.html" title="ICFPC 2017 — Debiania">унылейшей задачи 2017-го
года</a>, и даже <a href="https://blog.debiania.in.ua/posts/2019-12-13-icfpc-2019.html" title="ICFPC 2019 — Debiania">весьма удачный сиквел
2019-го</a> не смог реабилитировать сеттинг в моих глазах.</p>
<p>Тем не менее, я продолжил читать. Нам предстояло расположить музыкантов на сцене
так, чтобы стоящие вокруг неё слушатели получили от концерта максимальное
удовольствие. Оно зависело от ряда факторов:</p>
<ul>
<li>инструмента, на котором играет исполнитель (у каждого слушателя свой
собственный коэффициент «вкуса» к каждому инструменту);</li>
<li>расстояния между каждым исполнителем и каждым слушателем;</li>
<li>громкости, с которой играет музыкант;</li>
<li>расстояния между исполнителями, использующими одинаковые инструменты.</li>
</ul>
<p>Кроме того, одни музыканты могли «загораживать» других, а в более поздних
задачах в зале появились колонны, которые тоже глушили звук.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2023-task30.png" width="748px" height="748px" alt="Визуализация 30-й задачи. Прямоугольная комната рандомно заполнена
    красными точками (слушателями). Посередине справа расположена прямоугольная
    сцена, по периметру которой расположилась часть музыкантов, а остальные — по
    центру, образуя выгнутую вверх дугу" class="bleed" /></p>
</div>
<p>В общем, задача на оптимизацию. Совсем не впечатлившись, я пошёл смотреть, что
успели сделать мои сокомандники. (Я, как обычно, выступал
с друзьями-<a href="https://codingteam.org.ru" title="Сайт Цодингтима">цодингтимовцами</a>).</p>
<p>Выяснилось, что у нас есть инфраструктура для скачивания задач и заливки
решений, а также тупейший «солвер», который просто ставит всех музыкантов в угол
сцены. Такое решение невалидно, т.к. между музыкантами должно быть не менее
десяти единиц расстояния. Так что я первым делом написал чуть более умный
вариант, расставлявший исполнителей на прямоугольной сетке. И сильно удивился,
когда он не смог решить некоторые задачи! Оказалось, что там много музыкантов
и маленькая сцена, и в прямоугольной сетке не хватает узлов. Я написал ещё один
солвер, расставлявший музыкантов <a href="https://ru.wikipedia.org/wiki/%D0%A3%D0%BF%D0%B0%D0%BA%D0%BE%D0%B2%D0%BA%D0%B0_%D0%BA%D1%80%D1%83%D0%B3%D0%BE%D0%B2" title="Упаковка кругов — Википедия">более плотно</a>, и мы
наконец получили свои первые очки.</p>
<p>Также я допилил нашу инфру, чтобы рядом с каждым нашим решением хранился JSON со
скором (так как функция его расчёта заметно тормозила). Это позволяло быстрее
перерешивать задачи новыми солверами. Чуть позже Гсомикс добавит в JSON поле
с названием солвера, сгенерировавшего решение — а потом, с появлением улучшающих
солверов, будет сокрушаться, что мы не храним там полную историю ☺ Урок на
будущее: запилить такую инфраструктуру сразу, и хранить в ней как можно больше
информации о том, как было получено каждое решение. <a href="https://discord.com/channels/1118159165060292668/1118159165060292671/1128809559000100955" title="@coffeecup.winner в Discord">Кто-то из соперников так
и поступает</a>:</p>
<blockquote>
<p>[…] in the end our winning solvers looked like the following when invoked from
the CLI: <code>greedy+shake{cap=10}+vol10+genetic+genetic</code>, which would translate
into a chain of solver passes with greedy, shake (local optimization in real
space) with cycle cap of 10 full cycles, setting volume to 10 and two genetic
passes on top. We also implemented <code>load_best</code> solver, so you could test
end-of-chain stuff directly on the best solution in our repo […].</p>
</blockquote>
<h1 id="суббота">Суббота</h1>
<p>Наутро в командном чатике шли обсуждения каких-то диких идей с производными,
BFGS и ещё чем-то непонятным. Энтузиазма разбираться во всём этом у меня не
было, поэтому я занялся самым простым, что пришло в голову: переделал свои
вчерашние солверы, чтобы они расставляли музыкантов на сетке случайным образом.
Эти алгоритмы быстро нашли улучшения по многим задачам, и я ещё некоторое время
гонял их с <code>xargs -P9</code>.</p>
<p>Загружая решения, я заметил, что скор задач на сайте отличается от того, что
вычисляем мы. Оказалось, что мы неправильно определяем, «загородил» ли один
музыкант другого. По условиям правило следующее: отрезок музыкант–слушатель не
должен проходить менее чем в пяти единицах расстояния от другого музыканта.
Форневер придумал элегантную реализацию: если представить, что отрезок «раздуло»
на пять единиц во все стороны, то получится <a href="https://en.wikipedia.org/wiki/Stadium_%28geometry%29" title="Stadium (geometry) — Wikipedia">стадион</a>. Нам всего
лишь нужно определить, находится ли второй музыкант внутри этого стадиона.
В расчёты закралась ошибка: мы проверяли расстояние до <em>прямой</em>
музыкант–слушатель, а не <em>отрезка</em>, поэтому стадион получался бесконечно
вытянутым и в него попадали «лишние» музыканты.</p>
<p>Я исправил проблему, но многие скоры все равно не сходились с сайтом
организаторов. Да что же такое?! К счастью, в этот момент подвезли обновлённую
спецификацию. Это традиция ICFPC — каждые сутки или чаще задача из нерешаемой
превращается сначала в вообще нерешаемую, а потом в «они там совсем уже, что
ли?». В нынешнем обновлении появились колонны и возможность «улучшать» игру
музыканта, ставя его рядом с другими исполнителями на том же инструменте.
Быстренько запилив поддержку этих фич в скоринг и выпустив таким образом пар,
я вернулся к отладке.</p>
<p>Оказалось, что я допустил в скоринге колонн ту же ошибку, которую только что
починил для пар музыкант–слушатель. После её исправления скоры стали получше —
погрешность 1% относительно тех, что насчитал сайт организаторов — но всё ещё не
сходились. После долгого вглядывания в код я наконец-то увидел, что:</p>
<ol type="1">
<li>мы считаем границу стадиона его частью, хотя по условию она должна быть исключена;</li>
<li>чтобы определить, принадлежит ли точка отрезку, мы используем правило
треугольника и проверяем, что сумма двух коротких сторон примерно равна
длинной. Но по алгоритму мы и так знаем, что точка лежит на прямой, поэтому
достаточно проверить, что расстояния от точки до концов отрезка не превышают
длины самого отрезка.</li>
</ol>
<p>С фиксами этих проблем некоторые наши скоры наконец совпали с сайтом
организаторов. Но не все. ☹</p>
<p>Фокстран, Гсомикс и Форневер тем временем запилили новый солвер. Идея его
проста: строим на сцене сетку, для каждого инструмента вычисляем скор в каждом
узле сетки, а затем пытаемся расставить музыкантов так, чтобы максимизировать
суммарный скор. Да, здесь не учтены ни колонны, ни влияние других музыкантов, но
этого все равно хватило, чтобы сразу превзойти все мои «рандомные» решения.</p>
<p>За окном уже спустились сумерки, а я за весь день всего лишь пофиксил пару багов
в скоринге. Нужно было срочно заняться чем-то более плодотворным. Так как мысли
продолжали крутиться вокруг расчёта оценок, я естественным образом пришёл
к тому, что надо сделать какой-то итеративный алгоритм поиска, а для этого нужно
сделать итеративным сам алгоритм скоринга.</p>
<p>Остаток вечера ушёл на то, чтобы разбить формулу скора на составляющие
и написать скелет итеративного расчёта. Можно было бы чуток быстрее, но в этом
году мы с подачи Форневера писали на F#, поэтому заметная часть времени ушла на
гугление синтаксиса. Глубоко в ночи я закоммитил скелет алгоритма и ушёл спать.</p>
<h1 id="воскресенье">Воскресенье</h1>
<p>Понимая, что дела движутся слишком медленно, я поставил себе максимально простую
цель: реализовать все тудушки в итеративном скоринге и запилить поверх него
эволюционный алгоритм. Но что-то не задалось. Отчасти по неопытности, а отчасти
из-за незнания F# я довольно долго пилил сначала геттеры-сеттеры для
составляющих скора, а потом собственно вычисления. В итоге первая рабочая версия
появилась только к четырём вечера, и… результат не сошёлся с тем скорингом, что
у нас уже был!</p>
<p>Ладно (•̀⤙•́ ) Закатал рукава, пофиксил ошибку копи-пасты, пофиксил индексирование
в массивах, пофиксил проверки валидности решения, добавил юнит-тест с задачей из
спеки, исправил ещё одну ошибку в «стадионе» (он игнорировал препятствия, если
линия музыкант–слушатель была вертикальной). Юнит-тест прошёл, но по сравнению
с имевшимся скорингом мой алгоритм работал ужасно медленно.</p>
<p>Тем временем в командном чате пишут, что у нас закончились бесплатные минуты
GitHub Actions.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2023-wait-guys-we-had-ci.jpg" width="520px" height="358px" alt="Сцена из фильма «Мы — Миллеры». Сбитый с толку Кенни (которого играет Уилл Поултер) спрашивает: У нас что, был CI?" class="bleed" /></p>
</div>
<p>Для оптимизации итеративного скоринга, конечно, пригодился бы профилировщик, но
я разрабатываю без IDE, поэтому оптимизировал «на глаз». Итеративный скоринг
с самого начала строился вокруг персистентных массивов, которые копируются при
каждом изменении, так что стал смотреть на них. Первым делом поменял сеттеры,
чтобы они не меняли массив, если элемент уже имеет нужное значение — это дало
ускорение в несколько раз.</p>
<p>Других явных проблем я не видел, поэтому взялся за микрооптимизации, а именно
расположение данных в памяти. Классы-составляющие хранили матрицы, например,
матрицу расстояний музыкант–слушатель. Естественно, в памяти это представлялось
одномерным массивом, а индексы пересчитывались по нехитрой формулке. Я ожидал
получить заметный прирост производительности, расположив данные в более удобном
для программы порядке, но как ни старался, разницы на глаз не заметил.</p>
<p>Пока возился с индексами, наступило прозрение: для изменения <em>любого</em> элемента
матрицы мне приходится копировать её <em>целиком</em>! Переведя код на <a href="https://en.wikipedia.org/wiki/Iliffe_vector" title="Iliffe vector — Wikipedia">вектора
Илиффа</a>, я получил десятикратный прирост производительности
и наконец достиг паритета производительности с нашим «неитеративным» скорингом.</p>
<p>Правда, результаты по-прежнему не сходились ни с имевшимся скорингом, ни
с сайтом организаторов.</p>
<p>На часах было 11 вечера, а из-за работы это был последний день, когда я могу
играть. Пока я занимался итеративным скорингом, мысль о локальном поиске
трансформировалась сначала в эволюционный алгоритм, а затем в имитацию отжига.
Времени его реализовать не оставалось, поэтому я попытался объяснить смысл
итеративного скоринга Форневеру в надежде, что в понедельник он сам сможет
что-то запилить. Фокстран тоже заинтересовался, но так ничего и не реализовал.</p>
<p>Заодно кратко обсудили последнее обновление задачи, в котором музыкантам
добавили громкость. Она варьировалась от 0 до 10, на неё умножался скор каждого
музыканта. Гсомикс не видел в громкости никакого толку, и все наши решения
задавали десятку всем музыкантам. Мне с трудом верилось в бесполезность этого
обновления, поэтому я взял какой-то из наших «улучшающих» солверов, использующий
<a href="https://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D1%82%D0%BE%D0%B4_%D0%9D%D0%B5%D0%BB%D0%B4%D0%B5%D1%80%D0%B0_%E2%80%94_%D0%9C%D0%B8%D0%B4%D0%B0" title="Метод Нелдера — Мида — Википедия">метод Нелдера — Мида</a>, и поправил его, чтобы оптимизировалась
громкость. К сожалению, это не дало улучшений ни по одной из опробованных задач,
поэтому я в расстроенных чувствах почапал спать.</p>
<p>Уже стоя под душем, я вдруг допёр, что на самом деле у громкости <em>есть</em> польза:
с её помощью можно заглушить музыкантов с негативным скором! И, конечно же, это
легче всего сделать с помощью итеративного скоринга, потому что:</p>
<ol type="1">
<li>там есть доступ к скору каждого музыканта (в «неитеративном» скоринге эти
данные сразу суммируются в общий скор);</li>
<li>там есть возможность быстро пересчитать финальный скор после того, как
музыканту поменяли громкость.</li>
</ol>
<p>Поэтому я вернулся за клавиатуру и быстренько запилил новый «улучшающий» солвер
под названием suosi — <a href="https://dictionary.cambridge.org/dictionary/english/shape-up-or-ship-out" title="Shape up or ship out! — Cambridge English Dictionary">«shape up or ship out»</a>. Он брал готовое
решение, выставлял всем музыкантам единичную громкость, считал скор, а затем
менял громкости в зависимости от скора каждого музыканта — кто-то получал 0,
а остальные 10. Этот солвер смог-таки улучшить некоторые решения, но на смешные
сотни тысяч единиц; общий скор решения к тому моменту мог превышать миллиард.</p>
<p>Сдав работу коллегам и попросив Форневера погонять suosi на остальных, более
крупных задачах, я всё же ушёл спать.</p>
<h1 id="итоги">Итоги</h1>
<p>На момент заморозки скорборда мы были 51-ми из 155-и (или 161, если считать тех,
кто не решил ни единой задачи). Не самый блестящий наш результат, но и не
худший; так, средненький. Окончательные результаты будут объявлены в сентябре во
время конференции ICFP.</p>
<p>Задача понравилась. Я не сразу загорелся энтузиазмом, но потом втянулся,
и в воскресенье было сложно засыпа́ть — голова кишела мыслями о том, что ещё
можно было бы предпринять.</p>
<p>F# мне понравился маржинально больше, чем Scala. Да, с ним тоже приходится
разбираться по ходу дела; да, я не знаю дотнетовских апишек; да, моя
производительность далека от той, которую я достиг бы на «своём» языке. Тем не
менее, язык вполне юзабельный, можно применять (раз в два года :)</p>
<p>В шаблон проекта обязательно надо добавлять не только юнит-тесты, но также
проперти-тесты и, возможно, бенчмарки. Уже в процессе написания этого отчёта
я понял, что отличия между итеративным и «неитеративным» скорингом проще было бы
отлаживать, напиши я проперти-тест на их эквивалентность.</p>
<p>В этом году Фокстран, Гсомикс и Форневер хорошо скооперировались, Портнов
отвалился после первого дня, а мы с Аконом работали в основном в одиночку.
Сложно сказать, хорошо это или плохо. Я, например, получил море фана за те два
дня, что пилил итеративный солвер. Мне греет душу, что написанные мной солверы
держали нас в лидерборде весь первый день. В общем, нам удалось поймать какой-то
локальный оптимум, где нашлось место и кооперации, и индивидуализму.</p>
<p>За сим откланиваюсь. Если есть желание — поглядите на <a href="https://github.com/codingteam/icfpc-2023/" title="codingteam / icfpc-2023 — GitHub">наш код на
GitHub</a>, а если желания нет — почитайте <a href="https://fornever.me/en/posts/2023-07-10.icfpc-2023.html" title="ICFP Contest 2023 — F. von Never">отчёт
Форневера</a> с более подробной хроникой работы всей команды. И до
встречи в лидерборде ICFPC 2024!</p>]]></description>
    <pubDate>Sun, 16 Jul 2023 00:00:00 UT</pubDate>
    <guid>https://blog.debiania.in.ua/posts/2023-07-16-icfpc-2023.html</guid>
    <dc:creator>Александр Батищев</dc:creator>
</item>
<item>
    <title>ICFPC 2022</title>
    <link>https://blog.debiania.in.ua/posts/2022-09-08-icfpc-2022.html</link>
    <description><![CDATA[<p>В этом году контест почти что не состоялся: его организуют волонтёры, и в этот
раз их поиск <a href="https://twiter.com/andrejbauer/status/1528318449038024704" title="Andrej Bauer on Twitter">сильно затянулся</a>. К счастью, в конце мая
<a href="https://twitter.com/andrejbauer/status/1530512128804896768" title="Andrej Bauer on Twitter">волонтёры нашлись</a>, и первые выходные сентября
я провёл, решая очередную задачку вместе с друзьями из
<a href="https://codingteam.org.ru" title="Codingteam — an open community of engineers and programmers">Codingteam</a>.</p>
<p>Вопреки традиции, отпуск мне взять не удалось, поэтому к команде
я присоединился только вечером пятницы, спустя четыре с половиной часа после
начала соревнования. <a href="https://raw.githubusercontent.com/icfpcontest2022/icfpcontest2022.github.io/a38d452791b43cf2b32cd89447c39ca4fe606b09/ContestSpecification_v2_4.pdf" title="ICFP Contest 2022. Specification V2.3 — Alperen Keles">Задание</a> (<a href="https://blog.debiania.in.ua/misc/icfpc-2022-spec-2.4.pdf" title="ICFP Contest 2022. Specification V2.3 — Alperen Keles">зеркало</a>) вызывало лёгкое
ощущение дежавю: создавая прямоугольные трафареты и закрашивая сквозь них
полотно, мы должны были как можно быстрее и точнее воспроизвести заданную
картинку. Самих картинок-задач под конец соревнования набралось аж 40 штук,
причём они варьировались от обманчиво-тривиальных изображений Тетриса до вполне
реальных картин вроде «Крика» Эдварда Мунка. В общем, классика ICFPC: задание,
которое легко объяснить, но сложно выполнить.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2022-solution-21.png" width="800px" height="400px" alt="Наше решение 21-й задачи — картины «Композиция с жёлтым, синим и красным» Пита Мондриана" class="bleed" /></p>
</div>
<p>Решение нужно было записать при помощи специального языка из пяти команд:
разрезать полотно на кусочки («блоки») по горизонтали, вертикали или на четыре
части, поменять два кусочка местами, склеить два кусочка или закрасить кусочек
определённым цветом. За каждое действие мы платили цену, причём за действия над
мелкими кусочками гораздо <em>больше</em>, чем за действия над крупными. Кроме того,
учитывалась степень похожести результата на заданную картинку; она вычислялась
как сумма отклонений цветов каждого пикселя.</p>
<p>В этом году мы писали на Haskell, так что пытаться понять <a href="https://github.com/codingteam/icfpc-2022" title="codingteam/icfpc-2022: Codingteam's Submission for ICFP Programming Contest 2022 — GitHub">наш
код</a> совершенно бесполезно ☺ Давайте я просто расскажу, как
мы три дня пытались хоть что-то решить.</p>
<h1 id="пятница">Пятница</h1>
<p>Когда я после работы завалился в командный чатик, portnov и ForNeVeR уже
придумали тривиальную стратегию: измельчить полотно на однопиксельные кусочки
и закрасить их необходимыми цветами. Дорого, зато «степень похожести»
идеальная, т.е. нулевая. В это же время sergevp, у которого не было возможности
играть все три дня, вывалил мне в приват полноценный план работ, тоже
включавший эту идею.</p>
<p>К сожалению, получившиеся решения нельзя было отправить, потому что они не
влезали в установленный организаторами двухмегабайтный лимит. Поэтому первое,
что я сделал — это залил для всех задач пустое решение, состоявшее из одного
только комментария «# nothing». Так как турнирная таблица сортировалась сначала
по убыванию количества решённых задач, а затем по возрастанию стоимости
решений, то мы сразу попали на восьмое место. Чудно!</p>
<p>Дальше я занялся оценщиком решений — вернее, интерпретатором, потому что для
вычисления оценки нужно знать площадь задействованных в команде блоков. Тут
меня ждала засада.</p>
<p>Условие задачи было сформулировано так, будто бы у блоков (кусочков полотна)
есть иерархия: они делятся на «простые», которые залиты одним цветом,
и «сложные», состоящие из нескольких простых. Кроме того, у каждого блока есть
идентификатор, например «0», и при разрезании блока из него образуются блоки
с идентификаторами «0.0» и «0.1». Мы, естественно, смоделировали это деревом.
Теперь же я сидел и думал, как реализовать команду склеивания блоков: если
разрезать полотно по горизонтали, затем каждую половинку разрезать по
вертикали, а потом попытаться склеить четвертинки, принадлежащие разным
половинкам, то придётся обновлять всю иерархию, начиная с корня!</p>
<p>Вообще-то деревья — это моя любимая структура данных, но такие широкомасштабные
манипуляции даже для меня были чересчур. Поэтому я решил попробовать другое
представление: хранить отображение из идентификатора блока в его положение
и размеры (<code>Map BlockId Shape</code>), а также отображение из идентификатора блока во
множество идентификаторов блоков, которые он содержит (<code>Map BlockId (Set BlockId)</code>). Успешно реализовав поверх этого все виды разрезов, я принялся за
команду объединения — и обнаружил, что она опять не получается! В этот раз
мешало то, что у простых блоков, находящихся внутри сложного, нет
идентификаторов — они теряются, когда блоки объединяются. Таким образом, второе
отображение вообще не имело смысла, т.к. я ничего не мог положить во множества.</p>
<p>Тут мне в голову полезли мысли о том, чтобы создать матрицу, в ячейках которой
хранить идентификатор соответствующего пикселю блока. Правда, искать границы
блоков в этом случае пришлось бы с помощью <a href="https://ru.wikipedia.org/wiki/%D0%97%D0%B0%D0%BB%D0%B8%D0%B2%D0%BA%D0%B0" title="Заливка — Википедия">заливки</a>, для
оптимизации которой пришлось бы опять создавать отображение из идентификатора
в положение и размеры…</p>
<p>Портнов тем временем сделал разрезалку полотна на четыре части и заливку
каждого квадранта его средним цветом, но это почему-то давало оценки хуже, чем
у моего «пустого» решения. Видимо, операции заливки обходились настолько
дорого, что их не удавалось компенсировать улучшением «похожести».</p>
<p>Спать я ушёл в подавленном настроении. Бывало, конечно, чтобы мы не могли
придумать или реализовать ни единой рабочей стратегии, но чтобы не удавалось
даже написать структуры данных — это было впервые.</p>
<h1 id="суббота">Суббота</h1>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2022-solution-35.png" width="800px" height="400px" alt="Решение 35-й задачи, сгенерированное описанным далее солвером «биллборд»" class="bleed" /></p>
</div>
<p>Мысли про матрицу идентификаторов не давали уснуть, и в полвторого ночи меня
наконец осенило — достаточно разделить сущности! Нужно хранить отдельно —
текущее состояние полотна (т.е. картинку) и отдельно — данные обо всех
существующих блоках. Всё! Отсюда, кстати, и метафора про трафареты в начале
этого поста: программа либо меняет существующие трафареты, разрезая и объединяя
их, либо меняет полотно, раскрашивая его сквозь какой-то из трафаретов. К трём
часам ночи интерпретатор был готов, и я, отчитавшись в чате, отправился спать.</p>
<p>Пока я спал, Портнов реализовал вычисление «похожести», а ещё написал скрипты
для заливки решений — теперь это можно было сделать одним вызовом, а не в шесть
кликов в браузере. После (позднего) завтрака я занялся допиливанием
интерпретатора до полноценного оценщика, чтобы он не только рисовал финальную
картинку, но и вычислял стоимость решения. К полвторого код был готов, но
результаты почему-то не сходились с вычислениями на сайте организаторов.
В итоге оказалось, что разность цветов у нас хранится в однобайтовых
переменных, которые иногда <a href="https://ru.wikipedia.org/wiki/%D0%98%D1%81%D1%87%D0%B5%D0%B7%D0%BD%D0%BE%D0%B2%D0%B5%D0%BD%D0%B8%D0%B5_%D0%BF%D0%BE%D1%80%D1%8F%D0%B4%D0%BA%D0%B0" title="Исчезнование порядка — Википедия">антипереполняются</a>, а результат
квадратного корня мы храним во <code>Float</code>, которому иногда не хватает точности.
Классика. Перевёл всё на <code>Double</code> — и результаты наконец сошлись. Потом ещё
обнаружилось, что на длинных решениях оценщик сжирает всю память, но это
оказалось (вполне предсказуемой) проблемой с ленивостью, и <code>deepseq</code> быстро
привёл всё в норму.</p>
<p>Тем временем организаторы выкатили обновлённую спецификацию. Это, кстати, фишка
ICFPC — задача в целом не меняется, но время от времени выходят обновления,
привносящие усложнения и сюрпризы. В этом, первом, обновлении организаторы
добавили задачи с заранее раскрашенными полотнами и уже нарезанными блоками.
Теперь нужно было не просто нарисовать что-то с нуля, а дорисовать или
перерисовать то, что уже есть. Ощущение дежавю немного усилилось — ровно такое
же обновление было <a href="https://blog.debiania.in.ua/posts/2018-10-28-icfpc-2018.html" title="ICFPC 2018 — Debiania">в 2018-м году</a>. Первой идеей было свести
задачу к предыдущей: объединить все блоки обратно в один большой, а затем
решать задачу уже существующими солверами. Портнов занялся парсером нового
формата задач, а Форневер трудился над алгоритмом вырезания заданного
прямоугольника из полотна.</p>
<p>Тут в чатик нагрянул Akon32 и, быстро сориентировавшись, взялся оптимизировать
вычислялку среднего цвета в заданной области. У Портнова был солвер, пытающийся
подобрать точку разрезания полотна так, чтобы заливка получившихся кусочков их
средними цветами давала бы наилучший результат; этот солвер дико тормозил из-за
неэффективности вычисления среднего. Akon32 намеревался помочь делу, используя
<a href="https://robocraft.ru/computervision/536" title="29. OpenCV шаг за шагом. Интегральное изображение">интегральное представление изображения</a>, которое должно было
дать расчёт суммы пикселей за константное время.</p>
<p>В интерпретаторе тоже было что оптимизировать, чем я и занялся. Похоже,
источником тормозов была команда, заливавшая область определённым цветом:
полотно хранилось в виде иммутабельной картинки, и для заливки мне приходилось
делать её копию. Я хотел перейти на мутируемое изображение, но запутался
в монадах и не понял, как это сделать. Оно и к лучшему: благодаря
иммутабельности мы на следующий день легко напишем обёртку, позволяющую
откатывать результаты команд — и солверы получат возможность перебирать
варианты действий и останавливаться на лучшем.</p>
<p>Портнов тем временем сделал солвер, заливавший все исходные блоки новых задач
их средним цветом. И, конечно же, в условиях контеста не обошлось без шуток
и прибауток:</p>
<blockquote>
<p>&lt;portnov&gt; отправил решения на 26-й, 28-й, 29-й задачи, которые получились
лучше чем “# nothing”</p>
<p>&lt;Minoru&gt; надеюсь, с комментарием «# better than nothing»? :)</p>
<p>&lt;portnov&gt; Minoru: that’s why we call it beta.</p>
<p>&lt;portnov&gt; ’cause it’s betta than nothin</p>
</blockquote>
<p>А потом мы застряли. Akon32 знал, какой алгоритм применить к вычислению
среднего, но очень долго писал его на Haskell. Портнов пытался ускорить уже
существующий алгоритм с помощью библиотеки repa, но безрезультатно. Форневер
продолжал заниматься алгоритмом вырезания прямоугольника, и тоже без прогресса.
Я же, как уже сказано выше, погряз в трясине монад. Чатик окутало уныние, и мы
даже немного поспорили, на каком языке писать в следующий раз.</p>
<p>Чтобы ощутить хоть какой-то прогресс, я плюнул на мутабельные изображения
и занялся скриптом, который запускал бы все солверы на всех задачах и записывал
лучшие решения в наш репозиторий. Форневер наконец-то заставил свой код
компилироваться, словил исключение и теперь ждал, пока проект пересоберётся
более старым компилятором — оказалось, что в GHC 9.0.2 сломано профилирование,
без которого не работают стектрейсы. А без стектрейсов, вобщем-то, Хаскель
дебажится только внимательным чтением кода.</p>
<p>Уже после полуночи в чат подтянулся pink_snow. Я спихнул на него доработку
своего скрипта, чтобы тот умел обрабатывать более новые задачи, а сам ещё раз
перечитал формулу стоимости решения и принялся рассматривать исходные картинки.
<a href="https://blog.debiania.in.ua/posts/2021-07-17-icfpc-2021.html" title="ICFPC 2021 — Debiania">Прошлогодний опыт</a> показал, что мне полезно решить хотя бы
одну задачку руками, прежде чем писать автоматизированные солверы.</p>
<p>Напомню, стоимость операции обратно пропорциональна размеру самого большого
блока, который она использует. Мне стало интересно, как выгоднее всего нарезать
полотно на столбцы. Можно резать пополам и потом рекурсивно нарезать половинки,
а можно отрезать по одному столбцу за раз. Первый подход очень быстрый, но
из-за того, что под конец он работает с очень маленькими блоками шириной
в несколько пикселей, его стоимость оказывается просто астрономической.
Постепенное нарезание на столбцы хоть и требует экспоненциально больше команд,
но зато мы постоянно работаем с самым большим из доступных блоков и,
соответственно, минимизируем общую стоимость программы. Контринтуитивно, но
с арифметикой не поспоришь!</p>
<p>В итоге получилось что-то вроде биллборда, составляющего изображение из
вертикальных полос. К двум часам ночи <code>Alt.Solver.Billboard</code> был готов, и даже
улучшил наши решения по паре задач. Довольный первым существенным прогрессом,
я ушёл спать.</p>
<h1 id="воскресенье">Воскресенье</h1>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2022-solution-1.png" width="800px" height="400px" alt="Решение первой задачи, которое pink_snow сгенерировал с помощью Python" class="bleed" /></p>
</div>
<p>За ночь pink_snow написал на Python программу, генерирующую решение первой
задачи (на картинке выше), а также ускорил вычисление среднего цвета. Это, как
и ожидалось, существенно помогло cолверу, искавшему оптимальную точку разреза,
и мы получили несколько улучшенных решений. Портнов, проснувшись, занялся
новыми задачами, в которых изначально уже присутствовали какие-то блоки:
объединив их обратно в один большой, можно было свести эти задачи к предыдущим
и применить существующие солверы.</p>
<p>Позавтракав, я занялся улучшениями «биллборда», которые придумал ещё вчера.
Например, очевидно, что идущие подряд столбцы одного цвета можно рисовать все
скопом, одним действием. А если «загрубить» цвета, то повторяющихся столбцов
станет больше, и программа получится короче. Реализация этих нехитрых
оптимизаций помогла существенно улучшить целый ряд решений.</p>
<p>В процессе я придумал ещё более крутое улучшение. Напомню, «биллборд» работает
рекурсивно, постоянно отрезая от текущей области левый столбец и уходя
в правую. Так как рабочая область постоянно уменьшается, то стоимость каждого
следующего шага растёт. Со стоимостью закрашивания ничего поделать нельзя:
я вынужден красить как можно более мелкие области, чтобы увеличить степень
«похожести». А вот стоимость разрезов вполне можно сделать постоянной, если
после каждого разреза склеивать полотно обратно!</p>
<p>Алгоритм теперь выглядел так:</p>
<ol type="1">
<li>закрашиваем полотно средним цветом левого столбца;</li>
<li>для каждого столбца, начиная со второго:
<ol type="a">
<li>разрезаем полотно по линии перед этим столбцом — фактически, делим
полотно на уже готовую и ещё не готовую части;</li>
<li>закрашиваем правую часть полотна средним цветом текущего столбца;</li>
<li>склеиваем половинки обратно.</li>
</ol></li>
</ol>
<p>Согласно моим расчётам, это должно было уменьшить стоимость программы
в 1,75 раз.</p>
<p>Пока я занимался реализацией этой идеи, Портнов закончил с алгоритмом
склеивания исходных блоков и принялся комбинировать разные солверы. Это дало
хорошие результаты (какой сюрприз!), и он принялся писать некое общее API для
упрощения такого комбинирования.</p>
<p>Тем временем я доделал новый алгоритм «биллборда» и обнаружил, что он не
улучшает ни одного из существующих решений. Он не был плохим, он был просто…
другим: всё то, что удалось сэкономить благодаря мержам, алгоритм тратил на
создание большего количества столбцов. У меня была надежда, что алгоритм
«выстрелит» на более новых задачах, поэтому я принялся писать своё собственное
склеивание начальных блоков; то, что написал Портнов, мне не подходило, т.к.
там не было гарантий полного склеивания.</p>
<p>Тут организаторы снова обновили задачу. Отныне предварительно созданные блоки
могли быть заполнены не просто цветом, а какими-то картинками. У нас уже не
было сил с этим разбираться, поэтому мы просто забили. Ещё организаторы
добавили в турнирную таблицу информацию о лучшем решении каждой из задач.
Форневер, отчаявшись написать свой алгоритм вырезания прямоугольника,
проанализировал циферки и выяснил, какие из задач мы решили сильно хуже, чем
соперники. Результаты явно указывали на более новые задачи, так что у моей
работы были все шансы как-то улучшить наше положение. Форневер тем временем
взялся писать ручную решалку на C# (потому что этот язык он знает хорошо, а GUI
на Haskell мы уже когда-то делали и это была боль).</p>
<p>К десяти вечера я доделал мерж начальных блоков. Алгоритм был простейшим, но
я сразу же получил улучшения по ряду задач. Затем я добавил сортировку, чтобы
алгоритм мержил пару, включавшую наибольший возможный блок — и снова получил
ряд улучшений! Учитывая, что воскресенье было для меня последним днём
соревнований (из-за работы), я был очень рад получить такой ощутимый прогресс.</p>
<p>Ближе к ночи Форневер попробовал запускать тривиальный «попиксельный» солвер не
на исходной картинке, а на каком-то её приближении: он взял Paint.NET и вырезал
из картинки с тетрисом часть фигурок. И это дало решение лучше, чем на исходной
задаче! Правда, мы так и не поняли, почему, и Форневеру не удалось развить эту
идею.</p>
<p>Запилив напоследок базовую поддержку задач с блоками-картинками (на которые мы
изначально забили) и получив несколько улучшенных решений (относительно
«пустых»), я довольный ушёл спать.</p>
<h1 id="понедельник">Понедельник</h1>
<p>В понедельник я был занят работой, поэтому уже ничего не писал. Портнов
и Форневер попробовали всякие мелкие улучшения к уже существующим солверам, но
результатов это не принесло. Так и финишировали.</p>
<h1 id="выводы">Выводы</h1>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2022-solution-13.png" width="800px" height="400px" alt="Наше решение тринадцатой задачи" class="bleed" /></p>
</div>
<p>К моменту заморозки турнирной таблицы мы были 69-е из 93-х команд, решивших все
40 задач, и 150-ти команд, участвовавших в соревновании. В общем, средненько во
всех смыслах слова. <strong>Update 2022.09.17</strong>: <a href="https://icfpcontest2022.github.io/scoreboard/" title="Scoreboard — ICFPC 2022">в финале</a> мы 71-е
из 151-го.</p>
<p>В этом году у нас было очень плохое взаимодействие: все разбрелись по углам
и пилили каждый своё. А ещё, перечитывая лог командного чатика, я постоянно
натыкался на весьма перспективные идеи, которые во время контеста были
проигнорированы. Например, Форневер в какой-то момент предлагал выбрасывать из
программ финальные команды и смотреть, не улучшается ли от этого решение.
Звучит как дополнительная работа, но если реализовать это непосредственно
в интерпретаторе, то оптимизация будет практически бесплатной и автоматически
применится ко всем солверам. Похоже, нам следует выделять время для
периодического <em>вдумчивого</em> обмена идеями.</p>
<p>Та же мысль звучала и во время ретроспективы: после чтения спеки нам следует не
просто написать в чатик впечатления, а собраться и составить список
первоочередных задач, вплоть до уровня модулей и функций. Тогда больше шансов,
что все будут работать в общем направлении. Звучит как формализм, но, возможно,
капельки формализма нам как раз и не хватает.</p>
<p>А ещё в этом году мы поздно принялись за GUI и поэтому не знаем, помог бы он
нам или нет.</p>
<p>В целом я доволен. Несмотря на то, что контест готовили в последний момент, он
получился достаточно оригинальным и интересным. Так что, дорогие читатели, если
уж вы добрались до этого абзаца — вам обязательно надо поучаствовать в ICFPC
2023! До него ещё год, так что можете скоротать время чтением <a href="https://blog.debiania.in.ua/tags/icfpc.html" title="Posts tagged “icfpc” — Debiania">моих прошлых
отчётов</a> ;) Если владеете английским, почитайте ещё и <a href="https://fornever.me/en/posts/2022-09-05.icfpc-2022.html" title="Report from ICFP Contest 2022 — F. von Never">отчёт
Форневера</a> — у него своя перспектива.</p>]]></description>
    <pubDate>Thu, 08 Sep 2022 00:00:00 UT</pubDate>
    <guid>https://blog.debiania.in.ua/posts/2022-09-08-icfpc-2022.html</guid>
    <dc:creator>Александр Батищев</dc:creator>
</item>
<item>
    <title>ICFPC 2021</title>
    <link>https://blog.debiania.in.ua/posts/2021-07-17-icfpc-2021.html</link>
    <description><![CDATA[<p>ICFP Programming Contest — это ежегодное онлайн-соревнование, длящееся три дня
и предлагающее весьма нестандартные, почти что жизненные задачи. Например,
в прошлые годы мы <a href="https://blog.debiania.in.ua/posts/2018-10-28-icfpc-2018.html" title="ICFPC 2018 — Debiania">управляли 3D-принтером</a>
и «дезинтегратором», писали эмулятор аркады и <a href="https://blog.debiania.in.ua/posts/2014-07-31-icfpc-2014.html" title="ICFPC 2014 — Debiania">играли
в Пакмана</a>, даже <a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-1.html" title="ICFPC 2020 — Debiania">участвовали в межгалатических «звёздных
войнах»</a>! В общем, скучно не бывает
(<a href="https://blog.debiania.in.ua/posts/2017-09-03-icfpc-2017.html" title="ICFPC 2017 — Debiania">почти</a>).</p>
<p>В этом году контест проходил с 9-го по 13-е июля. Я традиционно играл в составе
<a href="https://codingteam.org.ru" title="Codingteam, an open community of engineers and programmers">Codingteam</a>, которую также представляли Akon32, ForNeVeR,
pink-snow, portnov и sergevp. Последний с нами ещё не выступал, но он любит
задачки и весь предыдущий год обсуждал со мной ката в CodeWars. Вообще приятно,
что команда обрастает новыми людьми, которые затем превращаются в постоянных
участников: больше идей, больше рук для их реализации, больше фана и выше места́
в рейтинге.</p>
<p>Код писали на Scala 2.13. Да, старье, но тройка только-только вышла и вряд ли
готова, да и мы к ней не готовы тоже :) Моя <a href="https://blog.debiania.in.ua/posts/2015-08-07-scala-for-haskellers.html" title="Scala for Haskellers — Debiania">шпаргалка для
хаскелистов</a> актуальна до сих пор. Репозиторий со всеми
наработками можно найти <a href="https://github.com/codingteam/icfpc-2021" title="codingteam/icfpc-2021 — Codingteam's submission for ICFP Programming Contest 2021">на GitHub</a>.</p>
<p>Дальше я буду рассказывать в основном про то, что делал сам. За обзором работы
<em>всей</em> команды смотрите <a href="https://fornever.me/ru/posts/2021-07-12.icfpc-2021-report.html" title="Отчёт об ICFP Contest 2021 — F. von Never">отчёт ForNeVeR-а</a>.</p>
<h1 id="задание">Задание</h1>
<p>Организаторы вдохновлялись японской передачей Brain Wall: игрок стоит на месте,
за спиной у него бассейн, а спереди на него едет стена с отверстием причудливой
формы. Чтобы не оказаться в воде, игрок должен как-нибудь извернуться
и протиснуться в дыру. Проще показать:</p>
<div class="center">
<p>
<video muted controls loop style="max-width: 20rem;">
<source src="https://blog.debiania.in.ua/misc/icfpc-2021-problem-1.webm" type="video/webm">
Здесь должно было быть видео задачи №1, в которой нужно заставить человечка
выгнуться в форме буквы λ. К сожалению, ваш браузер не поддерживает тег video ☹
</video>
</p>
</div>
<p>Игрок — это граф на плоскости. Мы можем как угодно переставлять его вершины, но
длины рёбер должны оставаться почти неизменными (задан процент максимального
отклонения). Ну и, конечно же, кроме человечков и лямбд бывают задачи посложнее:</p>
<div class="center">
<p>
<video muted controls loop style="max-width: 20rem;">
<source src="https://blog.debiania.in.ua/misc/icfpc-2021-problem-64.webm" type="video/webm">
А здесь должно было быть видео 64-й проблемы, где надо было распутать «клубок»
из вершин и рёбер и растянуть его в какую-то кляксу; к сожалению, ваш браузер не
поддерживает тег video ☹
</video>
</p>
</div>
<p>Таблички «663» и «0», которые подымают судьи — это оценки решения. Они
измеряются в «дизлайках», поэтому чем меньше, тем лучше; ноль — вообще идеально.
Рассчитывается просто: от каждой вершины отверстия вычисляется квадрат
расстояния до ближайшей вершины фигуры; сумма этих квадратов и есть оценка
решения. (Внимательный читатель будет вознаграждён). Все подробности условия
можно прочесть <a href="https://blog.debiania.in.ua/misc/icfpc-2021-spec-v4.1.pdf" title="ICFP Contest 2021, Specification v4.1">здесь</a>.</p>
<p>В целом это было очень похоже на <a href="https://blog.debiania.in.ua/posts/2016-08-08-icfpc-2016.html" title="ICFPC 2016 — Debiania">задачу 2016-го года про
оригами</a>, только там мы разворачивали фигуру
в <em>прямоугольный</em> лист, а здесь — в <em>произвольную</em> форму. Сходство поверхностно,
читайте дальше :)</p>
<h1 id="что-я-делал-в-пятницу-и-субботу-z-72">Что я делал в пятницу и субботу (Z-72)</h1>
<p>Пока Форневер разбирался с API для отправки решений, мы с Портновым генерировали
идеи. В целом было понятно, что алгоритм будет итеративным: мы будем двигать
вершины фигуры внутрь отверстия, потом исправлять длины рёбер и снова двигать
вершины, и так далее аж пока не найдём решение. Для исправления длин нам
виделось несколько вариантов: можно как-то искать подходящие положения вершин
(координаты целочисленные, так что пространство поиска ограничено), а можно
рассчитывать «силы», возникающие в рёбрах, и двигать ими вершины в «правильное»
положение.</p>
<p>Я настолько увлёкся этой физической аналогией, что даже перечитал главу
Ландсберга про статику и осознал, какие силы возникали бы в рёбрах треугольника,
парящего в пространстве, если приложить силу к любой его вершине. Чуть позже
Форневер и sergevp независимо реализуют более простой «force solver», который
считает рёбра пружинами и учитывает только силу упругости, и мои открытия не
пригодятся.</p>
<p>Тем временем с начала контеста прошло уже четыре часа, и я, бросив свои
размышления о физике, взялся переводить наши модели данных и парсер JSON на
длинную арифметику — организаторы как раз подтвердили, что координаты могут быть
сколь угодно большими. Памятуя о задаче про оригами, где координаты были не
просто большими, а <em>дикими</em>, мы с готовностью повелись на провокацию и стали
везде впиливать <code>BigInt</code> и другие хитрости. Я предлагал просто масштабировать
большие задачи, а алгоритмы писать на <code>Long</code>, но эта идея не нашла поддержки,
и я забил.</p>
<p>Закончив с арифметикой, я быстренько написал «оценщик» решений, использующий
вышеописанную формулу, и занялся первым «решателем»: мой «rotation solver»
методом градиентного спуска искал самый удачный поворот фигуры. Никаких
серьёзных задач он решить не мог, но я надеялся, что его удастся использовать
как базу для более крутых алгоритмов. Тем временем у меня наступила ночь,
и я ушёл спать.</p>
<p>В субботу я появился в чате ровно в тот момент, когда обсуждение дошло до A*.
Кто не понимает, в чём тут соль — марш читать <a href="https://blog.debiania.in.ua/tags/icfpc.html" title="Posts tagged “icfpc” — Debiania">все мои предыдущие
отчёты</a> :) Портнов обратил внимание, что треугольники можно только
двигать и поворачивать — их нельзя ни сложить, ни скрутить, потому что от этого
ломаются рёбра. Следовательно, если выделить из фигуры некий «остов», то искать
его положение будет сильно проще. Вдохновившись этими идеями, я написал алгоритм
поиска отдельных треугольников, а затем поправил написанный Портновым алгоритм
Брезенхэма: он у нас работал только с целочисленными радиусами, а в задачах
встречались величины вроде <span class="math inline">\(26 \sqrt{2}\)</span>.</p>
<p>Тут у меня начало шатать Интернет, а у sergevp и вовсе выключили свет, но мы
продолжали бороться: sergevp решал задачи вручную, удерживая нас на 25-м месте,
а я занялся поиском «остова». К Z-46 этот код был готов, и я занялся развитием
«верификатора», чтобы можно было проверять, находится ли точка внутри отверстия
(там уже был похожий код, просто не было метода, который можно было бы дёрнуть
снаружи). Заодно написал ещё и генератор случайных точек, лежащих внутри
отверстия — задел для будущих стохастических алгоритмов (которые так и не
появились).</p>
<p>Тем временем Форневер написал-таки «физический движок» с рёбрами-пружинами
и обнаружил, что он почему-то не сходится к стабильному состоянию даже после
небольших начальных возмущений. Кроме того, «движку» не хватало сил, которые
заставляли бы фигуру разместиться <em>внутри</em> отверстия. Чтобы это исправить,
я впилил простенький костыль, тянущий все «наружные» вершины к центру отверстия.
Из-за неправильного расчёта этих сил возник прикольный баг: в одной из задач,
где фигурой был журавлик оригами, силы заставляли его перевернуться головой вниз
и улетать вниз и вправо, на юго-восток, в тёплые края. Починилось расчётом силы
относительно <em>текущего</em> положения фигуры, а не исходного.</p>
<p>Настроение команды к этому моменту несколько упало: прошло уже полтора дня,
а все наши решения до сих пор делались руками. У нас уже были какие-то общие
идеи и базовый код, но не было видения, как из этого собрать полноценный
«солвер».</p>
<p>В Z-40, пока я учил наш визуализатор загружать решения из файла, в чат вернулся
sergevp и рассказал, что он успел понапридумывать в оффлайне. Среди прочего
упомянул и неудачные идеи, например:</p>
<blockquote>
<p>автопоиск нулевых решений от вершин дырки по целым координатам — перебор
экспоненциален (на сложных задачах нереально), и требует очень хитрых
оптимизаций (типа гамильтонов путь в графе), за контест я его точно не напишу,
забил.</p>
</blockquote>
<p>Тут меня совсем стал рубить сон, и я ушёл спать.</p>
<h1 id="воскресенье-z-28">Воскресенье (Z-28)</h1>
<p>Проснулся с чёткой мыслью: sergevp забраковал идею совершенно зря, потому что
есть замечательные генетические алгоритмы, которые живо найдут ему нулевое
решение. Это ведь так просто: нужно просто каждой вершине фигуры сопоставить
вершину отверстия (да-да), тогда расстояния будут нулевыми по построению,
и оценка тоже будет нулевой! Это, конечно, уже комбинаторная задача, а не
экспоненциальная, но генетика всё осилит!</p>
<p>Реализация выглядела так: координаты, в которых находится вершина фигуры — это
«ген», а «особь» — это набор «генов», то есть решение задачи. Чтобы
сгенерировать случайную особь, берутся случайные вершины отверстия (с
повторами). При мутациях 10% вершин заменяются на случайные, а при скрещивании
создаётся новая особь, в которой 90% генов взяты из одного предка, а остальные
из другого. Ну, а дальше всё стандартно: оцениваем каждую особь, складываем их
в <code>TreeSet</code> и оставляем только 10 тысяч лучших. Потом проходимся по этому
«поколению», случайным образом мутируя и скрещивая особей, и начинаем новый
виток отбора.</p>
<p>Первым камнем преткновения стали локальные минимумы: алгоритм собирал все рёбра
где-нибудь в углу, игнорируя их длины, и упорно не желал «разворачивать» фигуру
и тянуться к вершинам отверстия. В попытке это побороть я добавил «перезапуски»:
если текущее наилучшее решение не менялось в течение трёх тысяч итераций,
я заменял 100 лучших особей новыми и продолжал эволюцию. Смысл был в том, чтобы
дать шанс более слабым особям эволюционировать и найти решение получше. Это
работало неплохо, но алгоритм все равно не сходился.</p>
<p>Затем я додумался глянуть на промежуточные решения в визуализаторе и обнаружил,
что алгоритм преспокойно создаёт рёбра за пределами отверстия. Чтобы это
предотвратить, мне нужно было «наказывать» особей с такими рёбрами. К сожалению,
наш «валидатор» не умел сообщать, <em>какое именно</em> ребро оказалось за границей,
поэтому я позаимствовал соответствующий код у sergevp. На портирование ушло
совсем немного времени, и к Z-22 я наконец-то смог заставить генетику не
выходить за границы отверстия, пригрозив ей дикими штрафами — четвёртой степенью
длины ребра-нарушителя. Это помогло, но сходимость не улучшилась.</p>
<p>Спустя ещё пару часов я стал подозревать, что почти-идентичные решения заполняют
всё «поколение» и мешают эволюции. Для борьбы с этим я добавил дедупликацию: для
каждой возможной оценки хранилась только одна особь. Это существенно замедлило
работу, но, вроде, немного помогло со сходимостью.</p>
<p>Впрочем, алгоритм всё ещё продолжал собирать точки в каком-то одном углу
отверстия, и я ввёл «налог на однообразие»: поделил оценку особи на квадрат
уникальных координат, которые она использует. От этого у солвера совсем сорвало
крышу, он опять начал выбираться за пределы отверстия, и я окончательно
разочаровался в этом подходе.</p>
<h1 id="ночью-minoru-вообще-расколбасило-z-19">«Ночью Minoru вообще расколбасило» (Z-19)</h1>
<p>Наблюдая за промежуточными решениями генетики, я всё больше утверждался в мысли,
что поиск можно было бы направлять, если учитывать длину рёбер. Я даже
проанализировал имеющиеся задачи и выяснил, что у отверстия максимум 108 вершин,
а у фигур — 224 вершины и 350 рёбер. Итого между вершинами отверстия можно
сформировать чуть менее шести тысяч рёбер, из которых часть окажется за
пределами отверстия и будет отбракована, а из остальных мы отберём те, длины
которых соответствуют рёбрам фигуры. (Ага.) Пространство поиска можно сократить
ещё сильнее, если учесть, что каждая вершина фигуры имеет рёбра строго
определённой длины: например, если из неё выходят рёбра длиной 3 и 5 единиц, то
её нельзя расположить в вершине отверстия, из которой выходят только рёбра
длиной 1 и 3 единицы.</p>
<p>Я кратко обсудил эту идею с sergevp и, не получив убойных контр-аргументов,
принялся за дело. Нужно понимать, что на часах у меня было уже полдесятого
вечера, я кодил 11 часов кряду, а в голове вертелась фраза «constraint
optimization» и воспоминания о <a href="https://codingnest.com/modern-sat-solvers-fast-neat-underused-part-1-of-n/" title="Modern SAT solvers: fast, neat and underused (part 1 of N) — The Coding Nest">нескольких постах майнтейнера Catch2</a>.
Изначально я думал, что поиском будет заниматься A*, но пока я занимался
отсеиванием рёбер, идея про A* мутировала в совершенно шальную мысль:
преобразовать всю задачу в boolean satisfiability problem! После этого её можно
было бы скормить любому из существующих SAT-солверов (которые оптимизировались
под эту задачу годами) и быстро получить решение.</p>
<p>Boolean satisfiability problem, или SAT, формулируется просто: есть булевы
(двоичные) переменные и несколько выражений с их участием; нужно найти такое
значение переменных, что все выражения истинны.</p>
<p>Представьте, что у фигуры <span class="math inline">\(F\)</span> вершин, а у отверстия — <span class="math inline">\(H\)</span>. Пусть булева
переменная <span class="math inline">\(f_ih_j\)</span> будет истинной, если <span class="math inline">\(i\)</span>-я вершина фигуры должна быть
размещена в <span class="math inline">\(j\)</span>-й вершине отверстия. Теперь можно сформулировать правила,
описывающие «нулевое» решение задачи:</p>
<ol type="1">
<li><p><em>каждая вершина фигуры соответствует ровно одной вершине отверстия</em>, то есть
среди переменных <span class="math inline">\(f_ih_1, f_ih_2, f_ih_3, \ldots, f_ih_H\)</span> истинной должна
быть ровно одна. Звучит просто, а вот на языке булевой алгебры выглядит
страшновато:</p>
<p><span class="math display">\[\begin{array}{crrcrcrcccrl}
       &amp; ( &amp;       f_ih_1 &amp; \land &amp; \lnot f_ih_2 &amp; \land &amp; \lnot f_ih_3 &amp; \land &amp; \ldots &amp; \land &amp; \lnot f_ih_H &amp; ) \\
\lor   &amp; ( &amp; \lnot f_ih_1 &amp; \land &amp;       f_ih_2 &amp; \land &amp; \lnot f_ih_3 &amp; \land &amp; \ldots &amp; \land &amp; \lnot f_ih_H &amp; ) \\
\lor   &amp; ( &amp; \lnot f_ih_1 &amp; \land &amp; \lnot f_ih_2 &amp; \land &amp;       f_ih_3 &amp; \land &amp; \ldots &amp; \land &amp; \lnot f_ih_H &amp; ) \\
\vdots &amp;   &amp;       \vdots &amp;       &amp;       \vdots &amp;       &amp;       \vdots &amp;       &amp; \ddots &amp;       &amp;       \vdots &amp;   \\
\lor   &amp; ( &amp; \lnot f_ih_1 &amp; \land &amp; \lnot f_ih_2 &amp; \land &amp; \lnot f_ih_3 &amp; \land &amp; \ldots &amp; \land &amp;       f_ih_H &amp; )
\end{array}\]</span></p>
<p>И так для каждой из <span class="math inline">\(F\)</span> вершин фигуры. Хорошо, что эти штуки будет
генерировать компьютер!</p></li>
<li><p><em>вершина фигуры может быть помещена только в ту вершину отверстия, из которой
исходят рёбра нужной длины</em>. Например, если вершина <span class="math inline">\(f_3\)</span> может быть помещена
только в вершины <span class="math inline">\(h_4\)</span>, <span class="math inline">\(h_7\)</span> и <span class="math inline">\(h_9\)</span> (потому что у других нет всех нужных
рёбер), должно выполняться следующее условие:</p>
<p><span class="math display">\[f_3 \land (h_4 \lor h_7 \lor h_9)\]</span></p></li>
<li><p><em>если между двумя вершинами фигуры существует ребро, то эти вершины нужно
размещать в соседних вершинах отверстия</em>. Например, если между вершинами
<span class="math inline">\(f_1\)</span> и <span class="math inline">\(f_3\)</span> есть ребро, и при этом вершина <span class="math inline">\(f_1\)</span> может быть размещена
в вершинах <span class="math inline">\(h_2\)</span> и <span class="math inline">\(h_4\)</span>, а <span class="math inline">\(f_3\)</span> — в <span class="math inline">\(h_8\)</span> и <span class="math inline">\(h_9\)</span>, и к тому же подходящие
рёбра образуются только парами <span class="math inline">\((h_2, h_8)\)</span> и <span class="math inline">\((h_4, h_9)\)</span>, то при размещении
<span class="math inline">\(f_1\)</span> в <span class="math inline">\(h_2\)</span> нужно обязательно разместить <span class="math inline">\(f_3\)</span> в <span class="math inline">\(h_8\)</span>, а при размещении
<span class="math inline">\(f_1\)</span> в <span class="math inline">\(h_4\)</span> — обязательно разместить <span class="math inline">\(f_3\)</span> в <span class="math inline">\(h_9\)</span>:</p>
<p><span class="math display">\[(f_1h_2 \land f_3h_8) \lor (f_1h_4 \land f_3h_9)\]</span></p></li>
<li><p><em>если между двумя вершинами фигуры существует ребро, то эти вершины не могут
быть помещены в одну и ту же вершину отверстия</em>. Это совершенно очевидное
правило пришло мне в голову гораздо позже, когда я начал смотреть на
результаты первых трёх и увидел, что солвер ради решения готов сломать всю
геометрию. Тут пришлось даже вывести формулку, похожую на импликацию, но как
бы наоборот: <span class="math inline">\(\lnot a \lor \lnot b\)</span> означает, что <span class="math inline">\(a\)</span> и <span class="math inline">\(b\)</span> не могут быть
истинными одновременно. Теперь, если между <span class="math inline">\(f_1\)</span> и <span class="math inline">\(f_4\)</span> есть ребро, то для
каждой из <span class="math inline">\(H\)</span> вершин отверстия нужно сгенерировать условие:</p>
<p><span class="math display">\[\lnot f_1h_j \lor \lnot f_4h_j\]</span></p></li>
</ol>
<p>Выглядит несложно, правда? На часах Z-16, мы занимаем 26-е место рейтинга (из
158-и команд), меня клонит в сон — превосходные условия для кодинга. Погнали!</p>
<h1 id="moonshot-z-16">Moonshot (Z-16)</h1>
<p>Между мной и идеальными решениями всех задач стояла всего одна преграда:
я никогда раньше не пользовался SAT-солверами. К счастью, в тех же постах
майнтейнера Catch2 был упомянут <a href="http://minisat.se/" title="MiniSat is a minimalistic, open-source SAT solver">Minisat</a>, чья документация быстро
прояснила ситуацию:</p>
<ol type="1">
<li>солвер принимает на вход не произвольные выражения, а некую «конъюнктивную
нормальную форму» — термин, который я не слышал со времён университета;</li>
<li>КНФ записывается не как формулы выше, а в неком формате DIMACS;</li>
<li>решение выдаётся в каком-то третьем формате, специфичном для Minisat.</li>
</ol>
<p>КНФ — это такая запись булевого выражения, где отрицания стоят только рядом
с переменными, переменные объединяются только с помощью логического ИЛИ, а между
объединениями стоят только логические И. Например, <span class="math inline">\((f_1h_1 \lor \lnot f_2h_9) \land f_3h_5\)</span>. Чтобы из вышеприведённых формул получить КНФ, нужно в цикле
применять два закона и одну теорему, найти которые можно в Википедии. Освежив
память, я взялся писать «упрощалку» выражений.</p>
<p>Сперва у меня произошёл фальстарт: я был настолько занят размышлениями о КНФ,
что заточил под него AST булевых выражений. Конечно, это замечательно, что
результат получается по построению, но на входе у меня всё же не КНФ. Переделав
как надо, к Z-13 я получил (пока что не реализованную) возможность закодировать
вышеприведённые формулы и выдать их в требуемой форме.</p>
<p>Формат DIMACS оказался простым текстом, в котором сперва нужно указать
количество переменных и «клоз» (clause), а потом перечислить клозы одну за
другой. Клозы — это те части КНФ, которые объединяются через И. На это ушёл ещё
часок, основная часть которого была потрачена на осознание того, что
<code>StringBuffer</code> для задачи не подходит и вместо него нужен <code>Array[String]</code>.</p>
<p>Где-то к Z-9, то есть середине ночи, я наконец-то закодировал первое правило,
преобразовал его в КНФ, вывел DIMACS, руками скопировал его в Minisat и получил
решение самой простой задачи: повернув треугольник на 90°, уместить его в такое
же треугольное отверстие. На более крупных задачах моя «упрощалка» выражений
просто загибалось. Прочитав Википедию чуть внимательнее, я узнал, что некоторые
выражения при переходе к КНФ растут экспоненциально. Чтобы это побороть,
я нагуглил <a href="https://www.cs.jhu.edu/~jason/tutorials/convert-to-CNF.html" title="How to Convert a Formula to CNF">алгоритм получше</a>, но тут же отложил — сперва нужно было
дописать остальные куски кода.</p>
<p>А оставалось не так уж много: ещё три правила и автоматический запуск Minisat
с разбором результата. Впрочем, правила требовали вычисления длин рёбер
и отсеивания вершин, так что вся эта работа была завершена только к Z-6, то есть
утру понедельника.</p>
<p>Тем временем командный чат ожил, и я даже попытался уговорить Форневера сделать
мне быстрый преобразователь в КНФ, но он глянул на остальные задачи и предпочёл
заняться чем-нибудь более перспективным. Всё правильно сделал :)</p>
<p>Спустя ещё часок мой солвер научился решать <em>две</em> простейших задачи, и я взялся
за очередной «баг»: на всех остальных задачах солвер говорил, что какие-то из
ребёр фигуры невозможно построить между вершинами отверстия.</p>
<p>Внимательный читатель, наверное, уже охрип орать на монитор; потерпи, осталось
всего три десятка слов. В Z-3:11 я наконец догадался открыть в визуализаторе
одно из «нулевых» решений, которые ребята сделали руками, и обнаружил, что
некоторые вершины фигуры не совпадают с вершинами отверстия! Дальше просто
цитирую чат:</p>
<blockquote>
<p>&lt;ForNeVeR&gt;<br />
Не все точки решения должны содержаться в узлах отверстия.<br />
<br /></p>
<p>&lt;Minoru&gt;<br />
для нуля дизлайков? Но почему?<br />
<br /></p>
<p>&lt;ForNeVeR&gt;<br />
Там обратное требование<br />
В каждой точке дыры нужно иметь одну из точек фигуры<br />
У фигуры больше точек, чем у дыры, поэтому остальные точки могут делать что им захочется (в пределах ограничений).<br />
Рейтинг смотрит на ближайшую точку фигуры к каждой точке дыры.<br />
<br /></p>
<p>&lt;Minoru&gt;<br />
*схватился за голову*<br />
то есть<br />
я два дня занимаюсь полной ерундой<br />
и вы это терпите?<br />
<br /></p>
<p>&lt;ForNeVeR&gt;<br />
Мы не поняли, чем ты занимаешься :(</p>
</blockquote>
<p>А вернее, я не объяснил толком, чем я занимаюсь, поэтому меня не успели
остановить и разубедить. Эх!</p>
<p>Забавно, что эта ошибка была допущена именно мной. Ведь это же я писал
«оценщик», я должен был лучше всех знать, как считается оценка! И тем не менее:
код я написал правильно, а принцип запомнил неправильно; построил на нём две
заведомо нерабочие идеи и успешно спустил два дня в трубу.</p>
<p>Выпив чаю и собравшись с мыслями, последние три часа контеста я решал задачи
вручную.</p>
<h1 id="выводы">Выводы</h1>
<p>Личные:</p>
<ul>
<li><p>думать наперёд полезно, но у меня получается не очень хорошо. Вместо чтения
Ландсберга нужно было закодить «очевидную» идею с рёбрами-пружинами
и посмотреть, как она работает;</p></li>
<li><p>первым делом надо писать визуализатор и «щупать» задачи руками. Писать
«солвер», не решив ни единой задачи вручную — это хороший способ получить
максимум фана и минимум результата;</p></li>
<li><p>ну и ещё пара, которые я не буду писать в Интернете, но над которыми уже
думаю.</p></li>
</ul>
<p>А про команду написать нечего. Я в этом году слишком мало в команде участвовал,
чтоб о ней выводы делать :)</p>
<h1 id="результат">Результат</h1>
<p>В lightning division (первые 24 часа соревнования) мы <a href="https://icfpcontest2021.github.io/scoreboard-lightning.html" title="Scoreboard (Lightning Division) — ICFPC 2021">26-е из
137-и</a>, в финале — <a href="https://icfpcontest2021.github.io/scoreboard.html" title="Scoreboard (Full Division) — ICFPC 2021">30-е из 160-и</a>. Это лучший
наш результат за всё время и я, вообще-то, удивлён. Портнов на протяжении всего
контеста рассказывал про умных дядь из сильных команд, которые наверняка уже три
дня молотят задачи с помощью чудо-алгоритмов на супер-мейнфреймах, а оказалось,
что команда из 6-1 человек, решающая задачи почти что вручную, способна я этими
«умными дядями» тягаться. Чудеса!</p>
<p>Если вы сюда дочитали, то, во-первых — спасибо! Во-вторых, ребята из команды TBD
(вы могли видеть их вверху рейтинга на прошлых ICFPC) хотят организовать
в первом квартале следующего года тренировку по ICFPC и дать задачку с какого-то
из прошлых контестов. <a href="https://discord.gg/kBXtcNYFNY" rel="nofollow">Приглашают всех в свой Discord</a>. Увидимся ^_^</p>]]></description>
    <pubDate>Sat, 17 Jul 2021 00:00:00 UT</pubDate>
    <guid>https://blog.debiania.in.ua/posts/2021-07-17-icfpc-2021.html</guid>
    <dc:creator>Александр Батищев</dc:creator>
</item>
<item>
    <title>LEGO-модель Harley-Davidson Fat Boy</title>
    <link>https://blog.debiania.in.ua/posts/2021-01-05-lego-harley-davidson-fat-boy.html</link>
    <description><![CDATA[<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-00.jpg" alt="Рабочее место с ноутбуком, телефоном, чашкой чая и LEGO-моделью
    мотоцикла" width="1622px" height="1080px" loading="lazy" class="bleed"></p>
</div>
<p>Вот такая вот LEGO-модель теперь выглядывает из-за моего монитора и соблазняет
плюнуть на всё и уехать в закат.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-01.jpg" alt="LEGO-двигатель в процессе сборки" width="1622px" height="1080px" loading="lazy" class="bleed"></p>
</div>
<p>«Сердце» всей конструкции — двигатель с настоящими поршнями и цилиндрами. Под
шестерёнкой с красной осью есть даже заводской номер!</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-02.jpg" alt="Двигатель и задняя часть рамы вместе с задним колесом" width="1622px" height="1080px" loading="lazy" class="bleed"></p>
</div>
<p>Цепь из 43-х звеньев будет передавать движение с колеса на двигатель. При
движении по шестерёнке цепь немного «дырчит», напоминая звук работы двигателя.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-03.jpg" alt="К заденй части добавилось крыло и номерной знак" width="1080px" height="1622px" loading="lazy" class="bleed"></p>
</div>
<p>Номерной знак, похоже, «с потолка»; примечательно лишь то, что Harley-Davidson
основан в штате Висконсин.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-04.jpg" alt="Добавлено седло и крышка бензобака" width="1622px" height="1080px" loading="lazy" class="bleed"></p>
</div>
<p>Восхищаюсь тем, как из кусочков получаются плавные линии седла и заднего крыла.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-05.jpg" alt="Добавлены детальки, изображающие сам бензобак" width="1622px" height="1080px" loading="lazy" class="bleed"></p>
</div>
<p>Всё, заднее крыло уже не впечатляет — поглядите на бензобак! А выхлопная труба,
вы видели?</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-06.jpg" alt="Добавлена рулевая ось и переднее колесо" width="1622px" height="1080px" loading="lazy" class="bleed"></p>
</div>
<p>Почти готово. Тащусь от того, что выхлопные трубы и амортизаторы вилки сделаны
из совершенно одинаковых деталей. Вообще в этой модели очень классный баланс
между квадратным леговским примитивизмом и не-квадратными детальками, придающими
модели сходство с настоящим мотоциклом.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-07.jpg" alt="Полностью собранная модель, вид сбоку" width="1622px" height="1080px" loading="lazy" class="bleed"></p>
</div>
<p>Собрал! Ноутбук с пятнадцатидюймовым экраном для масштаба ☺</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-08.jpg" alt="Полностью собранная модель, вид в три четверти" width="1622px" height="1080px" loading="lazy" class="bleed"></p>
</div>
<p>Руль просто шикарен. Там есть даже маленькие рычажки!</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-09.jpg" alt="Полностью собранная модель, вид в три четверти сзади" width="1622px" height="1080px" loading="lazy" class="bleed"></p>
</div>
<p>Впервые в жизни обратил внимание, что у каждого цилиндра своя собственная
выхлопная труба.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-10.jpg" alt="Приборная панель с большим круглым спидометром" width="1622px" height="1080px" loading="lazy" class="bleed"></p>
</div>
<p>Не знаю, почему на тахометре именно «1974», но спидометр и отделка бензобака
повторяют реальность.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-11.jpg" alt="Вид сзади вдоль всей длины модели на фоне монитора ноутбука" width="1080px" height="1622px" loading="lazy" class="bleed"></p>
</div>
<p>Играюсь с глубиной резкости. И нет, это не пятно на линзе, это курсор.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-12.jpg" alt="Тот же вид, но фокус теперь на кромке заднего крыла" width="1080px" height="1622px" loading="lazy" class="bleed"></p>
</div>
<p>Целиком всю длину модели в ГРИП уместить так и не удалось.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-13.jpg" alt="Вид спереди" width="1080px" height="1622px" loading="lazy" class="bleed"></p>
</div>
<p>В фаре можно разглядеть отражение горе-фотографа, заваливающего горизонт этой
фотографии ☺</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/lego-harley-davidson-fat-boy-14.jpg" alt="Модель на кухонных весах, показывающих 707 грамм" width="1080px" height="1622px" loading="lazy" class="bleed"></p>
</div>
<p>Каждая из 1023-х деталек весит меньше грамма, но в сборе получается весьма
увесистая игрушка.</p>
<hr />
<p><em>Изначально этот пост был галереей на Imgur.com, но теперь будет здесь — чтоб не
потерялся.</em></p>]]></description>
    <pubDate>Tue, 05 Jan 2021 00:00:00 UT</pubDate>
    <guid>https://blog.debiania.in.ua/posts/2021-01-05-lego-harley-davidson-fat-boy.html</guid>
    <dc:creator>Александр Батищев</dc:creator>
</item>
<item>
    <title>ICFPC 2020: итоги и выводы</title>
    <link>https://blog.debiania.in.ua/posts/2020-07-26-icfpc-2020-part-6.html</link>
    <description><![CDATA[<p>Это шестая, заключительная часть моей серии отчётов об ICFPC 2020. Остальные
части ищите здесь:</p>
<ol type="1">
<li><a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-1.html" title="ICFPC 2020: предисловие — Debiania">предисловие</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-2.html" title="ICFPC 2020: день первый — Debiania">день первый</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-23-icfpc-2020-part-3.html" title="ICFPC 2020: день второй — Debiania">день второй</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-24-icfpc-2020-part-4.html" title="ICFPC 2020: день третий — Debiania">день третий</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-25-icfpc-2020-part-5.html" title="ICFPC 2020: день четвёртый — Debiania">день четвёртый</a>;</li>
<li>итоги и выводы (это то, что вы сейчас читаете).</li>
</ol>
<hr />
<p><a href="https://icfpcontest2020.github.io/#/scoreboard#final" title="ICFP Programming Contest 2020 — Pre-Final Scoreboard">Мы заняли 38-е место из 95-и</a>, при этом 30 команд не
заработали ни единого балла. Я удивлён, что мы не оказались среди их числа.</p>
<h1 id="о-задании">О задании</h1>
<p>Делать тизеры частью задания нельзя. ICFPC длится 72 часа, а не две недели. Те,
кто вникал в тизеры, в час T просто нажали F5 на ReadTheDocs и продолжили
то, что делали предыдущие 14 дней. Формально это давало некоторым командам
преимущество.</p>
<p>Дальше. <a href="https://icfpcontest2020.github.io/#/rules" title="ICFP Programming Contest 2020 — Contest Rules">Правила соревнования</a><a href="#fn1" class="footnote-ref" id="fnref1" role="doc-noteref"><sup>1</sup></a>, пункт 2, гласят (выделение моё):</p>
<blockquote>
<p>[…] <strong>Teams may not</strong> divide, merge, or <strong>collaborate</strong> after the start of the
contest.</p>
</blockquote>
<p>Вопреки этому соревнование началось именно с призыва собраться в Discord
и сообща разгадывать сообщения. Первые сутки мы не могли делать <em>ничего
другого</em>, кроме как коллаборироваться. Соревнование как таковое началось <a href="https://icfpcontest2020.github.io/#/post/2056" title="How to Actually Win the Contest — Blog — ICFP Programming Contest 2020">только
спустя 24 часа 45 минут</a>.</p>
<p>«Космолисп» и galaxy.txt оказались просто большим сайд-квестом. Да, там были
туториалы, способные помочь с основной задачей, но с тем же успехом мы,
наверное, могли проспать первые 46 часов, а после <a href="https://icfpcontest2020.github.io/#/post/2059" title="Relatively Fresh News From the World of Reverse-Engineering — Blog — ICFP Programming Contest 2020">публикации описания
команд</a> зависнуть в Discord и таким образом выяснить всё, чего не
знали.</p>
<p>За «космолисп» обидно вдвойне: вещь красивая, но программировать на нём довелось
только при написании тестов.</p>
<p>Задание было, фактически, «матрёшкой» из загадок, а ядром её оказалось довольно
скучная задача по управлению космофлотом. Да, там была неожиданная метрика
расстояний и странная гравитация, но интереса задаче придавали <em>исключительно</em>
загадки, в которые она была спрятана.</p>
<p>Я думаю, что понимаю исходные организаторов: сеттинг, загадки, «матрёшечность»
задания, финальное видео — всё это складывается во вполне стройный набор идей
и предпосылок. Озвученные выше огрехи допущены вовсе не по расхлябанности; они
являются прямым следствием попытки сделать уникальный контест.</p>
<p>Как я уже писал, ICFPC похож на жизнь, но у него есть одно ключевое отличие:
какая бы ни творилась дичь, насколько плохо не шли бы дела команды, я абсолютно
точно знаю дату и время, когда всё это закончится. Это даёт организаторам
некоторый карт-бланш, и в этом году им воспользовались по максимуму.</p>
<p>«Контур», не знаю, читает ли кто-то из вас этот отчёт, но: вы провели
действительно выдающийся ICFPC, и я снимаю перед вами шляпу. ❤️</p>
<h1 id="об-организации-соревнования">Об организации соревнования</h1>
<p>Вместо финального зачёта было восемь мини-контестов длиной от двух до
восемнадцати часов. Чтобы выиграть, нужно было хорошо выступить <em>во всех</em>.
Фактически это гарантирует, что команды выкатят в продакшен первый работающий
прототип, и в дальнейшем будут допиливать <em>его</em>, а не исследовать альтернативы.
Мне это кажется бессмысленным ограничением, я не понимаю, зачем так сделано.</p>
<p>Созданный организаторами антураж — бесподобен. Для приостановки неверия
достаточно было не читать никаких новостей, кроме блога Ивана Зайцева.</p>
<p>Техническая часть также была сделана отменно. Самое страшное, с чем
я столкнулся — это периодически отваливающиеся websocket-ы. Всё остальное
работало просто превосходно: билды за разумное время, нормально обновляющийся
лидерборд, никаких проблем с визуализатором и прочей инфраструктурой. Не
исключаю, что в конце соревнования мне только <em>казалось</em>, что билды стали чуть
медленнее.</p>
<h1 id="о-командной-работе">О командной работе</h1>
<p>Чтобы решить много загадок и проверить много идей, нужно много светлых голов
и вдвое больше рук. Пожалуй, моим основным вкладом в нынешний результат было
приглашение IngvarJackal-а и pink-snow к нам в команду: именно они написали
львиную долю того кода, что завоевал нам 38-е место.</p>
<p>Даже самым светлым головам нужна организация труда, будь то менеджер, вики
знаний, или же расписание статус-митингов. Для команды из восьми человек чат уже
не может использоваться для хранения знаний — слишком большой объём и слишком
мало структуры. Без должной координации часть потенциала участников останется
неиспользованной; так, мне кажется, случилось в этом году с mr146, unclechu
и Akon32 — любые поставленные задачи они решали быстро, да вот списка задач
у нас не было, и часть времени была потеряна впустую.</p>
<p>Когда программист переключается с «не-своего» на «свой» язык, разница
в производительности поражает. Из-за этого <a href="https://fornever.me/ru/posts/2020-07-26.icfpc-2020-report.html" title="Отчёт об ICFP Contest 2020 — F. von Never">Форневер подумывает</a>
в будущих контестах объединять языки через IPC, чтобы каждый писал на чём ему
удобно. Также напрашивается идея разделиться на мелкие «одноязыковые» команды,
но я так не хочу.</p>
<p>Самоуверенность убивает. Я пресёк две попытки превратить функцию <code>reduce</code>
в полноценную «вычислялку» «космолиспа» и, таким образом, задержал прогресс
команды на целые сутки.</p>
<p>Самоуверенность убивает, часть вторая: если сокомандник что-то пишет в чат —
прислушайся! При перечитывании лога мне в глаза постоянно бросались «голоса
разума»: «пора делать это», «нужно сконцентрироваться на том». В пылу контеста
эти сообщения легко воспринять просто как очередной TODO, коих и так десяток на
человека, но с этим импульсом необходимо бороться.</p>
<p>Когда мы играли впятером, разного рода «вспомогательные программы»
и «прототипы», не находя поддержки команды, отмирали сами собой. В этом же году
у нас оказалось достаточно рук, чтобы выполнить одну и ту же работу <em>трижды</em>, на
трёх разных языках. Результат неоднозначен: с одной стороны, только благодаря
такой свободе мы попали в лидерборд; с другой, обилие несовместимых реализаций
демотивирует команду и создаёт трения. Я хочу продолжать играть с этими людьми,
поэтому в будущих контестах буду сопротивляться повторению этой ошибки даже
в ущерб производительности.</p>
<p>Приглашаешь людей — подумай, как будешь координировать их работу. Взялся
координировать — считай это своей основной деятельностью, а не «так-то
я программист, просто пару раз в день ещё и переклички делаю».</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/do-or-do-not.jpg" width="980px" height="490px" loading="lazy" class="bleed" alt="Do or do not. There is no try. — Yoda («Звёздные войны»)" /></p>
</div>
<h1 id="о-python">О Python</h1>
<p>Что произошло: IngvarJackal написал прототип на Python; к началу оценивания
у нас не было работающего бота на Haskell; прототип на Python ушёл в продакшен;
система оценивания заставила меня допиливать то, что <em>уже</em> работает, а не то,
что я хочу. Отсюда ирония и картинки с Гарольдом.</p>
<p>Сам я надеюсь больше никогда не повторять этот опыт, но хотел бы записать одну
мысль для команд, использующих Python в качестве основного языка на ICFPC.
Вероятно, мысль можно расширить на любой интерпретируемый язык.</p>
<p>В этом году задача подразумевала отправку кода на сервер организаторов, где он
проходил тестирование и отправлялся в бой с сабмишенами других команд. Это очень
важный момент: он напрямую влияет на длительность цикла отладки. Опечатки
и ошибки типов, не пойманные локально, будут стоить вам около пяти минут
<em>каждая</em>: примерно столько времени в этом году проходило между моим пушем
и появлением на сайте лога с тестовым прогоном кода.</p>
<p>Если бы мы решали какую-то оффлайновую задачу (например, <a href="https://blog.debiania.in.ua/posts/2016-08-08-icfpc-2016.html" title="ICFPC 2016 — Debiania">про
оригами</a>), расклад был бы совершенно другим: опечатки можно было бы
поймать за считанные секунды, и Python (по этому критерию) оказался бы наравне
с компилируемыми языками. Но <em>в этом году</em> расклад был неудачным, и мы потеряли
много времени и нервов. Да что там: оба кандидата на финальный сабмишен были
забракованы именно из-за банальной ошибки с именами.</p>
<p>Урок (как мне кажется) в том, что Python нужно либо плотненько обкладывать
тестами, либо же ограничивать его применение вещами, которые можно полностью
проверить локально. Вместо тестов могут сгодиться type hints, mypy и ещё
какие-нибудь чекеры и линтеры, но в этом я уверен чуть меньше.</p>
<hr />
<p>На этом всё. Наш код можно найти <a href="https://github.com/codingteam/icfpc-2020" title="codingteam/icfpc-2020 — GitHub">на GitHub</a>. Команда в этом году была
настолько большой, что за всеми не уследишь, так что в дополнение к моему отчёту
поглядите ещё и на <a href="https://fornever.me/ru/posts/2020-07-26.icfpc-2020-report.html" title="Отчёт об ICFP Contest 2020 — F. von Never">мысли Форневера</a>. Надеюсь, позже в README
репозитория появятся ссылки и на другие отчёты.</p>
<p>Надеюсь увидеть тебя, дорогой читатель, и твою команду в лидерборде ICFPC 2021
;)</p>
<section id="footnotes" class="footnotes footnotes-end-of-document" role="doc-endnotes">
<hr />
<ol>
<li id="fn1"><p>Да, их кто-то читает. Да, <a href="https://blog.debiania.in.ua/tags/EULA.html">я зануда</a>.<a href="#fnref1" class="footnote-back" role="doc-backlink">↩︎</a></p></li>
</ol>
</section>]]></description>
    <pubDate>Sun, 26 Jul 2020 00:00:00 UT</pubDate>
    <guid>https://blog.debiania.in.ua/posts/2020-07-26-icfpc-2020-part-6.html</guid>
    <dc:creator>Александр Батищев</dc:creator>
</item>
<item>
    <title>ICFPC 2020: день четвёртый</title>
    <link>https://blog.debiania.in.ua/posts/2020-07-25-icfpc-2020-part-5.html</link>
    <description><![CDATA[<p>Это пятая часть моей серии отчётов об ICFPC 2020. Остальные части ищите здесь:</p>
<ol type="1">
<li><a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-1.html" title="ICFPC 2020: предисловие — Debiania">предисловие</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-2.html" title="ICFPC 2020: день первый — Debiania">день первый</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-23-icfpc-2020-part-3.html" title="ICFPC 2020: день второй — Debiania">день второй</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-24-icfpc-2020-part-4.html" title="ICFPC 2020: день третий — Debiania">день третий</a>;</li>
<li>день четвёртый (это то, что вы сейчас читаете);</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-26-icfpc-2020-part-6.html" title="ICFPC 2020: итоги и выводы — Debiania">итоги и выводы</a>.</li>
</ol>
<hr />
<h1 id="е-июля-понедельник-z-1530.-гоняемся-за-багами-в-модуляторе">20-е июля, понедельник, Z-15:30. Гоняемся за багами в модуляторе</h1>
<p>Итак, большинство команды разошлось спать, и на боевом посту остались только мы
с IngvarJackal. Он всё ещё возился с орбитами, а я принялся выяснять, почему мы
неправильно модулируем команду выстрела.</p>
<p>Весь этот код был покрыт какими-никакими, но тестами: закодировали всё, что
упоминалось в сообщениях инопланетян, дополнив проверками <span class="math inline">\(mod(dem(x)) \equiv x\)</span>
и <span class="math inline">\(dem(mod(x)) \equiv x\)</span> для нескольких известных нам <span class="math inline">\(x\)</span>. На самом деле,
модуляция списков — дело несложное. Там всего два правила:</p>
<pre><code>ap cons x y  ==  11 (mod x) (mod y)
nil          ==  00</code></pre>
<p>К примеру (числа не модулированы для читабельности):</p>
<pre><code>[]  ==  00
[42]  ==  11 42 00
[42, 13]  ==  11 42 11 13 00
(42, 13)  ==  11 42 13</code></pre>
<p>Именно с последними двумя примерами у нас и возникли проблемы. Дело в том, что
Python-код вместо честного <code>cons</code> использовал встроенные списки. Следовательно,
он не делал различий между <em>списком</em> двух элементов и <em>парой</em> двух элементов —
а ведь они должны модулироваться по-разному! Этот баг частично маскировался
демодулятором, который «срезал углы», игнорируя <code>nil</code>-ы.</p>
<p>Соответственно, после замены некоторых двухэлементных списков на кортежи
и допиливания модулятора у меня сломалась часть тестов, а после их починки мне
пришлось ещё и откатывать последний свой эксперимент с аргументами команды
выстрела. Но спустя два с половиной часа разборок, в Z-13:03, мне наконец
удаётся шмальнуть по противнику лазером.</p>
<p>Теперь у Codingteam есть пушки!</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2020-cannon.jpg" width="640px" height="480px" loading="lazy" alt="Герой мультфильма «Остров сокровищ», использующий пушку как пулемёт" /></p>
</div>
<p>Стратегия пока что простая: на каждом ходу наш корабль стреляет в первого
попавшегося противника. Всё. Этот подход будет держать нас в районе 25-го места
лидерборда в течение всего следующего раунда.</p>
<p>Тем временем в чате появился pink-snow и, вдохновившись нашей с IngvarJackal-ом
активностью, тоже включился в работу над ботом: IngvarJackal поручил ему завести
корабль в угол защищаемой области и висеть там. unclechu смотрел на визуализации
боёв и пытался вникнуть в механики игры. У меня было далеко за полночь, до конца
очередного этапа оценивания оставалось всего полчаса, и я встал перед выбором:
лечь спать и проснуться только к концу контеста, либо попытаться дотянуть до
появления «восточных» сокомандников и помочь им поскорее влиться в разработку
бота. Второй вариант показался мне более разумным вложением усилий, и я принялся
выделять стрельбу в отдельный модуль, чтобы проще было экспериментировать.</p>
<h1 id="z-1300.-только-бы-нам-ночь-простоять-да-день-продержаться">Z-13:00. Только бы нам ночь простоять да день продержаться</h1>
<p>Пока я возился, IngvarJackal успел выставить «стреляющего» бота против
соперников, дать ему сыграть пару десятков игр, и откатиться обратно
к не-стреляющему боту: похоже, из-за выстрелов корабль сильно перегревался, что
увеличивало расход топлива и приводило к падениям на планету. Закончив
с рефакторингом, я принялся смотреть чужие игры, чтобы понять, что происходит
с температурой и топливом. А тем временем…</p>
<blockquote>
<p>&lt;xxx&gt; конец стейджа</br>
&lt;xxx&gt; мы на нулевом месте</br>
&lt;xxx&gt; выше первого!</p>
</blockquote>
<p>Мы ничего не знали про начальные параметры кораблей, были только догадки: первый
параметр — количество топлива, второй — количество пушек, третий неизвестно,
четвёртый — тоже неизвестно, но обязательно больше нуля. Я принялся выяснять,
сколько пушек можно взять при 150-и единицах топлива. Для этого я делал
сабмишен, в котором просил, например, 64 пушки. Ждал 5 минут, пока это сбилдится
и пройдёт тесты. Когда тесты падали, я уменьшал количество пушек вдвое и снова
отправлял сабмишен. Спустя пять минут опять корректировал количество, и так
далее… Понадобилось полчаса, чтобы выяснить: при 150-и единицах топлива можно
взять 44 пушки.</p>
<p>Следующей задачей было выяснить, как пушки вообще влияют на бой. Я принялся
смотреть все игры подряд и скоро сформировал теорию:</p>
<ol type="1">
<li><p>пушки греют корпус, причём и у стреляющего (обязательно), и у цели (только
при попадании с небольшой дистанции и, похоже, ещё каких-то условиях);</p></li>
<li><p>как только корпус достигает критической температуры (которая в параметрах
корабля значилась как <code>x6</code>), топливо начинает буквально испаряться.</p></li>
</ol>
<p>Из этого следовала простая стратегия: палить по противнику, пока сам не начнёшь
перегреваться, надеясь, что перегреешь корабль соперника и он, потеряв топливо,
рухнет на планету. Видимо, сонливость берёт своё, потому что вместо простенькой
защиты от перегрева я берусь за более сложную задачу: стрелять, только когда мы
невдалеке от противника.</p>
<p>А эти самые противники тем временем <a href="https://icfpcontest2020.github.io/#/visualize?game=dd61d878-c3b5-4e74-8f88-38d8fda7813e">научились</a> создавать целый
флот и подрывать корабли, нанося нам дополнительный урон. Я тоже так хочу!</p>
<p>Но вместо флота приходится заняться подкручиванием стратегии с пушками: если
палить во все 44 дула, корабль мгновенно раскаляется, теряет топливо и плюхается
на планету — и всё это за какой-то десяток ходов. Ограничив мощность выстрела до
шестнадцати, я снова занялся алгоритмом выбора ближайшего противника.</p>
<h1 id="z-0900.-конец-очередного-этапа-оценивания">Z-09:00. Конец очередного этапа оценивания</h1>
<p>Под конец раунда у нас начинается полный бардак: у меня вроде как работает
ограничение радиуса, а у IngvarJackal-а работают новые орбиты, но при этом его
версия не стреляет (чтобы не раскалять корабль), а моя не содержит последних
наработок по орбитам. Сабмитим мою, но в целом это, конечно, фейл.</p>
<p>Оставив мне коротенький список TODO, IngvarJackal уходит вздремнуть,
а я добавляю простенькую защиту от перегрева и берусь выяснять, как создавать
больше кораблей. pink-snow тем временем научился загонять корабль в угол карты
и удерживать его там; IngvarJackal надеялся использовать эту стратегию для
корабля-защитника, чтобы соперникам сложнее было нас доставать.</p>
<p>Тут просыпаются ForNeVeR и portnov, и я быстренько пересказываю им всё то, что
успел узнать про игровые механики. ForNeVeR решительно отказывается лезть
в Python. Вместо этого он берётся доводить наш Haskell-код до состояния,
пригодного к сабмиту; в этом ему помогают unclechu и portnov. Настроение
подавленное: суббота была потрачена впустую, прогресс за воскресенье
неутешительный, и теперь, похоже, придётся делать финальный сабмишен на Python.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2020-oh-irony-2.jpg" width="1215px" height="394px" loading="lazy" class="bleed" alt="Гарольд, скрывающий боль, опять за нас рад" /></p>
</div>
<p>Тем временем моя работа над дополнительными кораблями даёт <a href="https://icfpcontest2020.github.io/#/visualize?game=34f34bdc-eb5d-43f2-8410-c536a9f6eb82">первые
плоды</a>: мне удаётся «отпочковать» четыре новых, но они тут же
падают, т.к. у них нет ни топлива, ни времени на коррекцию орбиты. Следующая
задача: «отпочковывать» корабли только после выхода на стабильную орбиту. Модуль
IngvarJackal-а содержит все необходимые вычисления, нужно только немного
порефакторить (famous last words).</p>
<p>До окончания контеста остаётся всего пять часов, поэтому всем, кто подаёт голос
в чате, я тут же выдаю задание: форкнуть мою ветку и экспериментировать с
«роями» кораблей, пытаясь понять, как заставить их стрелять — пока что все мои
попытки выдать «отпочковавшимся» кораблям оружие заканчивались провалом.
IngvarJackal, проснувшись, тоже берётся за эту задачу. Не падать мы научились,
теперь нужно научиться топить противника в море огня!</p>
<p>Просыпается Akon32 и берётся выяснять, как с помощью самоподрыва наносить
противникам максимальный урон. Кажется, он даже успеет что-то написать, но мы
это так и не смержим ☹</p>
<p>Незаметно подкрадывается Z-4:00, то есть начало предпоследнего раунда
оценивания.</p>
<p>После двух (!) часов попыток отрефакторить код IngvarJackal-а я ною ему в чатик
и он предлагает сонному мне абсолютно контр-интуитивное решение: скопировать его
модуль, выбросить всё ненужное и дописать что надо. Пока я этим занят, он
выкатывает альтернативную стратегию: его бот «отпочковывает» новые корабли после
десятого хода, к которому основной корабль обычно уже успевает выйти на
стабильную орбиту. Это очень элегантное и действенное решение проблемы, поэтому
я бросаю рефакторинг и берусь экспериментировать с оружием «отпочковавшихся»
кораблей.</p>
<p>К Z-2:00 у нас не готово ничего нового, и мы сабмитим тот же код, что
и в Z-9:00. Вся команда в мыле, все пытаются заставить «отпочковавшиеся» корабли
хотя бы разок пальнуть по противнику. Лидерборд замерзает, <a href="https://icfpcontest2020.github.io/#/scoreboard#full" title="ICFP Programming Contest 2020 — Scoreboard — Full Round">мы на 37-м
месте</a>. Возможности сравнить своего бота с чужими больше
нет; дальше придётся нащупывать прогресс вслепую.</p>
<p>В Z-00:26 выясняется, что в моей ветке баг: в перечне кораблей нет
«отпочковавшихся». Не удивительно, что они не стреляют! В срочном порядке делаю
cherry-pick исправления от IngvarJackal и пушу. Секунду спустя IngvarJackal
пишет, что переименовал одну из переменных класса с состоянием игры. Аргх!
Снова <code>git cherry-pick</code>, опять <code>git push</code>, скрещиваю пальцы.</p>
<p>Не дожидаясь результатов, в Z-00:11 делаю очередной сабмишен, в котором
«отпочковавшиеся» корабли палят по ближайшему противнику. IngvarJackal мержит
мои изменения и сабмитит свою собственную реализацию «роя кораблей». Впрочем,
его «осы» не умеют «жалить», так что финальным сабмишеном будет либо тот, что
я сделал только что, либо тот, что мы отправляли в Z-9:00. До конца соревнования
остаются считанные минуты, поэтому я в спешном порядке запускаю бои между всеми
парами сабмишенов: нашим старым и двумя новыми.</p>
<p>Наступает час Z, мы делаем F5, и сайт организаторов генерирует уже знакомый нам
мемасик (выделение моё):</p>
<blockquote>
<p>ICFP Programming Contest 2020 has started 3 days ago, <strong>will end a few seconds ago</strong></p>
</blockquote>
<p>А дальше я просто цитирую чат:</p>
<blockquote>
<p>[Z+00:02] &lt;xxx&gt; капец<br />
[Z+00:02] &lt;xxx&gt; я в одном месте distance не поменял<br />
[Z+00:02] &lt;xxx&gt; то есть все последующие сабмиты тоже багованные</p>
</blockquote>
<p>Из-за банальной опечатки последние 9 часов работы улетают коту под хвост.</p>
<hr />
<p>На этом я заканчиваю своё повествование о трёх безумных днях, проведённых нами
за разгадыванием инопланетных загадок, прохождением мини-квестов и написанием
систем управления боевым космическим кораблём. Завтра я опубликую
<a href="https://blog.debiania.in.ua/posts/2020-07-26-icfpc-2020-part-6.html" title="ICFPC 2020: итоги и выводы — Debiania">заключительный пост серии</a>, в котором подытожу своё
отношение к задаче и попытаюсь извлечь уроки из сделанных нами ошибок. А вы пока
что посмотрите видео, закрывшее контест — оно просто-таки берёт за душу:</p>
<div class="center">
<p>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/Me-0k5XGZWY" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>
</iframe>
</p>
</div>]]></description>
    <pubDate>Sat, 25 Jul 2020 00:00:00 UT</pubDate>
    <guid>https://blog.debiania.in.ua/posts/2020-07-25-icfpc-2020-part-5.html</guid>
    <dc:creator>Александр Батищев</dc:creator>
</item>
<item>
    <title>ICFPC 2020: день третий</title>
    <link>https://blog.debiania.in.ua/posts/2020-07-24-icfpc-2020-part-4.html</link>
    <description><![CDATA[<p>Это четвёртая часть моей серии отчётов об ICFPC 2020. Остальные части ищите
здесь:</p>
<ol type="1">
<li><a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-1.html" title="ICFPC 2020: предисловие — Debiania">предисловие</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-2.html" title="ICFPC 2020: день первый — Debiania">день первый</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-23-icfpc-2020-part-3.html" title="ICFPC 2020: день второй — Debiania">день второй</a>;</li>
<li>день третий (это то, что вы сейчас читаете);</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-25-icfpc-2020-part-5.html" title="ICFPC 2020: день четвёртый — Debiania">день четвёртый</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-26-icfpc-2020-part-6.html" title="ICFPC 2020: итоги и выводы — Debiania">итоги и выводы</a>.</li>
</ol>
<hr />
<h1 id="е-июля-воскресенье-z-3017.-обратно-в-бой">19-е июля, воскресенье, Z-30:17. Обратно в бой!</h1>
<p>Перечитывая с утра командный чатик, обнаруживаю, что pink-snow написал <em>ещё
одну</em> «вычислялку», в этот раз на Haskell. Она полностью следует <a href="https://message-from-space.readthedocs.io/en/latest/implementation.html" title="Galaxy Evaluator in Pseudocode">псевдокоду
организаторов</a> и, что самое важное, работает. Теперь portnov
и pink-snow выделяют её в отдельное приложение, а ForNeVeR модифицирует свой
проект на C#, чтобы GUI при каждом клике вызывал Haskell и рисовал результат.</p>
<p>unclechu тем временем закончил работу над демодулятором и ушёл спать. Соперники
заваливают Дискорд картинками из игры, и страсти накаляются: нам уже
совсем-совсем пора добраться до туториалов, которые многие команды прошли ещё
вчера.</p>
<p>В Z-30:17 ForNeVeR наконец получает первую картинку:</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2020-first-image.png" width="285px" height="232px" loading="lazy" alt="Первый результат `galaxy`: красная иконка галактики в центре, число
    1 в левом верхнем углу" /></p>
</div>
<p>Пообедав, он выкатывает новую версию GUI с масштабированием, и я отправляюсь
исследовать Галактику — в смысле, galaxy.txt. После нескольких кликов по иконке
галактики идёт десяток экранов, где нужно кликать в пересечение горизонтальной
и вертикальной линий — некая калибровка. А потом отображается та самая картинка,
которую недавно показали нам организаторы:</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2020-our-galaxy.png" width="1048px" height="883px" loading="lazy" class="bleed" alt="Галактика с обозначениями рас" /></p>
</div>
<p>У некоторых рас есть «игры», и я погружаюсь в угадывание правил одной из них;
portnov и Akon32, похоже, увлечены тем же самым. Один из только что проснувшихся
сокомандников недоумевает:</p>
<blockquote>
<p>&lt;xxx&gt; теперь у нас приложение на шарпе, а не на х-ле или питоне?<br />
&lt;yyy&gt; Шарповый фронтенд с хаскельным бэкендом<br />
&lt;zzz&gt; Да, планируем каждые восемь часов переписывать.</p>
</blockquote>
<p>ForNeVeR и mr146 заняты улучшением GUI, а pink-snow помогает им, впиливая
в «вычислялку» поддерживающие фичи.</p>
<p>Я тем временем делаю первые успехи в игре, где сверху показано инопланетное
число, а внизу нужно собрать некий шаблон на поле 3×3 пикселя. При нажатии на
любую клетку шаблона её цвет меняется с зелёного на оранжевый, при этом
какая-нибудь другая клетка шаблона становится чёрной. При удачно подобранном
шаблоне число вверху поля уменьшается, и поле снова заполняется зелёным.</p>
<p>Решения первых уровней похожи на разные повороты и отражения «глайдера» из игры
«Жизнь» Конвея. Следующие уровни решаются каким-то странным шаблоном, уже не
похожим на «Жизнь». Чатик тем временем подгоняет:</p>
<blockquote>
<p>&lt;xxx&gt; первые четыре уровня прошли, осталось ещё 8 или 9<br />
&lt;yyy&gt; из туториала в игре? &lt;_&lt;<br />
&lt;zzz&gt; нет :(<br />
&lt;zzz&gt; В игру мы пока не залогинились<br />
&lt;xxx&gt; да, тут капча<br />
&lt;xxx&gt; инопланетная<br />
&lt;yyy&gt; её нужно прокликать<br />
&lt;xxx&gt; yyy: я так и делаю!</p>
</blockquote>
<p>Из-за каких-то изменений в GUI и «вычислялке» появляется забавный баг: если
дважды быстро кликнуть в GUI, то второй клик не будет проигнорирован. Вместо
этого он будет применён после первого и, возможно, как-то повлияет на состояние
игры. Мне флегматично <a href="https://www.anekdot.ru/id/75546/" title="Анекдот №75546">советуют не кликать так быстро</a>. Люблю нашу
команду, такие отзывчивые люди, ух! ^_^</p>
<p>Тут mr146 подвозит в GUI возможность сохранять, загружать и править состояние
вселенной, и играть становится совсем удобно: больше не нужно «прокликивать»
экраны калибровки, а в случае ошибки в игре можно откатиться на предыдущий ход.</p>
<h1 id="z-2400.-мы-снова-вспоминаем-про-ingvarjackal-а">Z-24:00. Мы снова вспоминаем про IngvarJackal-а</h1>
<p>Так и не решив девятый уровень, я ухожу обедать. Вернувшись, обнаруживаю, что
организаторы опубликовали описание команд, которые нужно слать по HTTP для
управления космическим кораблём. Тут я снова вспоминаю про IngvarJackal-а,
который последние сутки в одиночку удерживает нас в районе 20-го места
лидерборда.</p>
<p>Лидерборд в этом году формировался необычно: сутки, оставшиеся до конца
соревнования, были разделены на несколько этапов. Очки за каждый этап
суммировались и формировали итоговый рейтинг команды. Таким образом, нам нужно
было <em>уже сейчас</em> зарабатывать баллы; выкатить решение в последний момент
и попасть в середину рейтинга было бы практически невозможно. Поэтому я бросаю
решать загадки galaxy.txt и присоединяюсь к IngvarJackal-у.</p>
<p>Игра на орбите происходит следующим образом: два корабля оказываются рядом
с планетой. Один из них нападает, второй защищается. Игра длится 256 ходов
(позже лимит подняли до 384-х). Задача атакующего — сбить защитника. Защитник,
в свою очередь, просто пытается пережить нападение.</p>
<p>Сразу же после старта игры корабли начинают падать на планету, поэтому
IngvarJackal первым делом научился поддерживать круговую орбиту. Это требовало
постоянных затрат топлива, но, по крайней мере, давало нашему кораблю пожить
хотя бы сотню ходов. Теперь IngvarJackal ставил эксперименты, пытаясь выяснить,
как именно в этом мире устроена гравитация.</p>
<p>Я же взялся за пушки. Все корабли оборудованы лазерами, которые мгновенно
поражают цели на любом расстоянии. Нужно заметить, что организаторы описали
команду для выстрелов, но не объяснили, как именно выстрелы действуют на
противника. По всей видимости, узнать это можно было только из туториалов, до
которых мы всё никак не могли добраться.</p>
<p>Команда выстрела требует трёх параметров: идентификатора стреляющего корабля,
координат цели, и ещё какого-то неизвестного параметра. В визуализаторе,
предоставленном организаторами, было видно, что после координат идут три числа,
а не один параметр.</p>
<p>Я принялся за эксперименты, и чего только не попробовал:</p>
<ul>
<li><p>стрелять не по прогнозируемым координатам противника, а и в ближайшие точки —
на случай, если команда выстрела принимается только при попадании. Это не
подтвердилось: по логу моего бота и по визуализации боя я видел, что
сгенерировал ровно те координаты, в которых оказался противник, но выстрела
не происходило;</p></li>
<li><p>поменять начальные параметры корабля (для которых не было документации, но мы
догадывались, что первый параметр из четырёх — это количество топлива);</p></li>
<li><p>не стрелять на первом ходу, потому что это постоянно заканчивалось ошибкой;</p></li>
<li><p>передавать в последнем параметре не одно число, а список из трёх (потому что
именно столько показывал визуализатор от организаторов);</p></li>
<li><p>вместо последнего параметра передавать три числа, которые я видел
в визуализаторе организаторов.</p></li>
</ul>
<p>Всё эти эксперименты закончились одинаково — ошибкой.</p>
<p>У остальных членов команды дела тоже шли с переменным успехом:</p>
<ul>
<li>pink-snow добавил какую-то обвязку для HTTP API;</li>
<li>ForNeVeR, замучившись отлаживать (де)модулятор, посоветовал портировать тесты
из Python на Haskell и отправился спать;</li>
<li>portnov добавил в «вычислялку» <code>send</code> и даже добрался до какого-то из туториалов;</li>
<li>unclechu, добавив каркас для сабмишенов на Haskell, тоже уснул;</li>
<li>у mr146 возник какой-то форс-мажор, и он отвалился до конца соревнования.</li>
</ul>
<p>Спустя почти шесть часов возни с командой выстрела мы с IngvarJackal наконец
решили проверить алгоритм модуляции — и обнаружили, что команды кодируются
неправильно! Мы должны передавать список команд, где каждая команда кодируется
отдельным списком, но получается какая-то ерунда:</p>
<div class="sourceCode" id="cb1"><pre class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="co">-- так должно быть</span></span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a>[<span class="dv">4</span>, <span class="dv">14</span>, [[<span class="dv">0</span>, <span class="dv">1</span>, [<span class="op">-</span><span class="dv">1</span>, <span class="dv">0</span>]], [<span class="dv">2</span>, <span class="dv">1</span>, [<span class="op">-</span><span class="dv">16</span>, <span class="op">-</span><span class="dv">48</span>], <span class="dv">4</span>]]]</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="co">-- так кодируем</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>[<span class="dv">4</span>, <span class="dv">14</span>, [[<span class="dv">0</span>, <span class="dv">1</span>, [<span class="op">-</span><span class="dv">1</span>, <span class="dv">0</span>]],  <span class="dv">2</span>, <span class="dv">1</span>, [<span class="op">-</span><span class="dv">16</span>, <span class="op">-</span><span class="dv">48</span>], <span class="dv">4</span>] ]</span></code></pre></div>
<p>То есть второй элемент списка не добавляется <em>в</em> него, а <em>конкатенируется</em>
с ним. При этом демодуляция неправильной кодировки возвращает правильный,
исходный список! IngvarJackal писал демодулятор, но ничего не может подсказать
по модулятору. portnov, писавший модулятор, делал это по коду mr146. В общем,
бардак. portnov уходит спать, а я закатываю рукава и погружаюсь в науку
о преобразовании «космолиспа» в нолики и единички…</p>
<p>Какой баг я найду? Успеем ли мы хоть чего-то достичь за последние 16 с лишним
часов? Читайте в <a href="https://blog.debiania.in.ua/posts/2020-07-25-icfpc-2020-part-5.html" title="ICFPC 2020: день четвёртый — Debiania">отчёте о последнем дне соревнования</a> ;)</p>]]></description>
    <pubDate>Fri, 24 Jul 2020 00:00:00 UT</pubDate>
    <guid>https://blog.debiania.in.ua/posts/2020-07-24-icfpc-2020-part-4.html</guid>
    <dc:creator>Александр Батищев</dc:creator>
</item>
<item>
    <title>ICFPC 2020: день второй</title>
    <link>https://blog.debiania.in.ua/posts/2020-07-23-icfpc-2020-part-3.html</link>
    <description><![CDATA[<p>Это третья часть моей серии отчётов об ICFPC 2020. Остальные части ищите здесь:</p>
<ol type="1">
<li><a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-1.html" title="ICFPC 2020: введение — Debiania">предисловие</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-2.html" title="ICFPC 2020: день первый — Debiania">день первый</a>;</li>
<li>день второй (это то, что вы сейчас читаете);</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-24-icfpc-2020-part-4.html" title="ICFPC 2020: день третий — Debiania">день третий</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-25-icfpc-2020-part-5.html" title="ICFPC 2020: день четвёртый — Debiania">день четвёртый</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-26-icfpc-2020-part-6.html" title="ICFPC 2020: итоги и выводы — Debiania">итоги и выводы</a>.</li>
</ol>
<hr />
<h1 id="е-июля-суббота-t1900.-я-возвращаюсь-в-строй">18-е июля, суббота, T+19:00. Я возвращаюсь в строй</h1>
<p>Просыпаюсь, готовлю завтрак и с тарелкой в руках сажусь читать командный чатик.
ForNeVeR и portnov, живущие восточнее и потому проснувшиеся ещё четыре часа
назад, вместе с pink-snow зафлудили весь лог обсуждением какой-то «вычислялки» для
«космолиспа». Решительно непонятно, в каком она состоянии: вроде как у pink-snow
всё есть, но при этом и Форневер, и Портнов пилят то же самое… Не выдержав,
устраиваю статус-митинг, который наконец вносит ясность:</p>
<ul>
<li>pink-snow написал на Python <a href="https://github.com/codingteam/icfpc-2020/blob/3a6f4d946f22a41f122ac5565feb590f03743720/pyvaluator/pyvaluator.py" title="icfpc-2020/pyvaluator.py at 3a6f4d946f22a41f122ac5565feb590f03743720 · codingteam/icfpc-2020">«вычислялку» для galaxy.txt</a>:
программу, берующую на вход целый исходник на «космолиспе» и «выполняющий»
её, аж пока не получит результат. В «вычислялке» нет мемоизации, так что
работает она весьма неспешно. Никто не может понять код, и у всех чешутся
руки переписать это на Haskell;</li>
<li>собственно, ForNeVeR занят первым шагом по переписыванию: парсером из текста
во входной тип <code>reduce</code>;</li>
<li>mr146 экспериментирует на C# с модуляцией-демодуляцией, то есть
преобразованием «космолиспа» в последовательность ноликов-единичек
и обратно. Это нужно для того, чтобы реализовать <code>send</code> и пообщаться,
наконец, с инопланетянами;</li>
<li>portnov сделал два неудачных подхода к написанию «вычислялки» по аналогии
с кодом на Python и теперь пребывает в некой прострации;</li>
<li>unclechu дописал биндинги для HTTP и спит;</li>
<li>остальные тоже спят.</li>
</ul>
<p>Тем временем некоторые соперники получают аж 10 очков, пройдя некий «первый
туториал», а организаторы выкладывают <a href="https://icfpcontest2020.github.io/#/post/2054" title="How to Run the Galaxy (Video Course)">видеоуроки</a> по разбору
и вычислению «космолиспа». Первый урок, про разбор, к этому моменту мы освоили
сами. Второй, про вычисление только левого поддерева, приходится впору — я вчера
как раз подбирался к этой идее, а тут мне сразу дали итоговый ответ. А вот
третий урок, про шаринг поддеревьев, я проигнорировал, потому что он показался
мне скорее оптимизацией, чем строгой необходимостью.</p>
<p>Тут я делаю ошибку: вместо того, чтобы с чистой головой сесть и закодить ровно
то, что описали организаторы, я всё ещё цепляюсь за уже написанный <code>reduce</code>.
Уверен, начни я с «нуля» — у нас к обеду субботы была бы готовая «вычислялка». Но
увы.</p>
<p>Как любит повторять Максим Дорофеев, между срочным и важным человек всегда
выбирает… понятное. Так что я решил заняться починкой сабмишенов. Как я уже
упоминал <a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-2.html#%D1%84%D0%BB%D0%B5%D1%88%D0%B1%D1%8D%D0%BA-%D1%81%D0%B1%D0%BE%D1%80-%D0%BA%D0%BE%D0%BC%D0%B0%D0%BD%D0%B4%D1%8B" title="ICFPC 2020: день первый — Debiania">во флешбеке про сбор команды</a>, я никого не
ввёл в курс дела касательно отправки результатов — а именно, о том, что весь наш
код собирается в Docker-контейнере без доступа к Интернету. unclechu, не знавший
об этом, добавил к нам в проект Servant и, таким образом, сломал сабмишены. Не
знал он и о том, что сабмишены должны при запуске проделывать определённые
действия. На устранение всех этих оплошностей у меня ушло два часа. Организаторы
всё не мержили мой пулл-реквест с обновлённым Dockerfile, так что я перешёл
к следующей задаче: помогать ForNeVeR-у с «вычислялкой».</p>
<p>Тут я снова проделал то же самое, что и вчера с pink-snow: убедил ForNeVeR-а,
что «вычислялка» должна быть циклом, опирающимся на <code>reduce</code> для упрощения
выражений. Я по-прежнему сопротивлялся всяким попыткам добавить подстановку тел
функций в саму <code>reduce</code>. Саботировав таким образом написание рабочей
«вычислялки» (во второй раз!), я ушёл обедать, а ForNeVeR продолжил бороться
с зависаниями на каком-то тривиальнейшем кусочке «космолиспа».</p>
<p>IngvarJackal тем временем занимался демодулятором на Python. Это была, кажется,
уже вторая реализация этого алгоритма. Впрочем, мы не видели в этом проблемы:
нам казалось, что в условиях повышенной неопределённости будет даже лучше, если
несколько под-команд будут одновременно исследовать задачу на удобных им языках.
Я даже предположить не мог, насколько хорошо отобьётся эта наша ставка.</p>
<p>Всё это время я каждые полчаса делал тестовый сабмишен, чтобы знать, когда
организаторы деплойнут новый образ с Servant. Сабмишены фейлились. Прошло три
часа, а они всё фейлятся. И тут…</p>
<h1 id="t2400-z-4800.-lightning-division-заканчивается">T+24:00, Z-48:00. Lightning division заканчивается</h1>
<p>Да-да, мы снова не успели ничего сделать в первые 24 часа. И в этот раз мне даже
не стыдно, потому что задача действительно дикая.</p>
<p>Как раз в это время проснулся unclechu, и мы с ним принялись выяснять, что же не
так с сабмишенами. Оказалось, что деплой уже давно выполнен, и я, по всей
видимости, не добавил в образ какие-то библиотеки: Servant-то там есть, а вот
десятка невесть откуда взявшихся вспомогательных пакетов — нет. Провозившись
с этим ещё полчаса и так и не поняв, как <code>stack</code> смог поставить Servant, но не
поставить его зависимостей, я предложил вернуться на http-conduit; благо, нам
нужны были только два эндпоинта. unclechu, честь ему и хвала, воспринял это
предложение стоически и взялся переделывать.</p>
<p>ForNeVeR тем временем вёл неравный бой с «вычислялкой». Тут надо пояснить, что
<code>reduce</code> пытается <em>максимально</em> упростить дерево, применяя к нему правила аж
пока дерево не перестанет меняться. Именно поэтому я противился добавлению туда
подстановки тел функций — это бы увело <code>reduce</code> в бесконечную рекурсию на любой
рекурсивной функции, ведь перед базой рекурсии всегда идёт какая-то проверка,
и <code>reduce</code> попыталась бы упростить обе ветки, в т.ч. ту, которая уже не
выполнится. ForNeVeR же пытался теперь написать некую «стратегию», которая
применяла бы <code>reduce</code> для упрощения выражений, а когда редуцировать уже нечего
— заменяла бы функцию в конце левого поддерева на её тело и начинала сначала.
Получавшийся комбайн упорно вис на примитивнейших примерах.</p>
<p>И тут, в Z-47:30, организаторы <a href="https://icfpcontest2020.github.io/#/post/2056" title="How to actually win the contest">наконец объясняют, как выиграть
контест</a>. Инопланетяне летят устраивать у нас на орбите звёздные
войны. Мы обязаны выиграть. Для этого командам нужно писать ботов, управляющих
космическими кораблями. Лучший бот сразится с пришельцами, прибытие которых
ожидается к концу контеста, в час Z: 20-го июля в 13:00 UTC. Ну наконец-то хоть
какая-то определённость!</p>
<p>Судя по новому документу организаторов, нам всё же нужно вычислить <code>galaxy</code>,
и там будут нетривиальные картинки:</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2020-game-galaxy.png" width="2184px" height="1841px" loading="lazy" class="bleed" alt="Изображение галактики с пометками различных рас" /></p>
</div>
<p>В связи с этим ForNeVeR передаёт работу над «вычислялкой» мне, а сам
отправляется писать на C# GUI, в котором мы могли бы исследовать рисунки
<code>galaxy</code>. Ко мне тут же присоединяется Портнов, и мы снова начинаем возиться.
Высказываются идеи добавить мемоизацию, чтобы не пересчитывать одинаковые
поддеревья. Проскакивает идея добавить в AST частично применённые функции. Мы
ходим кругами, пробуя то да сё.</p>
<p>mr146, посмотрев на наши мучения, берётся писать ещё одну (третью) версию
«вычислялки» на случай, если мы не преуспеем c Haskell: будет, хотя бы, что
прикрутить к GUI. Задач получше для него не находится, поэтому никто не
возражает.</p>
<h1 id="z-4400.-мы-вспоминаем-про-ingvarjackal-а">Z-44:00. Мы вспоминаем про IngvarJackal-а</h1>
<p>Дела с «вычислялкой» на Haskell заходят в тупик, и я решаю глянуть на
Python-код, который, вроде, работает. В процессе расспросов о том, как его
вообще запустить, обнаруживается, что на Python есть рабочие модулятор и кусочек
демодулятора, а IngvarJackal сидит без дела. У меня появляется первая правильная
идея за весь день, и я быстренько организую ветку, из которой можно слать
сабмишены на Python. IngvarJackal уходит мучить сервер организаторов тестами
в надежде выяснить что-нибудь про HTTP API, пока мы доводим до ума «вычислялки»
и GUI.</p>
<p>portnov показывает картинки из профилировщика, согласно которым дерево постоянно
растёт, а не редуцируется. Я возвращаюсь к разборкам с «вычислялкой».
Выясняется, что коллективными усилиями мы ушатали её в хлам: она виснет даже на
простейшем кусочке «космолиспа»:</p>
<pre><code>:1 = ap ap vec 2 5
:0 = ap ap cons :1 nil</code></pre>
<p>ForNeVeR, сделав часть GUI, удаляется спать, а portnov пытается добиться
какой-то ясности касательно наших дальнейших действий. Прошло уже 29 часов
с начала контеста, а у нас до сих пор один балл и нет понимания того, как
заработать ещё. Что это за туториалы, пройденные остальными командами? Как до
них добраться и как их проходить? Как это связано с HTTP API и <code>galaxy</code>?</p>
<p>Проводим статус-митинг и выясняем, что:</p>
<ul>
<li>unclechu уже выпилил Servant и добавил новые функции для двух нужных нам
эндпоинтов, а теперь планирует реализовать модулятор/демодулятор на Хаскеле;</li>
<li>IngvarJackal чинит (де)модулятор на питоне, обкладывая его тестами,
и готовится делать сабмишены для выяснения API;</li>
<li>mr146 пишет «вычислялку» на C#, но готов бросить, как только мы доведём до ума
код на Haskell;</li>
<li>portnov пытается привести в чувство хаскелевскую «вычислялку»;</li>
<li>остальные спят.</li>
</ul>
<p>Таким образом получается, что у нас, якобы-Haskell-команды, действительно
рабочий код есть только на Python, да и писали его вовсе не «старожилы»,
а «новички». Иронично!</p>
<h1 id="z-4130.-codingteam-на-первом-месте">Z-41:30. Codingteam на первом месте</h1>
<p>Akon32 обращает наше внимание на лидерборд, и мы наблюдаем невероятное:</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2020-we-are-number-one.png" width="1215px" height="394px" loading="lazy" class="bleed" alt="Codingteam на первом месте leaderboard-а" /></p>
</div>
<p>Один из тестовых сабмишенов IngvarJackal-а оказался настолько удачным, что
«обыгрывал» соперников несмотря на то, что никакой игровой логики в нём не было:
сабмишен выполнял все необходимые инициализации и бездействовал. Практически
пустой скрипт на Python выводит Haskell-команду на первое место лидерборда.</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2020-oh-irony.jpg" width="1215px" height="394px" loading="lazy" class="bleed" alt="Гарольд, скрывающий боль, рад за нас" /></p>
</div>
<h1 id="марш-смерти">Марш смерти</h1>
<p>Тем временем мы с portnov пришли к выводу, что пора объединять «вычислялку»
с <code>reduce</code>, потому что ни один из нас не может понять, как эти две функции между
собой меняют дерево. Промежуточные результаты показали небольшое улучшение:
«вычислялка» перестала виснуть на наших простых примерах, но по-прежнему
отказывалась что-либо вычислять в galaxy.txt.</p>
<p>Я стал подозревать, что дело в рекурсии, и попытался закодировать простенький
цикл вроде этого кода на Haskell:</p>
<div class="sourceCode" id="cb2"><pre class="sourceCode haskell"><code class="sourceCode haskell"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a>fn0 <span class="ot">=</span> fn1 <span class="dv">13</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>fn1 <span class="ot">=</span> \x <span class="ot">-&gt;</span> <span class="kw">if</span> x <span class="op">==</span> <span class="dv">0</span> <span class="kw">then</span> <span class="dv">42</span> <span class="kw">else</span> fn1 (x <span class="op">-</span> <span class="dv">1</span>)</span></code></pre></div>
<p>Тут со мной случилось то, что <a href="https://tonsky.livejournal.com/325818.html" title="IPFCP, ой, то есть ICFPC-2020 — tonsky.livejournal.com">Тонский назвал «принудительным ликбезом по
лябмда-исчислению»</a>: чтобы в <code>fn1</code> использовать <code>x</code> дважды (в
условии и в <code>else</code>-ветке), пришлось напрячь мозг и сообразить, как с помощью
двойного применения S-комбинатора «вывернуть» выражение, чтобы оно принимало <code>x</code>
как аргумент. Получился вот такой вот код на «космолиспе»:</p>
<pre><code>:0 = ap :1 13
:1 = ap ap s ap ap s if0 ap t 42 ap ap b :1 dec</code></pre>
<p>Он, естественно, не работал, и я потратил ещё чуток времени, добавляя
в «вычислялку» на Python поддержку <code>if0</code> и <code>dec</code>. Код же на Haskell успешно
редуцировал выражение к «42», только если в <code>:1</code> передавался ноль, единица или
двойка. Если тройка и больше — вычисление шло вразнос, S-комбинатор в левой
ветке не заменялся, и всё заканчивалось каким-то покорёженным выражением.</p>
<p>Мне понадобилось всего полчаса, чтобы сообразить, что, кодируя <code>if0</code>, я бездумно
переписал правила из сообщений инопланетян, и эта функция не была определена для
значений помимо нуля и единицы. Вот так вот всегда: тестируя руками, мечтаешь
о юнит-тестах; написав юнит-тесты, мечтаешь о свойствах на QuickCheck…</p>
<p>Тем временем организаторы смилостивились над нами и <a href="https://message-from-space.readthedocs.io/en/latest/implementation.html" title="Galaxy Evaluator in Pseudocode">выложили псевдокод
интерпретатора «космолиспа»</a>. mr146 успешно реализовал его на
С#, и мне следовало бы уже остановиться, но сонный мозг уже не способен был
принимать решения. Пообщавшись с проснувшимся unclechu, я пришёл к выводу, что
пора впиливать мемоизацию, а для этого нужно взять монаду <code>State</code> и держать
в ней <code>Map</code>, где ключом будет <code>Data.Unique.Unique</code>, соответствующий выражению,
а значение (само выражение) мы будем постепенно редуцировать. К счастью, тут
меня начал рубить сон, и я принял второе и последнее верное решение за день
— лечь спать.</p>
<p>Как поступят мои соратники, проснувшись и обнаружив, что «вычислялка» на Хаскеле
всё ещё не готова, зато есть две других? Сможем ли мы наконец понять, как
заработать баллы? Об этом читайте <a href="https://blog.debiania.in.ua/posts/2020-07-24-icfpc-2020-part-4.html" title="ICFPC 2020: день третий — Debiania">в следующем посте</a> :)</p>]]></description>
    <pubDate>Thu, 23 Jul 2020 00:00:00 UT</pubDate>
    <guid>https://blog.debiania.in.ua/posts/2020-07-23-icfpc-2020-part-3.html</guid>
    <dc:creator>Александр Батищев</dc:creator>
</item>
<item>
    <title>ICFPC 2020: день первый</title>
    <link>https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-2.html</link>
    <description><![CDATA[<p>Это вторая часть моей серии отчётов об ICFPC 2020. Остальные части ищите здесь:</p>
<ol type="1">
<li><a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-1.html" title="ICFPC 2020: введение — Debiania">предисловие</a>;</li>
<li>день первый (это то, что вы сейчас читаете);</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-23-icfpc-2020-part-3.html" title="ICFPC 2020: день второй — Debiania">день второй</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-24-icfpc-2020-part-4.html" title="ICFPC 2020: день третий — Debiania">день третий</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-25-icfpc-2020-part-5.html" title="ICFPC 2020: день четвёртый — Debiania">день четвёртый</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-26-icfpc-2020-part-6.html" title="ICFPC 2020: итоги и выводы — Debiania">итоги и выводы</a>.</li>
</ol>
<hr />
<h1 id="флешбэк-сбор-команды">Флешбэк: сбор команды</h1>
<p>Я традиционно играю с ребятами из <a href="https://codingteam.org.ru" title="Codingteam is an open community of engineers and programmers">сообщества Codingteam</a>, поэтому
для начала пингонул парочку «старожилов». Но хотелось большего. Из года в год
наша команда сталкивается с одной и той же проблемой: идей больше, чем рук.
Забыв, что в дополнение к рукам идут ещё и головы, генерирующие идеи, я задался
целью собрать для ICFPC 2020 как можно больше цодингтимовцев.</p>
<p>Мне удалось. В этом году Цодингтим представляли аж восемь человек — четыре
«старожила» и четыре «новичка»:</p>
<ol type="1">
<li>Akon32, для которого это был уже четвёртый контест;</li>
<li>ForNeVeR, тоже выступавший далеко не впервые;</li>
<li>IngvarJackal, который не знает Haskell и собирался помогать советами;</li>
<li>mr146, коллега ForNeVeR-а, имеющий богатый опыт в куче других соревнований,
но ещё не игравший в ICFPC;</li>
<li>pink-snow, тоже никогда не игравший в ICFPC, но уже здорово вложившийся
в разгадку тизеров;</li>
<li>portnov, известный вам по моим предыдущим отчётам;</li>
<li>unclechu, единственный из всей команды пишущий на Haskell за деньги, но
никогда не игравший в ICFPC;</li>
<li>я.</li>
</ol>
<p>Честно признаться, я был настолько ошарашен этим успехом, что даже попытался
уговорить ещё одного бывшего участника побыть у нас проект-менеджером: опыт
2016-го года показал, что статус-митинги существенно уменьшают неразбериху
и помогают поддерживать темп. К сожалению, он отказался. К ещё большему
сожалению, я решил, что попробую сам сыграть эту роль :) Но к этому мы ещё
вернёмся.</p>
<p>За пару дней до контеста мы все собрались в XMPP-конференции с мостом
в Телеграм и… всё. Здесь была допущена масса ошибок: мы не усердствовали
с проверкой окружений сборки; не добились коллективного понимания способа,
которым в этом году происходит отправка решений; даже не рассказали «новичкам»
о том, как мы обычно играем. То есть людей-то я пригласил, а ввести их в курс
дела мне в голову не пришло. Вот вам и проект-менеджер…</p>
<h1 id="е-июля-пятница-t0000.-старт">17-е июля, пятница, T+00:00. Старт</h1>
<p>Нажимаем F5, и сайт организаторов генерирует мемасик (выделение моё):</p>
<blockquote>
<p><strong>Starts a few seconds ago</strong> on July 17, 2020 @ 13:00 UTC</p>
</blockquote>
<p>А задания нет. Ну, ладно, нам не впервой — в былые годы сайт организаторов
вообще ложился под наплывом интересующихся. Просто жмём F5.</p>
<p>T+00:05. Радиоастроном Иван Зайцев публикует очередной пост, мол, пришла целая
пачка сообщений, помогайте расшифровывать. Оказалось, что организаторы ещё 25
минут назад <a href="https://twitter.com/icfpcontest2020/status/1284104337590030336">твитнули</a>, что отказываются от своей задачи
в пользу Пеговки. А Пеговка призывает скооперироваться в Дискорде и понять, чего
от нас хотят инопланетяне.</p>
<p>Настроение в командном чате обескураженное и подавленное:</p>
<blockquote>
<p>&lt;xxx&gt; Сорян, а где код-то писать нужно?<br />
&lt;xxx&gt; Кажется, тут просто какая-то угадайка? Или не?<br />
&lt;yyy&gt; xxx: код можно писать для того, чтобы упростить декодирование. Но, похоже, пока что контест состоит в том, чтобы апдейтить сайт на readthedocs<br />
&lt;xxx&gt; И кто выиграет в таком соцсоревновании?<br />
&lt;zzz&gt; Иван Зайцев!</p>
</blockquote>
<p>ICFPC, конечно, непредсказуем, но у команд все равно формируются некие ожидания.
Будет задача. Она будет непомерно сложной, быть может, в принципе не решаемой ни
за какое время, не говоря уж о 72-х часах. Команды будут соревноваться, и многие
должны будут проиграть, чтобы единицы могли победить.</p>
<p>Организаторы же сломали все ожидания. Вместо задачи — рекомендация разобраться,
в чём состоит задание. О сложности не известно ничего. Команды должны работать
вместе, а не соревноваться. Всё это настолько сильно шло вразрез с предыдущими
ICFPC, что я растерял весь запал, скопившийся за год ожидания.</p>
<p>Просто сидел и злился. Если бы я играл один, на этом месте я бы сдался.</p>
<p>Но со мной была команда, и никто другой не ушёл. Спустя 45 минут мне немного
полегчало, товарищи тоже сбросили с себя оковы первого впечатления, и мы взялись
за дело. mr146 погрузился в декодирование новых сообщений, записывая мысли
в гуглодок. Я взялся помогать, но нелюбовь к загадкам взяла своё,
и я переключился на другую задачку.</p>
<p>pink-snow обнаружил, что за тестовый сабмишен выдают один балл. Надо заметить,
что система сабмишенов пока что никак не вязалась с заданием: она ожидала
POST-запроса по адресу и с параметром, указанными в командной строке. В ответ
нам возвращали тот же параметр. Отказываясь верить в то, что задание ограничено
разгадыванием сообщений, я трачу время на эксперименты с API: шлю туда 42, потом
случайные числа, потом <em>большие</em> случайные числа. В ответ всегда приходит то,
что было отправлено.</p>
<blockquote>
<p>&lt;я&gt; xxx: рандомное чиселко уже 6 минут тестируется<br />
&lt;xxx&gt; минору сломал пришельцев случайным числом<br />
&lt;xxx&gt; межпланетарный дипломатический конфликт получился</p>
</blockquote>
<p>Спустя полтора часа организаторы публикуют <a href="https://icfpcontest2020.github.io/#/post/2049">очередной пост</a>, где сообщают,
что Зайцев принимает некое огромное сообщение. Тем временем Портнов с N-ой
попытки таки доносит мысль, что пора писать код. Из дешифрованных сообщений
понятно, что перед нами некий «космолисп» — как бы Лисп, но вместо скобочек
польская запись и оператор частичного применения. К примеру, вот так
записывается список двух элементов <code>[1, 2]</code>:</p>
<pre><code>ap ap cons 1 cons 2 nil</code></pre>
<p>Также в «космолиспе» есть арифметика, <a href="https://en.wikipedia.org/wiki/SKI_combinator_calculus" title="SKI combinator calculus — Wikipedia">комбинаторы SKI</a> и целый
ряд функций ввода-вывода: некоторые рисуют картинки, другие что-то отправляют
инопланетянам и возвращают ответ. После короткого обсуждения становится понятно,
что выражения легко разбираются в бинарное дерево, где <code>ap</code> являются узлами,
а все остальные токены — листьями. Я сажусь писать функцию <code>reduce</code>,
«редуцирующую» любое выражение в как можно меньшее.</p>
<p>Тем временем прошло уже четыре часа с начала контеста. ForNeVeR, начитавшись
расшифровок сообщений, отправляется спать, а организаторы наконец-то публикуют
соломинку, связывающую «загадки» с HTTP API: Swagger-документ описывает, как
можно отправить инопланетянам сообщение и получить ответ. Становится очевидно,
что пятнадцатое сообщение, описывающее команду <code>send</code>, напрямую мапится в это
API. unclechu отправляется писать биндинги.</p>
<p>Спустя ещё сорок минут Зайцев наконец публикует то самое «огромное сообщение»,
galaxy.txt. На поверку оно оказывается тремястами килобайтами кода на
«космолиспе», где главная функция называется <code>galaxy</code>. Несколько предыдущих
сообщений описывают некий «протокол запуска», согласно которому в <code>galaxy</code> нужно
передавать некие параметры, получать результат, который снова передавать
в <code>galaxy</code>, и так далее.</p>
<p>Очевидно, что нам нужно как можно скорее запустить <code>galaxy</code> и понять, чего от
нас хотят инопланетяне. Рождается несколько идей:</p>
<ol type="1">
<li><p>навалиться всем вместе и довести функцию <code>reduce</code> до рабочего состояния,
реализовав всё необходимое для вычисления <code>galaxy</code>;</p></li>
<li><p>оттранслировать galaxy.txt в какой-нибудь «настоящий» Лисп, например, Scheme,
и выполнить;</p></li>
<li><p>оттранслировать galaxy.txt в Haskell и выполнить.</p></li>
</ol>
<p>Тут наконец-то «выстреливает» тот факт, что у нас большая команда: мы выбираем
<em>все</em> опции сразу. mr146 и я занимаемся <code>reduce</code>, pink-snow берётся за
трансляцию в Haskell, а portnov — в Racket. Спустя пару часов последние две идеи
отпадают: GHC не осиливает вывести типы рекурсивных функций, а Racket считает,
что все функции в galaxy.txt не принимают аргументов, и отказывается что-либо
вычислять. Высказав напоследок идею о том, как это исправить, Портнов
отправляется спать.</p>
<p>pink-snow берётся думать, как поверх ещё не дописанной <code>reduce</code> сделать функцию,
которая вычисляла бы <code>galaxy</code>. Дело в том, что сама <code>galaxy</code> ничего толком не
делает, она просто вызывает вспомогательную функцию, которая вызывает другие,
и так для графа из 1338-и функций. <code>reduce</code> умеет упрощать только отдельные
выражения, но не умеет подставлять в них тела других функций. Почему-то никому
не приходит в голову просто расширить <code>reduce</code>, чтобы она покрывала и вызовы
функций тоже. Вместо этого я убеждаю pink-snow написать цикл, в котором дерево
будет сначала упрощаться с помощью <code>reduce</code>, а затем в него будут подставляться
все константы — и так до тех пор, пока дерево меняется. Эта моя глупость нам ещё
аукнется…</p>
<p>Пока pink-snow думает, мы с mr146 реализуем в <code>reduce</code> все используемые
в <code>galaxy</code> функции, кроме рисования картинок и отправки HTTP запроса. Биндинги
к HTTP ещё не готовы, поэтому я ухожу спать, оставив соратникам моё видение
задач на следующий день:</p>
<ol type="1">
<li>объединив HTTP-биндинги от unclechu с функцией от pink-snow, получить
полноценную «вычислялку» выражений;</li>
<li>протестировать её на нескольких сообщениях, результат которых нам известен
заранее;</li>
<li>запустить наконец galaxy.txt.</li>
</ol>
<p>Послушают ли меня коллеги? Удастся ли нам реализовать задуманное? Успеем ли мы —
впервые в нашей истории! — что-то отправить на lightning round? Читайте
<a href="https://blog.debiania.in.ua/posts/2020-07-23-icfpc-2020-part-3.html" title="ICFPC 2020: день второй — Debiania">в следующем посте</a> :)</p>]]></description>
    <pubDate>Wed, 22 Jul 2020 00:00:00 UT</pubDate>
    <guid>https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-2.html</guid>
    <dc:creator>Александр Батищев</dc:creator>
</item>
<item>
    <title>ICFPC 2020: предисловие</title>
    <link>https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-1.html</link>
    <description><![CDATA[<p>Соревнование закончилось ещё позавчера, но во мне до сих пор бушуют эмоции. По
сложности и непредсказуемости ICFPC соперничает с самой жизнью, и в этом году
организаторы, кажется, подобрались к цели максимально близко — может быть, даже
<em>чересчур</em> близко. Я не смогу уместить все детали в один репортаж, так что
вместо одного поста будет шесть:</p>
<ol type="1">
<li>предисловие (это то, что вы сейчас читаете);</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-2.html" title="ICFPC 2020: день первый — Debiania">день первый</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-23-icfpc-2020-part-3.html" title="ICFPC 2020: день второй — Debiania">день второй</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-24-icfpc-2020-part-4.html" title="ICFPC 2020: день третий — Debiania">день третий</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-25-icfpc-2020-part-5.html" title="ICFPC 2020: день четвёртый — Debiania">день четвёртый</a>;</li>
<li><a href="https://blog.debiania.in.ua/posts/2020-07-26-icfpc-2020-part-6.html" title="ICFPC 2020: итоги и выводы — Debiania">итоги и выводы</a>.</li>
</ol>
<h1 id="погодите-а-что-такое-icfpc">Погодите, а что такое ICFPC?</h1>
<p>Это соревнование по программированию, приуроченное к ежегодной Международной
конференции по функциональному программированию (International Conference on
Functional Programming, ICFP). Вопреки названию, программировать можно на любом
языке, не обязательно функциональном. Соревнование длится 72 часа, но также есть
отдельный приз для тех, кто за первые сутки преуспеет больше остальных.</p>
<p>Основная «фишка» ICFP Programming Contest — это задачи. Они, мягко говоря,
нетривиальные. Как правило, задачу придумывают академики, и корни её зачастую
уходят в какую-нибудь зубодробительную область вроде оптимизационных проблем,
теории алгоритмов, робототехники, орбитальной механики или чего-нибудь такого.
Впрочем, за пару часов можно успеть нахвататься достаточно основ, чтобы хоть
как-то поучаствовать в соревновании.</p>
<p>Кроме того, задачи обычно «нерешаемые», и оценки не количественные,
а качественные: команда А решила задачу лучше, чем команда Б. Нередко это
выливается в прямое противостояние команд (например, кто <a href="https://blog.debiania.in.ua/posts/2017-09-03-icfpc-2017.html" title="ICFPC 2017 — Debiania">займёт больше «шахт»
на карте</a>), хотя бывают и простые гонки на то, кто дальше
продвинется (например, <a href="https://blog.debiania.in.ua/posts/2016-08-08-icfpc-2016.html" title="ICFPC 2016 — Debiania">по профилю оригами угадает, какие складки оно образует
на листе</a>).</p>
<h1 id="за-две-недели-до-контеста">За две недели до контеста</h1>
<p>Так уж повелось, что незадолго до начала соревнования организаторы «разогревают»
публику несколькими намёками на то, что может быть в задании. Как правило, это
пара коротких твитов, которые станут понятны лишь после начала контеста.</p>
<p>Я не люблю загадки, так что тизеры всегда игнорировал. Но в этом году
организаторы устроили настолько масштабную акцию, что не рассказать о ней
попросту нельзя.</p>
<p>Итак, за две недели до соревнования организаторы <a href="https://twitter.com/icfpcontest2020/status/1278965948461056000" title="ICFP Contest 2020 on Twitter">ретвитнули</a>
обращение своего знакомого учёного Ивана Зайцева, работающего на радиотелескопе
в Пеговке (смотреть сто́ит ради одних только антуража и атмосферы):</p>
<div class="center">
<p>
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/EjL-5EuQeCU" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>
</iframe>
</p>
</div>
<p>Зайцев якобы принял некие сигналы, похожие на сообщение, и просит их
расшифровать. В опубликованном им аудиофайле двумя разными тонами была
закодирована монохромная картинка с непонятными символами:</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2020-msg01.png" width="240px" height="470px" loading="lazy" alt="По левому краю столбец с квадратиками, справа от каждого квадратика
    несколько вертикальных палочек" /></p>
</div>
<p>Учёный создал <a href="https://message-from-space.readthedocs.io/en/latest/message15.html" title="Message from space">страничку на ReadTheDocs</a> (берегитесь — спойлеры!)
и чат в Discord, поощряя будущих участников ICFPC работать вместе и помочь ему
расшифровать послание. Народ быстро пришёл к выводу, что квадраты слева — это
кодировка для чисел, показанных справа. На картинке мы видим ноль, единицу и так
далее до восьми. Каждая точка внутри «квадрата» обозначает бит, и квадраты 3×3
кодируют четыре бита (диапазон чисел [3; 15]). Сразу же появилась гипотеза
о том, как кодируются числа больше пятнадцати.</p>
<p>На следующий день было получено новое сообщение, показывавшее больше интервалов
чисел. Вчерашняя гипотеза подтвердилась. Затем последовали сообщения
с отрицательными числами, отношением равенства, функциями инкремента
и декремента, суммы, произведения, целочисленного деления, введены понятия
переменных, булевых значений и несколько отношений порядка. Всё это
кульминировало тремя сообщениями, два из которых описывали конвертирование чисел
из «квадратного» представления в «линейное» и обратно, а третье, непохожее на
все предыдущие, выглядело как картина:</p>
<div class="center">
<p><img src="https://blog.debiania.in.ua/images/icfpc-2020-msg15.png" width="512px" height="704px" loading="lazy" alt="Похоже, нечто прилетает сюда из космоса, и между сторонами происходит
    какой-то обмен сообщениями" /></p>
</div>
<p>Таким образом, инопланетяне две недели описывали нам некий язык выражений,
и напоследок предлагают пообщаться. Зачем всё это? Какое отношение это имеет
к контесту? Согласитесь, сделать задачу продолжением тизеров было бы весьма
убого и даже несколько нечестно: некоторые уже потратили две недели на
«въезжание» в контекст и, таким образом, имели преимущество над вновь
прибывшими.</p>
<p>О том, что произошло в первый день соревнования, читайте <a href="https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-2.html" title="ICFPC 2020: день первый — Debiania">в следующем
посте</a>.</p>]]></description>
    <pubDate>Wed, 22 Jul 2020 00:00:00 UT</pubDate>
    <guid>https://blog.debiania.in.ua/posts/2020-07-22-icfpc-2020-part-1.html</guid>
    <dc:creator>Александр Батищев</dc:creator>
</item>

    </channel>
</rss>
