Advent of Code 2019 D08

Восьмой день оказался для меня сильно проще остальных.

После того, как Санта наконец-то сумел выдать на ускорителях нужную тягу и вырваться из притяжения Юпитера и хаоса вращающихся вокруг него тел, эльфы радостно загрузили Санту новой задачей — нужно заскочить на Марс и перезагрузить марсоход.

Высадившись рядом с ним, выяснилось, что он уже перезагружается и ждёт когда кто-нибудь введёт пароль от его BIOS. Эльф сфотографировал пароль и отправил его Санте через космическую сеть цифровой связи.

К сожалению, изображения, отправляемые через эту сеть, кодируются в особый формат ФАК: формат для асинхронного космоса. Никто из эльфов не может вспомнить, почему так произошло… они отправляют сведения о том, как его раскодировать.

Первая половина задачи

Изображения отправляются как последовательность цифр, каждая из которых обозначает цвет пикселя. Пиксели заполняют изображение слева направо и сверху вниз. Каждое изображение состоит из слоёв одинакового размера, которые заполняются таким образом. Первое число представляет из себя левый верхний угол первого слоя, а последнее — правый нижний последнего слоя.

Принимаемое изображение размером 25×6. Чтобы убедиться, что сообщение не повреждено, эльфы просят найти слой, в котором меньше всего нулей, и сообщить число, являющееся результатом умножения числа единиц на число двоек в этом слое.

В качестве исходных данных мы получаем литерал чисел длиной 15000 значений. Рассмотрение показало, что в списке возможны всего три значения: 0, 1 и 2.

Для начала определим две важных константы:

let imgSize = 25 * 6
let imgLayers = inputData.Length / imgSize

Теперь порежем этот поток данных на куски, равные размеру изображения. Возьмём число слоёв, и передадим в функцию отображения, которая из исходного потока будет выкусывать фрагменты. Формула подсчёта местоположения левой и правой границ проста и понятна.

let layers =
    [ 0 .. imgLayers - 1 ] |> List.map (fun x -> inputData.[(x * imgSize)..((x * imgSize) + imgSize - 1)])

Если бы исходные данные не были бы кратны размеру изображения, было бы чуть более интересно.

Для решения первой половины задачи каждый из полученных слоёв мы превратим в структуру, в которой поля отвечают за число нулей, единиц и двоек. Я поначалу думал, что такую структуру можно определить анонимно на месте, но пришлось объявить тип.

Механика функции вполне проста: исходные слои функция List.map отображает в список вышеупомянутых структур, а в качестве функции для отображения выступает лямбда с функцией свёртки (мы сворачиваем слой чисел в одну-единственную структуру, каждый раз возвращая новую её копию).

В конце мы сортируем список структур по значению одного поля — X.

type layerInfo =
    { X : int
      Y : int
      Z : int }

let sortByX =
    layers
    |> List.map (fun x ->
        List.fold (fun acc y ->
            match y with
            | 0 -> { acc with X = acc.X + 1 }
            | 1 -> { acc with Y = acc.Y + 1 }
            | 2 -> { acc with Z = acc.Z + 1 })
            { X = 0
              Y = 0
              Z = 0 } (Array.toList x))
    |> List.sortBy (fun i -> i.X)

Ответ на первую половину задачи:

let answer1 = (List.head sortByX).Y * (List.head sortByX).Z

Вторая половина задачи

Теперь можно расшифровать изображение. Изображение выводится сложением слоёв. Числа означают следующее: 0 соответствует чёрному цвету, 1 белому, а 2 — прозрачности.

Первый слой располагается «впереди», а последний — «позади», так что если в заданной позиции первых двух слоёв пиксель прозрачный, а в третьем чёрный, то в итоговом изображении в этом месте будет чёрный пиксель.

Какое сообщение написано на присланной картинке?

Алгоритмически решение очевидно: проход по первому слою с занесением в итоговое изображение значений 0 или 1, и уход «вглубь» структуры с поиском в той же позиции первого нужного значения.

Для этого создадим рекурсивную функцию, которая принимает список списков (исходную структуру изображения), номер слоя в списке и позицию пикселя в этом слое.

Если находим в слое двойку, переходим на слой ниже (lst + 1) в той же структуре данных с той же позицией. Понятно что тут я поленился поставить проверку — теоретически нужна проверка на глубину слоя в структуре изображения, ведь вполне возможно, что все слои вплоть до последнего будут прозрачными, и на последнем слое программа упадёт, потому что обращение к следующему слою станет невозможным. Но мы просто решаем весёлые задачки.

let rec get (metalist : int [] list) (lst : int) position =
    match metalist.[lst].[position] with
    | 0 -> 0
    | 1 -> 1
    | 2 -> get metalist (lst + 1) (position)

После этого пропускаем число пикселей в изображении через List.map, который в качестве лямбды вызывает нашу рекурсивную функцию и возвращает список из всех значений итогового изображения.

let answer2 = [ 0 .. imgSize - 1 ] |> List.map (fun x -> get layers 0 x)

Вывести итоговый список размером 25×6 — тривиально.