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

Advent of Code - Day 5

elixir-livebook/day5.livemd

Advent of Code - Day 5

Mix.install([
  {:kino_aoc, "~> 0.1"},
  # {:benchee, "~> 1.0", only: :dev}
  {:kino_benchee, github: "akoutmos/kino_benchee", branch: "initial_release_prep"}
])

Introduction

Puzzle

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2023", "5", System.fetch_env!("LB_AOC_SESSION"))

Parser

Code - Parser

defmodule Parser do
  def parse(input) do
    [seeds | maps] =
      input
      |> String.split("\n\n")

    seeds = seeds |> String.split(["seeds: ", " "], trim: true) |> Enum.map(&String.to_integer/1)

    maps =
      maps
      |> Enum.map(fn map ->
        [name | pairs] =
          map
          |> String.split([" map:", "\n"], trim: true)

        {name,
         pairs
         |> Enum.map(fn pair ->
           pair
           |> String.split(" ", trim: true)
           |> Enum.map(&String.to_integer/1)
         end)}
      end)
      |> Enum.into(%{})

    %{seeds: seeds, maps: maps}
  end
end

Tests - Parser

ExUnit.start(autorun: false)

defmodule ParserTest do
  use ExUnit.Case, async: true
  import Parser

  @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
  """
  @expected %{
    maps: %{
      "fertilizer-to-water" => [{53..60, -4}, {11..52, -11}, {0..6, 42}, {7..10, 50}],
      "humidity-to-location" => [{56..92, 4}, {93..96, -37}],
      "light-to-temperature" => [{77..99, -32}, {45..63, 36}, {64..76, 4}],
      "seed-to-soil" => [{98..99, -48}, {50..97, 2}],
      "soil-to-fertilizer" => [{15..51, -15}, {52..53, -15}, {0..14, 39}],
      "temperature-to-humidity" => [{69..69, -69}, {0..68, 1}],
      "water-to-light" => [{18..24, 70}, {25..94, -7}]
    },
    seeds: [79, 14, 55, 13]
  }

  test "parse test" do
    actual = parse(@input)
    assert actual == @expected
  end
end

ExUnit.run()

Part One

Code - Part 1

defmodule PartOne do
  @progression [
    "seed-to-soil",
    "soil-to-fertilizer",
    "fertilizer-to-water",
    "water-to-light",
    "light-to-temperature",
    "temperature-to-humidity",
    "humidity-to-location"
  ]

  def solve(input) do
    IO.puts("--- Part One ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(input) do
    %{seeds: seeds, maps: maps} =
      input
      |> Parser.parse()

    seeds
    |> Enum.map(fn seed ->
      Enum.reduce(@progression, seed, fn step, acc ->
        Enum.find_value(maps[step], acc, fn [dest, source, len] ->
          if acc in source..(source + len - 1) do
            acc + dest - source
          else
            nil
          end
        end)
      end)
    end)
    |> Enum.min()
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

defmodule PartOneTest do
  use ExUnit.Case, async: true
  import PartOne

  @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
  """
  @expected 35

  test "part one" do
    actual = run(@input)
    assert actual == @expected
  end
end

ExUnit.run()

Solution - Part 1

PartOne.solve(puzzle_input)

Part Two

Code - Part 2

defmodule PartTwo do
  @progression [
    "seed-to-soil",
    "soil-to-fertilizer",
    "fertilizer-to-water",
    "water-to-light",
    "light-to-temperature",
    "temperature-to-humidity",
    "humidity-to-location"
  ]

  # random jump value
  @jump 19424

  def solve(input) do
    IO.puts("--- Part Two ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(input, jump \\ @jump) do
    %{seeds: seeds, maps: maps} =
      input
      |> Parser.parse()

    seed_ranges =
      seeds
      |> Enum.chunk_every(2)
      |> Enum.map(fn [a, b] -> a..(a + b - 1) end)

    # work backwards - starting with min location of 0 and 
    # iteratively jumping until we find a seed that contained within a of initial seed ranges
    # we have to remember this is likely not the smallest location for which we have a seed
    potential_match =
      Stream.iterate(0, &(&1 + jump))
      |> find_seed_from_location(seed_ranges, maps)

    # once we know we are working with a seed that is within a range
    # try and find the smallest location that has a seed contained in a range
    (potential_match - jump)
    |> Stream.iterate(&(&1 + 1))
    |> find_seed_from_location(seed_ranges, maps)
  end

  def find_seed_from_location(locations, seed_ranges, maps) do
    Enum.find(locations, fn location ->
      seed =
        @progression
        |> Enum.reverse()
        |> Enum.reduce(location, fn step, acc ->
          Enum.find_value(maps[step], acc, fn [dest, source, len] ->
            if acc in dest..(dest + len - 1) do
              acc - dest + source
            else
              nil
            end
          end)
        end)

      Enum.any?(seed_ranges, fn range -> seed in range end)
    end)
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

defmodule PartTwoTest do
  use ExUnit.Case, async: true
  import PartTwo

  @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
  """
  @expected 46

  test "part two" do
    actual = run(@input, 1)
    assert actual == @expected
  end
end

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)

Benchmarking

defmodule Benchmarks do
  def part1(input) do
    PartOne.run(input)
  end

  def part2(input) do
    PartTwo.run(input)
  end
end

Benchee.run(
  %{
    "day05.part1" => &Benchmarks.part1/1,
    "day05.part2" => &Benchmarks.part2/1
  },
  inputs: %{
    "puzzle" => puzzle_input
  },
  time: 1,
  memory_time: 1,
  reduction_time: 1
)