176 lines
5.2 KiB
Elixir
Executable file
176 lines
5.2 KiB
Elixir
Executable file
#!/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()
|