#!/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 <>, 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()