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

AoC 2023 Day 5

2023/day5.livemd

AoC 2023 Day 5

Mix.install([
  {:kino_aoc, "~> 0.1.5"},
  {:nimble_parsec, "~> 1.4"}
])

Input

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2023", "5", System.fetch_env!("LB_AOC_COOKIE_SECRET"))
# puzzle_input = """
# seeds: 79 14 55 13

# seed-to-soil map:
# 50 98 2
# 52 50 48

# soil-to-fertilizer map:
# 0 15 37
# 37 52 2
# 39 0 15

# fertilizer-to-water map:
# 49 53 8
# 0 11 42
# 42 0 7
# 57 7 4

# water-to-light map:
# 88 18 7
# 18 25 70

# light-to-temperature map:
# 45 77 23
# 81 45 19
# 68 64 13

# temperature-to-humidity map:
# 0 69 1
# 1 0 69

# humidity-to-location map:
# 60 56 37
# 56 93 4
# """ |> String.trim()

Parsing

defmodule Almanac.Parser do
  import NimbleParsec

  ignore_literal = fn text ->
    text |> string() |> ignore()
  end

  tagged_word = fn t ->
    ascii_string([?a..?z], min: 1) |> tag(t)
  end

  newline = ignore(string("\n"))
  space = ignore(string(" "))
  number = integer(min: 1)

  seeds =
    ignore_literal.("seeds:")
    |> times(concat(space, number), min: 1)
    |> tag(:seeds)

  map_header =
    tagged_word.(:from)
    |> concat(ignore_literal.("-to-"))
    |> concat(tagged_word.(:to))
    |> concat(ignore_literal.(" map:"))
    |> concat(newline)
    |> tag(:header)

  map_line =
    concat(number, optional(space))
    |> times(3)
    |> concat(optional(newline))
    |> tag(:line)

  map =
    map_header
    |> repeat(map_line)
    |> optional(newline)
    |> tag(:map)

  almanac =
    seeds
    |> times(newline, 2)
    |> repeat(map)

  defparsec(:parse, almanac)
end
defmodule Almanac do
  defstruct [:seeds, :mappings]

  def parse(input) do
    {:ok, parsed, "", _, _, _} = Almanac.Parser.parse(input)

    mappings =
      parsed
      |> Keyword.get_values(:map)
      |> Enum.map(&parse_mapping/1)

    %Almanac{
      seeds: parsed[:seeds],
      mappings: mappings
    }
  end

  defp parse_mapping(mapping) do
    [from: [from], to: [to]] = mapping[:header]

    ranges =
      Keyword.get_values(mapping, :line)
      |> Enum.map(fn [dest, src, length] ->
        range = src..(src + length - 1)
        offset = dest - src

        %{
          range: range,
          offset: offset
        }
      end)

    %{
      from: from,
      to: to,
      ranges: ranges
    }
  end

  def map_number(ranges, number) do
    ranges
    |> Enum.find(fn %{range: range} -> number in range end)
    |> case do
      nil -> number
      %{offset: offset} -> number + offset
    end
  end

  def next_mapping(mappings, from) do
    Enum.find(mappings, &match?(%{from: ^from}, &1))
  end

  def resolve(%Almanac{seeds: seeds, mappings: mappings}) do
    resolve("seed", seeds, mappings)
  end

  def resolve("location", numbers, _), do: numbers

  def resolve(from, numbers, mappings) do
    mapping = next_mapping(mappings, from)
    new_numbers = Enum.map(numbers, &map_number(mapping.ranges, &1))
    resolve(mapping.to, new_numbers, mappings)
  end
end
parsed = Almanac.parse(puzzle_input)

Part 1

Almanac.resolve(parsed) |> Enum.min()

Part 2

resolve_range = fn range, almanac ->
  range
  |> Stream.map(fn n ->
    [res] = Almanac.resolve("seed", [n], almanac.mappings)
    res
  end)
  |> Enum.min()
end

Just bruteforce across all cores. This took 2.5 hours to finish on my system :)

parsed.seeds
|> Stream.chunk_every(2)
|> Stream.map(fn [f, t] -> Range.new(f, f + t - 1) end)
|> Task.async_stream(&resolve_range.(&1, parsed), ordered: false, timeout: :infinity)
|> Stream.map(fn {:ok, n} -> n end)
|> Enum.min()