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

Advent of Code 2024 - Day 09

2024/09.livemd

Advent of Code 2024 - Day 09

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/9/input", opts).body

Helpers

defmodule DiskFragmenter do
  def parse_disk_map(puzzle_input) do
    String.trim(puzzle_input)
    |> String.graphemes()
    |> Enum.chunk_every(2, 2)
    |> Enum.with_index()
    |> Enum.flat_map(fn
      {[file, space], i} ->
        List.duplicate("#{i}", String.to_integer(file)) ++
          List.duplicate(".", String.to_integer(space))

      {[file], i} ->
        List.duplicate("#{i}", String.to_integer(file))
    end)
  end

  def calculate_checksum(list) do
    Enum.with_index(list)
    |> Enum.reduce(0, fn
      {value, i}, acc when value != "." ->
        i * String.to_integer(value) + acc

      _el, acc ->
        acc
    end)
  end

  def fill_dots(list, acc \\ [])

  def fill_dots([], acc), do: acc

  def fill_dots([char | rest], acc) when char == "." do
    case Enum.reverse(rest) |> Enum.find_index(&(&1 != ".")) do
      nil ->
        fill_dots(rest, acc ++ [char])

      i ->
        {value, rest} = List.pop_at(rest, length(rest) - 1 - i)
        fill_dots(rest, acc ++ [value])
    end
  end

  def fill_dots([char | rest], acc) do
    fill_dots(rest, acc ++ [char])
  end

  def fill_blocks(blocks, acc \\ [])
  def fill_blocks([], acc), do: List.flatten(acc)

  def fill_blocks([[first | _rest] = block | rest], acc) when first == "." do
    case find_suitable_block(rest, length(block)) do
      nil ->
        fill_blocks(rest, acc ++ [block])

      {num_block, index} ->
        extra_dots = length(block) - length(num_block)
        remaining_dots = if extra_dots > 0, do: [List.duplicate(".", extra_dots)], else: []
        new_rest = List.replace_at(rest, index, List.duplicate(".", length(num_block)))
        fill_blocks(remaining_dots ++ new_rest, acc ++ [num_block])
    end
  end

  def fill_blocks([block | rest], acc), do: fill_blocks(rest, acc ++ [block])

  defp find_suitable_block(blocks, dot_size) do
    blocks
    |> Enum.reverse()
    |> Enum.with_index()
    |> Enum.find(fn {[first | _] = list, _} ->
      first != "." &amp;&amp; length(list) <= dot_size
    end)
    |> case do
      nil -> nil
      {block, i} -> {block, length(blocks) - 1 - i}
    end
  end
end

Puzzle 1

puzzle_1 = fn ->
  DiskFragmenter.parse_disk_map(puzzle_input)
  |> DiskFragmenter.fill_dots()
  |> DiskFragmenter.calculate_checksum()
end

puzzle_1.()

Puzzle 2

puzzle_2 = fn ->
  DiskFragmenter.parse_disk_map(puzzle_input)
  |> Enum.chunk_by(&amp;(&amp;1 == "."))
  |> Enum.flat_map(fn el ->
    Enum.chunk_by(el, &amp; &amp;1)
  end)
  |> DiskFragmenter.fill_blocks()
  |> DiskFragmenter.calculate_checksum()
end

puzzle_2.()