Комментарии 5
2025 / День 1: Тайный вход
Постановка задачи
Часть первая.
У эльфов есть хорошие и плохие новости.
Хорошие новости состоят в том, что они открыли для себя управление проектами. Это дало им инструменты, необходимые для предотвращения их обычной рождественской чрезвычайной ситуации. Например, теперь они знают, что украшения Северного полюса должны быть готовы вскоре, чтобы другие критически важные задачи могли начаться вовремя.
Плохие новости в том, что они поняли: у них возникла другая чрезвычайная ситуация. Согласно их планированию ресурсов, ни у одного из них не осталось времени на украшение Северного полюса!
Чтобы спасти Рождество, эльфам нужен ты, чтобы ты закончил украшать Северный полюс к 12 декабря.
Собирай звезды, решая головоломки. Каждый день будут становиться доступными две головоломки; вторая головоломка открывается после решения первой. Каждая головоломка дает одну звезду. Удачи!
Ты прибываешь к тайному входу на базу Северного полюса, готовый начать украшать. К сожалению, похоже, пароль был изменен, и ты не можешь войти. Документ, приклеенный к стене, любезно объясняет:
«В связи с новыми протоколами безопасности пароль заперт в сейфе ниже. Пожалуйста, см. прилагаемый документ для получения новой комбинации».
У сейфа есть циферблат только со стрелкой; вокруг циферблата расположены числа от 0 до 99 по порядку. Когда ты поворачиваешь циферблат, он издает небольшой щелчок каждый раз, когда стрелка проходит очередное число.
Прилагаемый документ (твой входной файл головоломки) содержит последовательность вращений, по одному в строке, объясняющих, как открыть сейф. Вращение начинается с буквы L или R, которая указывает, нужно ли вращать влево (в сторону меньших чисел) или вправо (в сторону больших чисел). Затем в записи вращения идет числовое значение – расстояние, которое показывает, на сколько щелчков нужно повернуть циферблат в этом направлении.
Например, если циферблат указывал на 11, то вращение R8 приведет к тому, что циферблат будет указывать на 19. После этого вращение L19 заставит его указывать на 0.
Поскольку циферблат круговой, поворот циферблата влево от 0 на один щелчок заставляет его указать на 99. Аналогично, поворот циферблата вправо от 99 на один щелчок заставляет его указать на 0.
Таким образом, если циферблат указывал на 5, то вращение L10 приведет к тому, что он будет указывать на 95. После этого вращение R5 может привести к тому, что он будет указывать на 0.
Изначально циферблат указывает на 50.
Ты мог бы просто следовать инструкциям, но на недавнем обязательном официальном учебном семинаре по безопасности тайного входа на Северный полюс тебе объяснили, что сейф на самом деле является приманкой. Настоящий пароль — это количество раз, когда циферблат оказывается установленным на 0 после любого вращения из последовательности.
Например, пусть в приложенном документе содержатся следующие вращения:
L68
L30
R48
L5
R60
L55
L1
L99
R14
L82
Следуя этим вращениям, ты заставишь циферблат двигаться следующим образом:
Циферблат изначально указывает на 50.
Циферблат поворачивается L68 и начинает указывать на 82.
Циферблат поворачивается L30 и начинает указывать на 52.
Циферблат поворачивается R48 и начинает указывать на 0.
Циферблат поворачивается L5 и начинает указывать на 95.
Циферблат поворачивается R60 и начинает указывать на 55.
Циферблат поворачивается L55 и начинает указывать на 0.
Циферблат поворачивается L1 и начинает указывать на 99.
Циферблат поворачивается L99 и начинает указывать на 0.
Циферблат поворачивается R14 и начинает указывать на 14.
Циферблат поворачивается L82 и начинает указывать на 32.
Поскольку в этом процессе циферблат указывает на 0 суммарно три раза, пароль в этом примере равен 3.
Проанализируй вращения в приложенном документе. Каков фактический пароль для открытия двери?
Твой ответ в головоломке был ★★★★.
Часть вторая
Ты уверен, что это правильный пароль, но дверь не открывается. Ты стучишь, но никто не отвечает. Пока ты раздумываешь, ты лепишь снеговика.
Пока ты катаешь снежки для снеговика, находишь еще один документ по безопасности, который, должно быть, упал в снег:
«В связи с более новыми протоколами безопасности, пожалуйста, используйте метод пароля 0x434C49434B до дальнейшего уведомления».
Ты вспоминаешь с учебного семинара, что «метод 0x434C49434B» означает, что на самом деле нужно считать количество раз, когда любой щелчок приводит к тому, что циферблат указывает на 0, независимо от того, происходит ли это в конце вращения или в его процессе.
Следуя тем же вращениям, что и в примере выше, циферблат оказывается на нуле еще несколько дополнительных раз в ходе вращений:
Циферблат изначально указывает на 50.
Циферблат поворачивается L68 и начинает указывать на 82; в ходе этого вращения он один раз указывает на 0.
Циферблат поворачивается L30 и начинает указывать на 52.
Циферблат поворачивается R48 и начинает указывать на 0.
Циферблат поворачивается L5 и начинает указывать на 95.
Циферблат поворачивается R60 и начинает указывать на 55; в ходе этого вращения он один раз указывает на 0.
Циферблат поворачивается L55 и начинает указывать на 0.
Циферблат поворачивается L1 и начинает указывать на 99.
Циферблат поворачивается L99 и начинает указывать на 0.
Циферблат поворачивается R14 и начинает указывать на 14.
Циферблат поворачивается L82 и начинает указывать на 32; в ходе этого вращения он один раз указывает на 0.
В этом примере циферблат указывает на 0 три раза в конце вращений и еще три раза в процессе вращений. Таким образом, в этом примере новый пароль равен 6.
Будь осторожен: если циферблат указывает на 50, одно-единственное вращение вроде R1000 приведет к тому, что циферблат укажет на 0 десять раз, прежде чем вернуться обратно на 50!
Используя метод пароля 0x434C49434B, каков пароль для открытия двери?
Твой ответ в головоломке был ★★★★.
Моё решение:
defmodule Day1 do
@input File.read!("day1_1.input")
@phase1 false
if @phase1 do
defp day1_clicks_r(value) when rem(value, 100) == 0, do: 1
defp day1_clicks_r(_value), do: 0
defp day1_clicks_l(current, count), do: day1_clicks_r(current - count)
else
defp day1_clicks_r(value), do: div(value, 100)
defp day1_clicks_l(0, count), do: abs(div(count, 100))
defp day1_clicks_l(current, count), do: abs(div(current - count - 100, 100))
end
defp parse_safe_turn(<<"L", count::binary>>, {current, clicks}) do
count = String.to_integer(count)
{rem(10_000 + current - count, 100), clicks + day1_clicks_l(current, count)}
end
defp parse_safe_turn(<<"R", count::binary>>, {current, clicks}) do
count = String.to_integer(count)
{rem(current + count, 100), clicks + day1_clicks_r(current + count)}
end
def parse_input(input \\ @input, start \\ 50) do
~r/\s+/
|> Regex.split(input, trim: true)
|> Enum.reduce({start, 0}, &parse_safe_turn/2)
end
end
end
Решение (метапрограммирование довольно изящное получилось):
defmodule H do
def seq(i, count) do
List.duplicate(
{:"::", [], [{:seq, [], Elixir}, {:-, [], [{:binary, [], nil}, {:size, [], [i]}]}]},
count
)
end
end
defmodule Day2 do
@input "day2_1.input" |> File.read!() |> String.trim()
Enum.each(1..100, fn i ->
def forged_1(<<seq::binary-size(unquote(i)), seq::binary-size(unquote(i))>>), do: 1
end)
def forged_1(_), do: 0
import Aoc2025.H
for count <- 2..12, i <- 1..100 do
def forged_2(<<unquote_splicing(seq(i, count))>>), do: 1
end
def forged_2(_), do: 0
def calc(input \\ @input) do
input
|> String.split(",", trim: true)
|> Stream.map(&String.split(&1, "-", trim: true))
|> Stream.map(fn [b, e] -> String.to_integer(b)..String.to_integer(e) end)
|> Stream.map(fn range -> Enum.reduce(range, 0, &(&2 + &1 * forged_2("#{&1}"))) end)
|> Enum.sum()
end
end
Brute force.
defmodule Day3 do
@input "day3_1.input" |> File.read!() |> String.trim()
@phase1 false
if @phase1 do
defp parse(<<d1::binary-size(1), d2::binary-size(1), rest::binary>>),
do: do_parse_1(rest, {String.to_integer(d1), String.to_integer(d2)})
defp do_parse("", {d1, d2}), do: d1 * 10 + d2
defp do_parse(<<d::binary-size(1), rest::binary>>, {d1, d2}) do
d = String.to_integer(d)
acc =
cond do
d > d1 and rest != "" -> {d, -1}
d > d2 -> {d1, d}
true -> {d1, d2}
end
do_parse(rest, acc)
end
else
import Aoc2025.H
defp parse(<<d::binary-size(1), rest::binary>>),
do: do_parse(rest, {String.to_integer(d), unquote_splicing(minus_ones(11))})
defp do_parse("", digits),
do: digits |> Tuple.to_list() |> Integer.undigits()
defp do_parse(
<<d::binary-size(1), rest::binary>>,
{d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, d12}
) do
d = String.to_integer(d)
acc =
cond do
d > d1 and byte_size(rest) >= 11 -> {d, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}
d > d2 and byte_size(rest) >= 10 -> {d1, d, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}
d > d3 and byte_size(rest) >= 9 -> {d1, d2, d, -1, -1, -1, -1, -1, -1, -1, -1, -1}
d > d4 and byte_size(rest) >= 8 -> {d1, d2, d3, d, -1, -1, -1, -1, -1, -1, -1, -1}
d > d5 and byte_size(rest) >= 7 -> {d1, d2, d3, d4, d, -1, -1, -1, -1, -1, -1, -1}
d > d6 and byte_size(rest) >= 6 -> {d1, d2, d3, d4, d5, d, -1, -1, -1, -1, -1, -1}
d > d7 and byte_size(rest) >= 5 -> {d1, d2, d3, d4, d5, d6, d, -1, -1, -1, -1, -1}
d > d8 and byte_size(rest) >= 4 -> {d1, d2, d3, d4, d5, d6, d7, d, -1, -1, -1, -1}
d > d9 and byte_size(rest) >= 3 -> {d1, d2, d3, d4, d5, d6, d7, d8, d, -1, -1, -1}
d > d10 and byte_size(rest) >= 2 -> {d1, d2, d3, d4, d5, d6, d7, d8, d9, d, -1, -1}
d > d11 and byte_size(rest) >= 1 -> {d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d, -1}
d > d12 -> {d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, d}
true -> {d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, d12}
end
do_parse(rest, acc)
end
end
def calc(input \\ @input) do
input
|> String.split(["\s", "\n"], trim: true)
|> Enum.sum_by(&parse/1)
end
end
Элегантный переход от первой части ко второй без модификаций исходного кода: |> Stream.iterate(&calc/1)
defmodule Day4 do
@input "day4_1.input" |> File.read!() |> String.trim()
defp read_input(input) do
input
|> String.split(["\s", "\n"], trim: true)
|> then(fn [h | t] ->
stub = "." |> List.duplicate(byte_size(h)) |> Enum.join()
[stub, h | t] ++ [stub]
end)
|> Enum.map(&("." <> &1 <> "."))
end
def calc({input, acc} \\ {@input, 0}) do
input
|> read_input()
|> Enum.chunk_every(3, 1, :discard)
|> Enum.reduce({0, []}, fn [h, l, t], {count, res} ->
res = ["" | res]
Enum.reduce(1..(byte_size(l) - 2), {count, res}, fn idx, {count, [hres | tres]} ->
with <<_::binary-size(idx - 1), r1::binary-size(3), _::binary>> <- h,
<<_::binary-size(idx - 1), r21::binary-size(1), r22::binary-size(1),
r23::binary-size(1), _::binary>> <- l,
<<_::binary-size(idx - 1), r3::binary-size(3), _::binary>> <- t,
false <- r22 == "@" and String.count(r1 <> r21 <> r23 <> r3, "@") < 4,
do: {count, [r22 <> hres | tres]},
else: (_ -> {count + 1, ["x" <> hres | tres]})
end)
end)
|> then(fn {count, data} -> {Enum.join(data, "\n"), acc + count} end)
end
def calc_2(input \\ @input) do
{input, 0}
|> Stream.iterate(&calc/1)
|> Enum.reduce_while({0, -1}, fn
{_, prev}, {acc, prev} -> {:halt, acc}
{_, count}, {acc, _} -> {:cont, {count, acc}}
end)
end
end
defmodule Day5 do
@input "day5_1.input" |> File.read!() |> String.split("\n\n", trim: true)
def calc([ranges, ids] \\ @input) do
ranges =
ranges
|> String.split(["\s", "\n"], trim: true)
|> Enum.map(&String.split(&1, "-", trim: true))
|> Enum.map(fn [b, e] -> String.to_integer(b)..String.to_integer(e)//1 end)
ids
|> String.split(["\s", "\n"], trim: true)
|> Enum.reduce({0, []}, fn id, {count, ids} ->
id = String.to_integer(id)
Enum.reduce_while(ranges, {count, ids}, fn range, {count, ids} ->
if id in range, do: {:halt, {count + 1, [id | ids]}}, else: {:cont, {count, ids}}
end)
end)
end
def in_ranges([ranges, _ids] \\ @input) do
ranges
|> String.split(["\s", "\n"], trim: true)
|> Enum.map(&String.split(&1, "-", trim: true))
|> Enum.map(fn range -> Enum.map(range, &String.to_integer/1) end)
|> Enum.sort()
|> merge([])
|> Enum.sum_by(fn [f, l] -> l - f + 1 end)
end
defp merge([], acc), do: Enum.reverse(acc)
defp merge([range | rest], []), do: merge(rest, [range])
defp merge([[f2, l2] | rest], [[f1, l1] | acc]) do
if f2 <= l1 + 1 do
merged = [f1, max(l1, l2)]
merge(rest, [merged | acc])
else
merge(rest, [[f2, l2], [f1, l1] | acc])
end
end
end

Advent of Artificial Code