Обновить

Комментарии 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

Day 2.

Решение (метапрограммирование довольно изящное получилось):

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

Day 3.

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

Day 4.

Элегантный переход от первой части ко второй без модификаций исходного кода: |> 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

Day 5.

  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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации