Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

๐ŸŽ„ Year 2024 ๐Ÿ”” Day 21

elixir/notebooks/2024/day21.livemd

๐ŸŽ„ Year 2024 ๐Ÿ”” Day 21

Mix.install([
  {:arrays, "~> 2.1"}
])

Setup

defmodule Helper do
  @move_priority String.graphemes("")
  @char_to_move %{
    "^" => {0, -1},
    ">" => {1, 0},
    "v" => {0, 1},
    "<" => {-1, 0},
    "A" => {0, 0}
  }

  def generate_navigations(keypad) do
    {max_x, max_y} = {Arrays.size(keypad) - 1, Arrays.size(keypad[0]) - 1}

    keys = for x <- 0..max_x, y <- 0..max_y, keypad[x][y] != nil, do: {x, y, keypad[x][y]}

    for key1 <- keys,
        key2 <- keys do
      {x1, y1, e1} = key1
      {x2, y2, e2} = key2

      c1 =
        if(x1 < x2, do: List.duplicate(">", x2 - x1), else: List.duplicate("<", x1 - x2))
        |> Enum.join("")

      c2 =
        if(y1 < y2, do: List.duplicate("v", y2 - y1), else: List.duplicate("^", y1 - y2))
        |> Enum.join("")

      paths =
        ["#{c1}#{c2}A", "#{c2}#{c1}A"]
        |> Enum.uniq()
        |> Enum.reject(fn chars ->
          chars
          |> String.graphemes()
          |> Enum.scan({x1, y1}, fn char, {x, y} ->
            {xm, ym} = @char_to_move[char]
            {x + xm, y + ym}
          end)
          |> Enum.map(fn {x, y} -> keypad[x][y] end)
          |> then(&amp;Enum.any?(&amp;1, fn e -> e == nil end))
        end)

      optimal_path =
        paths
        |> Enum.min_by(fn <> <> _ ->
          Enum.find_index(@move_priority, fn e -> e == c end)
        end)

      {[e1, e2], optimal_path}
    end
    |> Map.new()
  end
end
instructions =
  File.read!("#{__DIR__}/../../../inputs/2024/day21.txt")
  |> String.split("\n", trim: true)

digit_keypad =
  [
    ["7", "8", "9"],
    ["4", "5", "6"],
    ["1", "2", "3"],
    [nil, "0", "A"]
  ]
  |> List.zip()
  |> Enum.map(&amp;Tuple.to_list/1)
  |> Enum.map(&amp;Arrays.new/1)
  |> Arrays.new()

arrow_keypad =
  [
    [nil, "^", "A"],
    ["<", "v", ">"]
  ]
  |> List.zip()
  |> Enum.map(&amp;Tuple.to_list/1)
  |> Enum.map(&amp;Arrays.new/1)
  |> Arrays.new()

Part 1

defmodule Part1 do
  @navigations Map.merge(
                 Helper.generate_navigations(digit_keypad),
                 Helper.generate_navigations(arrow_keypad)
               )

  def solve(sequence, 0) do
    sequence
  end

  def solve(sequence, level) do
    new_sequence =
      ("A" <> sequence)
      |> String.graphemes()
      |> Enum.chunk_every(2, 1, :discard)
      |> Enum.map(fn chunk -> Map.fetch!(@navigations, chunk) end)
      |> Enum.join()

    solve(new_sequence, level - 1)
  end
end

instructions
|> Enum.map(fn sequence ->
  lenght_of_shortest = Part1.solve(sequence, 3) |> String.length()

  numeric_part =
    Regex.run(~r/(\d{3})A/, sequence, capture: :all_but_first)
    |> then(fn [e] -> String.to_integer(e) end)

  lenght_of_shortest * numeric_part
end)
|> Enum.sum()
|> tap(fn result -> if result == 212_488, do: IO.puts("Correct ๐ŸŽ‰"), else: IO.puts("Wrong") end)

Part 2

defmodule Part2 do
  @navigations Map.merge(
                 Helper.generate_navigations(digit_keypad),
                 Helper.generate_navigations(arrow_keypad)
               )

  def solve(sequence, 0) do
    sequence |> String.length()
  end

  def solve(sequence, level) do
    sequence
    |> get_portion_frequencies()
    |> Enum.map(fn {sequence, count} ->
      case :ets.lookup(:memo, {sequence, level}) do
        [{_, result}] ->
          result * count

        [] ->
          next_sequence = solve_part(sequence)
          result = solve(next_sequence, level - 1)
          :ets.insert(:memo, {{sequence, level}, result})
          result * count
      end
    end)
    |> Enum.sum()
  end

  def solve_part(sequence) do
    ("A" <> sequence)
    |> String.graphemes()
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.map(fn chunk -> Map.fetch!(@navigations, chunk) end)
    |> Enum.join()
  end

  def get_portion_frequencies(sequence) do
    sequence
    |> String.graphemes()
    |> Enum.reduce([[]], fn c, [head | tail] ->
      if c == "A" do
        [[], Enum.reverse([c | head]) | tail]
      else
        [[c | head] | tail]
      end
    end)
    |> Enum.drop(1)
    |> Enum.map(&amp;Enum.join(&amp;1, ""))
    |> Enum.frequencies()
  end
end

if :ets.whereis(:memo) == :undefined do
  :ets.new(:memo, [:set, :public, :named_table])
end

result =
  instructions
  |> Enum.map(fn sequence ->
    lenght_of_shortest = Part2.solve(sequence, 26)

    numeric_part =
      Regex.run(~r/(\d{3})A/, sequence, capture: :all_but_first)
      |> then(fn [e] -> String.to_integer(e) end)

    lenght_of_shortest * numeric_part
  end)
  |> Enum.sum()

:ets.delete(:memo)

result
|> tap(fn result ->
  if result == 258_263_972_600_402, do: IO.puts("Correct ๐ŸŽ‰"), else: IO.puts("Wrong")
end)