AdventOfCode/2024/day12.exs

177 lines
5.2 KiB
Elixir
Raw Normal View History

2024-12-12 13:53:17 +00:00
#!/usr/bin/env elixir
defmodule Day12 do
defmodule Region do
defstruct label: nil, plots: MapSet.new(), perimeter: 0
end
def part1({grid, size_x, size_y}) do
find_regions(grid, size_x, size_y)
|> Enum.map(fn region -> MapSet.size(region.plots) * region.perimeter end)
|> Enum.sum()
end
def find_regions(grid, size_x, size_y) do
for x <- 0..(size_x - 1), y <- 0..(size_y - 1), reduce: {[], MapSet.new()} do
{regions, seen} ->
case find_region(x, y, grid, seen) do
{:already_found, _seen} -> {regions, seen}
{region, seen} -> {[region | regions], seen}
end
end
|> elem(0)
end
def find_region(x, y, grid, seen, region \\ nil) do
if MapSet.member?(seen, {x, y}) do
{region || :already_found, seen}
else
region = region || %Region{label: Map.fetch!(grid, {x, y})}
seen = MapSet.put(seen, {x, y})
region_neighbours = get_region_neighbours(x, y, region.label, grid)
region = %Region{
region
| plots: MapSet.put(region.plots, {x, y}),
perimeter: region.perimeter + (4 - length(region_neighbours))
}
Enum.reduce(region_neighbours, {region, seen}, fn {x, y}, {region, seen} ->
find_region(x, y, grid, seen, region)
end)
end
end
def get_region_neighbours(x, y, label, grid) do
grid
|> Map.take([{x - 1, y}, {x + 1, y}, {x, y - 1}, {x, y + 1}])
|> Map.filter(&match?({_coord, ^label}, &1))
|> Map.keys()
end
def part2({grid, size_x, size_y}) do
find_regions(grid, size_x, size_y)
|> Enum.map(fn region -> MapSet.size(region.plots) * count_sides(region) end)
|> Enum.sum()
end
def count_sides(region) do
{min_x, _y} = Enum.min_by(region.plots, fn {x, _y} -> x end)
{_x, min_y} = Enum.min_by(region.plots, fn {_x, y} -> y end)
{max_x, _y} = Enum.max_by(region.plots, fn {x, _y} -> x end)
{_x, max_y} = Enum.max_by(region.plots, fn {_x, y} -> y end)
left =
Enum.reduce(min_x..max_x, 0, fn x, sides ->
Enum.reduce(min_y..max_y, {sides, false}, fn y, {sides, sequence?} ->
plot? = MapSet.member?(region.plots, {x, y})
open? = MapSet.member?(region.plots, {x - 1, y})
cond do
plot? and not open? and not sequence? -> {sides + 1, true}
plot? and not open? and sequence? -> {sides, true}
true -> {sides, false}
end
end)
|> elem(0)
end)
right =
Enum.reduce(min_x..max_x, 0, fn x, sides ->
Enum.reduce(min_y..max_y, {sides, false}, fn y, {sides, sequence?} ->
plot? = MapSet.member?(region.plots, {x, y})
open? = MapSet.member?(region.plots, {x + 1, y})
cond do
plot? and not open? and not sequence? -> {sides + 1, true}
plot? and not open? and sequence? -> {sides, true}
true -> {sides, false}
end
end)
|> elem(0)
end)
top =
Enum.reduce(min_y..max_y, 0, fn y, sides ->
Enum.reduce(min_x..max_x, {sides, false}, fn x, {sides, sequence?} ->
plot? = MapSet.member?(region.plots, {x, y})
open? = MapSet.member?(region.plots, {x, y - 1})
cond do
plot? and not open? and not sequence? -> {sides + 1, true}
plot? and not open? and sequence? -> {sides, true}
true -> {sides, false}
end
end)
|> elem(0)
end)
bottom =
Enum.reduce(min_y..max_y, 0, fn y, sides ->
Enum.reduce(min_x..max_x, {sides, false}, fn x, {sides, sequence?} ->
plot? = MapSet.member?(region.plots, {x, y})
open? = MapSet.member?(region.plots, {x, y + 1})
cond do
plot? and not open? and not sequence? -> {sides + 1, true}
plot? and not open? and sequence? -> {sides, true}
true -> {sides, false}
end
end)
|> elem(0)
end)
left + right + top + bottom
end
def input do
with [input_filename] <- System.argv(),
{:ok, input} <- File.read(input_filename) do
{grid, _, x, y} =
for <<char::binary-1 <- input>>, reduce: {%{}, 0, 0, 0} do
{grid, x, max_x, y} ->
case char do
"\n" -> {grid, 0, x, y + 1}
char -> {Map.put(grid, {x, y}, char), x + 1, max_x, y}
end
end
{grid, x, y}
else
_ -> :error
end
end
#######################
# HERE BE BOILERPLATE #
#######################
def run do
case input() do
:error -> print_usage()
input -> run_parts_with_timer(input)
end
end
defp run_parts_with_timer(input) do
run_with_timer(1, fn -> part1(input) end)
run_with_timer(2, fn -> part2(input) end)
end
defp run_with_timer(part, fun) do
{time, result} = :timer.tc(fun)
IO.puts("Part #{part} (completed in #{format_time(time)}):\n")
IO.puts("#{inspect(result)}\n")
end
defp format_time(μsec) when μsec < 1_000, do: "#{μsec}μs"
defp format_time(μsec) when μsec < 1_000_000, do: "#{μsec / 1000}ms"
defp format_time(μsec), do: "#{μsec / 1_000_000}s"
defp print_usage do
IO.puts("Usage: elixir dayDay12.exs input_filename")
end
end
Day12.run()