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

Day 5: If You Give A Seed A Fertilizer

2023/day_05.livemd

Day 5: If You Give A Seed A Fertilizer

Mix.install([:kino])

input = Kino.Input.textarea("Please paste your input:")

Part 1

Run in Livebook

https://adventofcode.com/2023/day/5

[seeds_str | maps_str] =
  input
  |> Kino.Input.read()
  |> String.split("\n\n", trim: true)

seeds =
  seeds_str
  |> String.trim_leading("seeds: ")
  |> String.split(" ")
  |> Enum.map(&String.trim/1)
  |> Enum.map(&String.to_integer/1)

maps =
  maps_str
  |> Enum.map(fn map_str ->
    [map_name | map_values_str] = map_str |> String.split("\n")

    %{"from" => from, "to" => to} =
      Regex.named_captures(~r/(?.+)-to-(?.+) map:/, map_name)

    map_values =
      map_values_str
      |> Enum.map(fn map_value_str ->
        map_value_str |> String.split(" ") |> Enum.map(&String.to_integer/1)
      end)

    %{from: from, to: to, map_values: map_values}
  end)

mappers =
  maps
  |> Enum.map(fn %{map_values: map_values} ->
    fn input ->
      map_values
      |> Enum.find_value(fn [destination_start, source_start, range_length] ->
        case input in source_start..(source_start + (range_length - 1)) do
          false -> nil
          true -> input + destination_start - source_start
        end
      end)
      |> then(fn
        nil -> input
        output -> output
      end)
    end
  end)

mappers
|> Enum.reduce(seeds, fn mapper, values -> values |> Enum.map(mapper) end)
|> Enum.min()

Part 2

https://adventofcode.com/2023/day/5#part2

# range: [start, end)

seed_ranges =
  seeds_str
  |> String.trim_leading("seeds: ")
  |> String.split(" ")
  |> Enum.map(&String.trim/1)
  |> Enum.map(&String.to_integer/1)
  |> Enum.chunk_every(2)
  |> Enum.map(fn [a, b] -> {a, a + b} end)

map_values_list =
  maps
  |> Enum.map(&(&1.map_values |> Enum.sort_by(fn [_, source_start, _] -> source_start end)))

map_values_list
|> Enum.reduce(seed_ranges, fn map_values, ranges ->
  new_ranges =
    ranges
    |> Enum.flat_map(fn range ->
      map_values
      |> Enum.reduce_while(
        {range, _new_ranges = []},
        fn [destination_start, source_start, range_length], {{first, last}, new_ranges} ->
          source_end = source_start + range_length
          map_diff = destination_start - source_start

          {ramain_range, new_ranges} =
            cond do
              # disjoint
              last <= source_start ->
                {nil, [{first, last} | new_ranges]}

              first >= source_end ->
                {{first, last}, new_ranges}

              # intersection
              first < source_start and last <= source_end ->
                {nil,
                 [
                   {first, source_start},
                   {source_start + map_diff, last + map_diff} | new_ranges
                 ]}

              first >= source_start and last >= source_end ->
                {{source_end, last}, [{first + map_diff, source_end + map_diff} | new_ranges]}

              # include
              first < source_start and last >= source_end ->
                {{source_end, last},
                 [
                   {first, source_start},
                   {source_start + map_diff, source_end + map_diff}
                   | new_ranges
                 ]}

              # included
              first >= source_start and last <= source_end ->
                {nil, [{first + map_diff, last + map_diff} | new_ranges]}
            end

          case {ramain_range, new_ranges} do
            {nil, new_ranges} -> {:halt, {nil, new_ranges}}
            {remain_range, new_ranges} -> {:cont, {remain_range, new_ranges}}
          end
        end
      )
      |> case do
        {nil, new_ranges} -> new_ranges
        {remain_range, new_ranges} -> [remain_range | new_ranges]
      end
    end)

  new_ranges
end)
|> Enum.min_by(fn {first, _} -> first end)
|> elem(0)