Powered by AppSignal & Oban Pro

Day 5

notebooks/day05.livemd

Day 5

Mix.install([
  {:req, "~> 0.4.5"},
  {:kino, "~> 0.11.3"}
])

Input

session = System.fetch_env!("LB_AOC_SESSION")

input =
  Req.get!(
    "https://adventofcode.com/2023/day/5/input",
    headers: [{"Cookie", ~s"session=#{session}"}]
  ).body

Kino.Text.new(input, terminal: true)

Part 1

[seeds | maps] = String.split(input, "\n\n")

to_integers = fn line ->
  Regex.scan(~r/\d+/, line)
  |> List.flatten()
  |> Enum.map(&String.to_integer/1)
end

seeds = to_integers.(seeds)
maps =
  for map <- maps do
    [_ | ranges] = String.split(map, "\n", trim: true)

    for range <- ranges do
      range
      |> to_integers.()
      |> List.to_tuple()
    end
  end
to_location = fn seed ->
  for map <- maps, reduce: seed do
    acc ->
      Enum.reduce_while(map, acc, fn {dst, src, len}, val ->
        if val >= src and val < src + len do
          {:halt, val - src + dst}
        else
          {:cont, val}
        end
      end)
  end
end

seeds
|> Enum.map(to_location)
|> Enum.min()

Part 2

seed_ranges =
  Enum.chunk_every(seeds, 2)
  |> Enum.map(&amp;List.to_tuple/1)

overlap_and_map = fn {dst_off, src_off, src_len}, {in_off, in_len} = input ->
  input_end = in_off + in_len - 1
  src_end = src_off + src_len - 1

  if input_end < src_off or src_end < in_off do
    {:disjoint, input}
  else
    map_off = max(in_off, src_off)
    map_end = min(input_end, src_end)

    mapped = {dst_off + map_off - src_off, map_end - map_off + 1}

    unmapped =
      [
        {in_off, map_off - in_off},
        {map_end + 1, input_end - map_end}
      ]
      |> Enum.filter(fn {_, n} -> n > 0 end)

    {:intersecting, mapped, unmapped}
  end
end

to_locations = fn seed_range ->
  for map <- maps, reduce: [seed_range] do
    inputs ->
      for map_range <- map,
          reduce: {[], inputs} do
        {cur_mapped, cur_unmapped} ->
          {new_mapped, new_unmapped} =
            for range <- cur_unmapped,
                reduce: {[], []} do
              {mapped_acc, unmapped_acc} ->
                case overlap_and_map.(map_range, range) do
                  {:disjoint, range} ->
                    {mapped_acc, unmapped_acc ++ [range]}

                  {:intersecting, mapped, unmapped} ->
                    {mapped_acc ++ [mapped], unmapped_acc ++ unmapped}
                end
            end

          {cur_mapped ++ new_mapped, new_unmapped}
      end
      |> Tuple.to_list()
      |> List.flatten()
  end
end

seed_ranges
|> Enum.map(to_locations)
|> List.flatten()
|> Enum.map(&amp;elem(&amp;1, 0))
|> Enum.min()