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

Day 5: If You Give A Seed A Fertilizer – Advent of Code 2023

2023/05.livemd

Day 5: If You Give A Seed A Fertilizer – Advent of Code 2023

input =
  File.stream!("/Users/pw/src/weiland/adventofcode/2023/input/05.txt")
  |> Enum.join("")

# |> Stream.map(&String.trim/1)

test_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"

Parsing

parse_from_stream = fn raw_almanac ->
  seeds =
    raw_almanac
    |> Stream.take(1)
    |> Enum.to_list()
    |> Enum.at(0)
    |> then(fn "seeds: " <> seeds -> seeds end)
    |> String.split(" ")
    |> Enum.map(&amp;String.to_integer/1)

  maps =
    raw_almanac
    # remove seeds
    |> Stream.drop(1)
    |> Stream.reject(&amp;(&amp;1 == ""))

  %{seeds: seeds, maps: maps}
end

parse_string = fn raw_almanac ->
  ["seeds: " <> seeds | maps] = String.split(raw_almanac, "\n\n")

  maps =
    maps
    |> Enum.map(&amp;String.split(&amp;1, "\n"))
    |> Enum.map(fn [map | numbers] ->
      [[_match, from, to]] = Regex.scan(~r/(.*)-to-(.*) map:/, map)

      ins =
        numbers
        |> Enum.reject(fn l -> l == "" end)
        |> Enum.map(fn l -> String.split(l, " ") |> Enum.map(&amp;String.to_integer/1) end)

      [from, to, ins]
    end)

  %{seeds: String.split(seeds, " ") |> Enum.map(&amp;String.to_integer/1), maps: maps}
end

Part One

find_min_location = fn %{seeds: seeds, maps: maps} ->
  Enum.reduce(maps, seeds, fn [_s, _d, map], acc ->
    Enum.map(acc, fn seed ->
      Enum.reduce_while(map, seed, fn [d, s, l], _acc ->
        if s <= seed &amp;&amp; seed < s + l do
          {:halt, d + (seed - s)}
        else
          {:cont, seed}
        end
      end)
    end)
  end)
  |> Enum.min()
end

part_one = fn input ->
  input
  |> parse_string.()
  |> find_min_location.()
end

IO.inspect(part_one.(test_input) == 35, label: "Smoke test part one")

part_one.(input)

Part Two

res = fn %{seeds: seeds, maps: blocks} ->
  map_matrix =
    blocks
    |> Enum.map(fn [_, _, maps] ->
      Enum.map(maps, fn [d, s, l] ->
        %{
          destination_start: d,
          destination_end: d + l - 1,
          source_start: s,
          source_end: s + l - 1
        }
      end)
    end)

  map_matrix
  |> Enum.with_index()
  |> Enum.map(fn {maps, i} ->
    maps
    |> Enum.map(fn m ->
      Enum.slice(map_matrix, 0, i + 1)
      # reduce right
      |> Enum.reverse()
      |> Enum.reduce(m.destination_start, fn maps, acc ->
        n = Enum.find(maps, fn m -> acc >= m.destination_start &amp;&amp; acc <= m.destination_end end)
        if n, do: n.source_start + (acc - n.destination_start), else: acc
      end)
    end)
  end)
  |> List.flatten()
  |> Enum.filter(fn seed ->
    seeds
    |> Enum.chunk_every(2)
    |> Enum.any?(fn [s, len] -> seed >= s &amp;&amp; seed < s + len end)
  end)
  |> Enum.map(fn seed ->
    map_matrix
    |> Enum.reduce(seed, fn maps, acc ->
      m = Enum.find(maps, fn m -> acc >= m.source_start &amp;&amp; acc <= m.source_end end)
      if m, do: m.destination_start + (acc - m.source_start), else: acc
    end)
  end)
  |> Enum.min()
end

part_two = fn input ->
  input
  |> parse_string.()
  |> res.()
end

IO.inspect(part_two.(test_input) == 46, label: "Smoke test part one")
part_two.(input)