Я большой поклонник языка программирования Elixir. Поэтому я решил решить с его помощью все проблемы Advent of Code.
Оглавление | Пришествие Кода День 1 | 🚧 Появление кода, день 3 🚧
Материалы второго дня можно найти здесь: https://adventofcode.com/2022/day/2
Во второй день наши друзья-эльфы играют в «Камень, ножницы, бумага», также известную как Рошамбо или Джанкенпон, чтобы решить, кто сядет ближе всего к хранилищу закусок. Невероятно родственный.
Победитель определяется по очкам. За победу вы получаете 6 очков, за ничью — 3 очка, за поражение — ни одного очка. Кроме того, рука с камнем дает одно очко, бумага — два, а ножницы — три.
Таким образом, если вы выиграете раунд и бросите руку-ножницы, вы получите 6 + 3 = 9
очков.
Эльфам так понравилась ваша вчерашняя работа, что они записывают свои броски, чтобы вы могли выиграть! Каждый эльф написал свою собственную руку, похожую на камень, ножницы, бумагу, и, что удобно, они написали и вашу руку. Основная шпаргалка, которую они вам дали, выглядит так:
A Y B X C Z
Камень — это и A
, и X
, Бумага — это и B
, и Y
, а Ножницы — это C
и Z
. Нам нужно будет определить, кто побеждает в каждом раунде, суммировать очки и выяснить, сколько очков мы набрали.
Давайте приступим к моделированию нашей первой задачи второго дня.
Есть несколько способов решения подобных проблем. Моя первая попытка использовала математику по модулю 3 для математического преобразования списков [A, B, C]
и [X, Y, Z]
(по сути преобразования [Rock, Paper, Scissors]
) в [0, 1, 2]
.
Для тех, кто не знаком с математикой по модулю, давайте кратко поговорим об этом. Ряд целых чисел без знака [0, 1, 2, 3, 4, 5, 6, 7, …]
продолжается бесконечно. Назовем этот набор I. Это означает, что бесконечное число чисел можно разделить на 2, на 3, на 5 и так далее. Мы можем взять наш бесконечно продолжающийся набор целых чисел и преобразовать его в наше проблемное пространство [Камень, ножницы, бумага], разделив на 3 количество различных результатов и получив остаток.
В нашей формуле f(x) = x % 3 for x in I
,[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
(или I)становится [0, 1, 2, 0, 1, 2, 0, 1, 2, …]
, также повторяясь бесконечно. Давайте смоделируем этот набор не с помощью чисел, а с помощью наших результатов «Камень, ножницы и бумага».
rock paper scissors paper scissors rock scissors rock paper
Перемещение по горизонтальной оси слева направо и перемещение по вертикальной оси сверху вниз — это преобразование (n + 1) % 3
в нашем проблемном пространстве.
rock paper scissors paper scissors
Это наше проигрышное направление. Если their_hand == (our hand + 1) % 3
верно, мы можем определить с помощью математики, что наша рука проигрывает.
Когда мы перемещаем эти оси в противоположном направлении, происходит математическое преобразование (n — 1) % 3
. Однако мы имеем дело с целыми числами без знака по модулю, а не с числами со знаком. Давайте переделаем это — 1
, чтобы получить такой же остаток при делении на три.
Давайте удостоверимся, что когда мы вычитаем единицу, мы не получаем отрицательное число. В нашем проблемном пространстве мы знаем, что набор [0, 1, 2]
повторяется бесконечно, поэтому давайте воспользуемся этим фактом, чтобы манипулировать нашей матрицей.
Если число, с которого мы начинаем, равно x
, и мы вычитаем единицу:
[0, 1, 2, 0, 1, 2, 0, 1, x, 0, 1, 2, 0, 1, 2] (x - 1) % 3 = 1 (x + 2) % 3 = 1
На самом деле мы запрашиваем предыдущее число в наборе или число, стоящее на две позиции перед нашим числом.
Итак, вместо f(x) = (x — 1) % 3
давайте использовать f(x) = (x + 2) % 3
.
Помните, что эти числа соответствуют нашему списку исходов [Rock, Paper, Scissors]
. Теперь мы можем сказать, что their_hand == (our hand + 2) % 3
верно, мы можем определить с помощью математики, что наша рука выигрывает.
Не все задачи следует моделировать с помощью математики. Правда в том, что мы не компьютеры. Многие думают, что программирование связано с математикой, но обычно это не так. Большая часть необходимой математики в компьютерных науках абстрагирована от нас, разработчиков, и мы беспокоимся о таких проблемах, как отображение взаимосвязей вещей. Почти как наш собственный мозг. В конце концов, код пишется мозгом, а не компьютером.
Чрезвычайно сложно расширить эту математику на что-то полезное для второго решения. Признаюсь, я был в тупике. Я объясню почему, когда мы туда доберемся.
А пока мы решим проблему 1 по-другому, используя небольшой размер нашего проблемного пространства в свою пользу.
Эликсир имеет структуры вместо классов. Структуры работают как классы, имея атрибуты и методы, изменяющие структуру. В отличие от классов с экземплярами, данные в Elixir неизменяемы. Когда вы манипулируете структурой, скажем, устанавливая Person.name
в “Johnny Appleseed”
, вы возвращаете измененную структуру в другом месте памяти. Старая структура собрана из мусора. Таким образом, структуры не имеют концепции экземпляров класса.
Сопоставление шаблона со структурой в Эликсире очень просто. Если вы новичок в языке или в моей серии, я рассказал о сопоставлении с образцом в своей статье первого дня этой серии. Там я обсуждаю переменные сопоставления с образцом, литералы и списки. Здесь я буду обсуждать сопоставление шаблонов со структурой.
Давайте, наконец, погрузимся в код в lib/day02/day2.ex
:
defmodule AdventOfCode.Day2 do @loss 0 @draw 3 @win 6 def read_input() do (__DIR__ <> "/input.txt") |> File.read!() |> String.split(~r/\r?\n/, trim: true) end end
Небольшая врезка об именах функций Elixir: read_input()
и read_input(arg)
с одним аргументом — это разные функции, и в терминах Elixir они обозначаются как read_input/0
и read_input/1
соответственно. Косая черта — это количество аргументов, которые имеет функция, и называется arity
функции. Я буду ссылаться на множество функций в этой серии, поэтому, начиная с этой записи дня 2, я буду писать их так.
Мы копируем read_input/1
из нашей записи первого дня и меняем повторение в нашем регулярном выражении String.split/3
с соответствия \r\n\r\n
и \n\n
на соответствие \n
или \r\n
. (\r?\n){2}
становится \r?\n
.
Далее давайте смоделируем наши руки «Камень, ножницы, бумага» в виде структуры «Рука»:
defmodule AdventOfCode.Day2.Hand do defstruct [:val, :points, :beats, :loses_to] def new(:rock) do %AdventOfCode.Day2.Hand{ val: :rock, points: 1, beats: :scissors, loses_to: :paper } end def new(:paper) do %AdventOfCode.Day2.Hand{ val: :paper, points: 2, beats: :rock, loses_to: :scissors } end def new(:scissors) do %AdventOfCode.Day2.Hand{ val: :scissors, points: 1, beats: :paper, loses_to: :rock } end def from_character("A"), do: new(:rock) def from_character("B"), do: new(:paper) def from_character("C"), do: new(:scissors) def from_character("X"), do: new(:rock) def from_character("Y"), do: new(:paper) def from_character("Z"), do: new(:scissors) end
Модули Эликсира могут находиться в любом каталоге или с любым именем файла, если код находится в папке lib/
. Вы можете поместить эти модули в разные файлы, и это рекомендуется для больших проектов. Но этот модуль специфичен для этой проблемы, поэтому я просто помещу их в один файл.
Обратите внимание, что совпадения нашего шаблона в new/1
создают структуру со всей информацией, необходимой для определения победителя и проигравшего. Мы смогли смоделировать исходы рук таким образом, потому что наши исходы ограничены. Для большего набора результатов вам нужно будет смоделировать форму структуры по-другому, чтобы учесть этот размер.
Вернемся к нашему модулю Day2 и решим задачу 1 с нашими новыми структурами:
alias AdventOfCode.Day2.Hand def solution1 do read_input() |> Stream.map(&calculate_hand_score/1) |> Enum.sum() end def calculate_hand_score(line) do [their_hand, my_hand] = String.split(line, " ") compare_hands( Hand.from_character(their_hand), Hand.from_character(my_hand) ) end def compare_hands(their_hand, my_hand) do cond do my_hand.val == their_hand.beats -> my_hand.points + @loss my_hand.val == their_hand.val -> my_hand.points + @draw my_hand.val == their_hand.loses_to -> my_hand.points + @win end end
alias AdventOfCode.Day2.Hand
сокращает название нашей структуры и модуля до Hand
. Мы создаем обе руки из строки ввода, используя нашу функцию Hand.from_character/1
в calculate_hand_score/1
, что дает нам всю информацию, необходимую для определения исхода раунда. compare_hands/2
дает нам счет за каждый раунд.
Макрос cond
позволяет нам перечислить множество логических вычислений в последовательности, и первое значение, которое будет оценено как истинное, будет возвращаемым значением cond
. И снова Эликсир дает нам инструмент для пропуска операторов if
.
Давайте скомпилируем наш код в нашем терминале:
iex -S mix
И выполните нашу функцию, чтобы распечатать решение задачи 1 дня 2.
AdventOfCode.Day2.solution1
Когда я решаю проблему, мне нравится писать расширяемые решения. Обычно две части проблемы связаны между собой, и если вы правильно спроектируете свой код, вы сможете настроить себя на быстрые и минимальные изменения для решения проблемы 2. Именно здесь мое первоначальное решение с использованием модульной математики потерпело неудачу.
Давайте еще раз посмотрим на наши наборы [0, 1, 2]
, [Rock, Paper, Scissors]
в виде списка [loss,draw,win]
. Если наша рука равна 0 (Камень), то 1 — это то, кому мы проигрываем (Бумага), 2 (Ножницы) — это то, кому мы выигрываем, а 3 или 0 — это ничья.
Это также может быть смоделировано в виде набора. Учитывая число или результат руки, следующие три числа, то есть +1, +2 и +3, будут набором результатов игры [loss, win, draw]
. Помните, что важна не позиция в сете, а позиция относительно нашей руки.
Именно здесь, в Задаче 2, мы сопоставляем символы X
, Y
и Z
из нашего входного файла с результатами loss
, draw
, win
. Если мы расширим наш вышеприведенный набор [проигрыш, выигрыш, ничья] до бесконечности по вертикальной и горизонтальной оси, мы заметим несколько вещей в нашем наборе, основанном на [Rock, Paper, Scissors]
:
loss win draw loss win draw loss win draw loss win draw win draw loss win draw loss win draw loss win draw loss draw loss win draw loss win draw loss win draw loss win loss win draw loss win draw loss win draw loss win draw win draw loss win draw loss win draw loss win draw loss
Во-первых, мы замечаем, что для создания набора [loss, draw, win]
, когда задача отображает X
, Y
и Z
, нам нужно выполнить другую манипуляцию с набором позиций, чтобы извлечь, что такое рука. Это растопило мой мозг, и в этот момент я решил использовать структуры в этом ограниченном наборе результатов.
Многие задачи в информатике можно смоделировать с помощью математики, но это не значит, что так и должно быть.
Решение отпустить мою предыдущую неудачу дало жизнь этому решению проблемы 2:
defmodule AdventOfCode.Day2.Hand do def from_strategy(%AdventOfCode.Day2.Hand{beats: loser}, "X") do new(loser) end def from_strategy(%AdventOfCode.Day2.Hand{val: draw}, "Y") do new(draw) end def from_strategy(%AdventOfCode.Day2.Hand{loses_to: winner}, "Z") do new(winner) end end defmodule AdventOfCode.Day2 do def solution2 do read_input() |> Stream.map(&calculate_strategy_score/1) |> Enum.sum() end def calculate_strategy_score(line) do [their_hand, strategy] = String.split(line, " ") their_hand = Hand.from_character(their_hand) compare_hands( their_hand, Hand.from_strategy(their_hand, strategy) ) end end
Благодаря from_character/1
и нашей структуре у нас есть все необходимое из одной руки, чтобы знать, кто проигрывает, а кто выигрывает. Мы сопоставляем шаблон в from_strategy/1
, чтобы получить значение, которое нас интересует для этого функционального предложения “X”
проигрыш, "Y"
ничья и “Z”
выигрыш. Затем мы строим соответствующую руку с new/1
.
Еще раз компилируем код и получаем наше решение:
AdventOfCode.Day2.solution2
Исходный код этой статьи можно найти по адресу https://github.com/benjamindburke/advent-of-code-2022/blob/main/lib/day02/day2.ex.
Чтобы узнать больше о Advent of Code и других темах, следите за мной на Medium по адресу https://talesoffullstack.medium.com/
Ваша поддержка помогает мне решить, является ли этот контент полезным и информативным, а ваши отзывы также помогают мне создавать более качественные и подробные статьи. Если вам понравилась эта статья, пожалуйста, поставьте лайк или напишите комментарий!