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

Advent of Code 2024 - Day 21

2024/day-21.livemd

Advent of Code 2024 - Day 21

Mix.install([
  {:kino, github: "livebook-dev/kino"},
  {:memoize, "~> 1.4"},
])

kino_input = Kino.Input.textarea("Please paste your input file: ")

Part 1

input = Kino.Input.read(kino_input)

# Gave up, https://github.com/ibarakaiev/advent-of-code-2024/blob/main/lib/advent_of_code2024/day21.ex
defmodule Keypad do
  @numeric_keypad %{
    "0" => {3, 1},
    "A" => {3, 2},
    "1" => {2, 0},
    "2" => {2, 1},
    "3" => {2, 2},
    "4" => {1, 0},
    "5" => {1, 1},
    "6" => {1, 2},
    "7" => {0, 0},
    "8" => {0, 1},
    "9" => {0, 2},
  }

  @directional_keypad %{
    "<" => {1, 0},
    "v" => {1, 1},
    ">" => {1, 2},
    "^" => {0, 1},
    "A" => {0, 2},
  }

  def expand_sequence(sequence, [], _level) do
    {Enum.count(sequence), []}
  end

  def expand_sequence(sequence, current_chars, level) do
    sequence
    |> Enum.reduce({0, current_chars},
      fn target_char, {acc, [current_char | rest_chars]} ->
        current_char
        |> sequence_for_keypad(target_char, if(level == 0, do: :numeric, else: :directional))
        |> Enum.map(fn sequence ->
          {expanded_sequence, updated_chars} = expand_sequence(sequence, rest_chars, level + 1)

          {acc + expanded_sequence, [target_char | updated_chars]}
        end)
        |> Enum.min_by(fn {sequence_length, _} -> sequence_length end)
      end)
  end

  defp sequence_for_keypad(starting_char, target_char, keypad_type) do
    keypad =
      case keypad_type do
        :numeric -> @numeric_keypad
        :directional -> @directional_keypad
      end

    {starting_i, starting_j} = keypad[starting_char]
    {target_i, target_j} = keypad[target_char]

    horizontal_moves = List.duplicate(if(target_j > starting_j, do: ">", else: "<"), abs(target_j - starting_j))
    vertical_moves = List.duplicate(if(target_i > starting_i, do: "v", else: "^"), abs(target_i - starting_i))

    cond do
      (keypad_type == :numeric and starting_i == 3 and target_j == 0) or
          (keypad_type == :directional and starting_i == 0 and target_j == 0) ->
        [vertical_moves ++ horizontal_moves ++ ["A"]]

      (keypad_type == :numeric and starting_j == 0 and target_i == 3) or
          (keypad_type == :directional and starting_j == 0 and target_i == 0) ->
        [horizontal_moves ++ vertical_moves ++ ["A"]]

      true ->
        [horizontal_moves ++ vertical_moves ++ ["A"], vertical_moves ++ horizontal_moves ++ ["A"]]
    end
  end
end

input
|> String.split("\n", trim: true)
|> Enum.map(&amp;String.graphemes/1)
|> Enum.map(fn number ->
  {sequence, _} = Keypad.expand_sequence(number, List.duplicate("A", 2+1), 0)
  numeric_part = number |> Enum.reject(&amp;(&amp;1 == "A")) |> Enum.join() |> String.to_integer()

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

Part 2

input = Kino.Input.read(kino_input)

# Gave up, https://github.com/ibarakaiev/advent-of-code-2024/blob/main/lib/advent_of_code2024/day21.ex
defmodule KeypadPart2 do
  use Memoize
  
  @numeric_keypad %{
    "0" => {3, 1},
    "A" => {3, 2},
    "1" => {2, 0},
    "2" => {2, 1},
    "3" => {2, 2},
    "4" => {1, 0},
    "5" => {1, 1},
    "6" => {1, 2},
    "7" => {0, 0},
    "8" => {0, 1},
    "9" => {0, 2},
  }

  @directional_keypad %{
    "<" => {1, 0},
    "v" => {1, 1},
    ">" => {1, 2},
    "^" => {0, 1},
    "A" => {0, 2},
  }

  defmemo expand_sequence(sequence, [], _level) do
    {Enum.count(sequence), []}
  end

  defmemo expand_sequence(sequence, current_chars, level) do
    sequence
    |> Enum.reduce({0, current_chars},
      fn target_char, {acc, [current_char | rest_chars]} ->
        current_char
        |> sequence_for_keypad(target_char, if(level == 0, do: :numeric, else: :directional))
        |> Enum.map(fn sequence ->
          {expanded_sequence, updated_chars} = expand_sequence(sequence, rest_chars, level + 1)

          {acc + expanded_sequence, [target_char | updated_chars]}
        end)
        |> Enum.min_by(fn {sequence_length, _} -> sequence_length end)
      end)
  end

  defp sequence_for_keypad(starting_char, target_char, keypad_type) do
    keypad =
      case keypad_type do
        :numeric -> @numeric_keypad
        :directional -> @directional_keypad
      end

    {starting_i, starting_j} = keypad[starting_char]
    {target_i, target_j} = keypad[target_char]

    horizontal_moves = List.duplicate(if(target_j > starting_j, do: ">", else: "<"), abs(target_j - starting_j))
    vertical_moves = List.duplicate(if(target_i > starting_i, do: "v", else: "^"), abs(target_i - starting_i))

    cond do
      (keypad_type == :numeric and starting_i == 3 and target_j == 0) or
          (keypad_type == :directional and starting_i == 0 and target_j == 0) ->
        [vertical_moves ++ horizontal_moves ++ ["A"]]

      (keypad_type == :numeric and starting_j == 0 and target_i == 3) or
          (keypad_type == :directional and starting_j == 0 and target_i == 0) ->
        [horizontal_moves ++ vertical_moves ++ ["A"]]

      true ->
        [horizontal_moves ++ vertical_moves ++ ["A"], vertical_moves ++ horizontal_moves ++ ["A"]]
    end
  end
end

input
|> String.split("\n", trim: true)
|> Enum.map(&amp;String.graphemes/1)
|> Enum.map(fn number ->
  {sequence, _} = KeypadPart2.expand_sequence(number, List.duplicate("A", 25+1), 0)
  numeric_part = number |> Enum.reject(&amp;(&amp;1 == "A")) |> Enum.join() |> String.to_integer()

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