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

Advent of Code 2024 - Day 04

2024/04.livemd

Advent of Code 2024 - Day 04

Mix.install([
  {:req, "~> 0.5"},
  {:benchee, "~> 1.3"}
])

Input

opts = [headers: [{"cookie", "session=#{System.fetch_env!("LB_AOC_SESSION")}"}]]
puzzle_input = Req.get!("https://adventofcode.com/2024/day/4/input", opts).body
grid =
  String.split(puzzle_input, "\n", trim: true)
  |> Enum.map(fn row ->
    String.codepoints(row)
    |> Enum.with_index()
    |> Enum.map(fn {char, index} -> {index, char} end)
    |> Map.new()
  end)
  |> Enum.with_index()
  |> Enum.map(fn {char, index} -> {index, char} end)
  |> Map.new()

Grid

defmodule Grid do
  defp all_dirs, do: diagonal_dirs() ++ orthogonal_dirs()

  defp diagonal_dirs, do: [{-1, -1}, {1, -1}, {1, 1}, {-1, 1}]

  defp orthogonal_dirs, do: [{0, -1}, {1, 0}, {0, 1}, {-1, 0}]

  defp flip_dir({col, row}), do: {-col, -row}

  defp move_dir({col_modifier, row_modifier} = _dir, {col, row} = _pos) do
    {col + col_modifier, row + row_modifier}
  end

  def check(grid, pos) do
    for dir <- all_dirs() do
      check(grid, dir, pos)
    end
    |> Enum.sum()
  end

  def check_x(grid, pos) do
    result =
      for dir <- diagonal_dirs() do
        check_x(grid, dir, pos)
      end
      |> Enum.sum()

    if result > 1, do: 1, else: 0
  end

  def check_x(grid, dir, pos) do
    start_pos = dir |> flip_dir() |> move_dir(pos)

    case build_sequence(grid, 3, start_pos, dir) do
      "MAS" -> 1
      _invalid -> 0
    end
  end

  def check(grid, dir, pos) do
    case build_sequence(grid, 4, pos, dir) do
      "XMAS" -> 1
      _invalid -> 0
    end
  end

  defp build_sequence(grid, amount, start_pos, dir) do
    Enum.reduce(0..(amount-1), {"", start_pos}, fn _index, {text, pos} ->
      current_text = at(grid, pos)
      new_pos = move_dir(dir, pos)

      {text <> current_text, new_pos}
    end)
    |> elem(0)
  end

  def at(grid, {col, row} = _pos) do
    get_in(grid, [row, col]) || ""
  end
end
col_size = Map.get(grid, 0) |> Map.keys() |> length()
row_size = Map.keys(grid) |> length()

col_range = 0..(col_size - 1)
row_range = 0..(row_size - 1)

Puzzle 1

puzzle_1 = fn ->
  Enum.reduce(row_range, 0, fn row, row_acc ->
    Enum.reduce(col_range, row_acc, fn col, col_acc ->
      col_acc + Grid.check(grid, {col, row})
    end)
  end)
end

puzzle_1.()

Puzzle 2

puzzle_2 = fn ->
  Enum.reduce(row_range, 0, fn row, row_acc ->
    Enum.reduce(col_range, row_acc, fn col, col_acc ->
      col_acc + Grid.check_x(grid, {col, row})
    end)
  end)
end

puzzle_2.()

Benchmarks

Benchee.run(%{
  "puzzle_1" => fn -> puzzle_1.() end,
  "puzzle_2" => fn -> puzzle_2.() end
})
Name ips average deviation median 99th %
puzzle_1 15.40 64.94 ms ±3.90% 64.37 ms 82.61 ms
puzzle_2 27.07 36.94 ms ±12.27% 36.32 ms 73.06 ms