Day 12
Part 1
# input = File.stream!("12/example.txt")
input = File.stream!("12/input.txt")
# input = "EEEEE
# EXXXX
# EEEEE
# EXXXX
# EEEEE" |> String.split()
defmodule U do
def parse(input) do
{_, map} = Enum.reduce(input, {0, %{}}, fn line, {row, map} ->
{_, map} =
Enum.reduce(String.codepoints(line), {0, map}, fn
"\n", acc ->
acc
char, {col, map} ->
pos = {col, row}
{col + 1, Map.put(map, pos, char)}
end)
{row + 1, map}
end)
map
end
def neighbors({x, y}) do
[{x + 1, y}, {x, y + 1}, {x - 1, y}, {x, y - 1}]
end
end
map = U.parse(input)
defmodule P1 do
def find_region(_, [], _, _, _) do
make_ref()
end
def find_region(char, [pos | tail], map, map_new, visited) do
found = map_new[pos]
if found do
{_, region} = found
region
else
neighbors =
Enum.filter(U.neighbors(pos), fn pos ->
map[pos] == char && !MapSet.member?(visited, pos)
end)
find_region(char, neighbors ++ tail, map, map_new, MapSet.put(visited, pos))
end
end
def to_regions(map) do
Enum.reduce(Enum.sort(map), %{}, fn {pos, char}, map_new ->
region = find_region(char, [pos], map, map_new, MapSet.new())
Map.put(map_new, pos, {char, region})
end)
end
def by_region(map) do
Enum.reduce(map, %{}, fn {pos, {char, region}}, regions ->
per =
Enum.reduce(U.neighbors(pos), 0, fn pos, acc ->
case map[pos] do
{_, ^region} -> acc
_ -> acc + 1
end
end)
Map.update(regions, {char, region}, {1, per}, fn {area, reg_per} ->
{area + 1, reg_per + per}
end)
end)
end
def calc(regions) do
Enum.reduce(regions, 0, fn {_, {area, per}}, acc ->
acc + area * per
end)
end
end
P1.to_regions(map) |> P1.by_region() |> P1.calc()
Part 2
defmodule P2 do
def sides({x, y}) do
[{x + 1, y, :r}, {x, y + 1, :b}, {x - 1, y, :l}, {x, y - 1, :t}]
end
def visit_direction(visited, {x, y}, dir, side, positions) do
visited = MapSet.put(visited, {x, y, side})
side_pos =
case side do
:r -> {x + 1, y}
:b -> {x, y + 1}
:l -> {x - 1, y}
:t -> {x, y - 1}
end
if MapSet.member?(positions, side_pos) do
visited
else
next =
case dir do
:r -> {x + 1, y}
:b -> {x, y + 1}
:l -> {x - 1, y}
:t -> {x, y - 1}
end
if MapSet.member?(positions, next) do
visit_direction(visited, next, dir, side, positions)
else
visited
end
end
end
def calc_per({x, y} = pos, positions, visited) do
Enum.reduce(sides(pos), {0, visited}, fn {next_x, next_y, side}, {per, visited} ->
cond do
MapSet.member?(positions, {next_x, next_y}) ->
{per, visited}
MapSet.member?(visited, {x, y, side}) ->
{per, visited}
true ->
{per + 1,
if side == :t || side == :b do
visited
|> visit_direction(pos, :r, side, positions)
|> visit_direction(pos, :l, side, positions)
else
visited
|> visit_direction(pos, :t, side, positions)
|> visit_direction(pos, :b, side, positions)
end}
end
end)
end
def by_region(map) do
by_region = Enum.group_by(map, fn {_, reg} -> reg end, fn {pos, _} -> pos end)
Enum.reduce(by_region, %{}, fn {region, positions}, regions ->
positions = MapSet.new(positions)
{per, _} =
Enum.reduce(positions, {0, MapSet.new()}, fn pos, {acc, visited} ->
{per, visited} = calc_per(pos, positions, visited)
{acc + per, visited}
end)
Map.put(regions, region, {Enum.count(positions), per})
end)
end
end
P1.to_regions(map) |> P2.by_region() |> P1.calc()
Part 2. Corners counting
defmodule P2_Corners do
def count_corners(map, {x, y} = pos) do
cur_region = map[pos]
[r, rb, b, bl, l, lt, t, tr] =
[
{x + 1, y},
{x + 1, y + 1},
{x, y + 1},
{x - 1, y + 1},
{x - 1, y},
{x - 1, y - 1},
{x, y - 1},
{x + 1, y - 1}
]
|> Enum.map(fn next_pos -> map[next_pos] == cur_region end)
outer_corners = Enum.count([{r, b}, {b, l}, {l, t}, {t, r}], fn x -> x == {false, false} end)
inner_corners =
Enum.count([{r, b, rb}, {b, l, bl}, {l, t, lt}, {t, r, tr}], fn x ->
x == {true, true, false}
end)
outer_corners + inner_corners
end
def by_region(map) do
Enum.reduce(map, %{}, fn {pos, region}, regions ->
corners = count_corners(map, pos)
Map.update(regions, region, {1, corners}, fn {count, reg_corners} ->
{count + 1, reg_corners + corners}
end)
end)
end
end
P1.to_regions(map) |> P2_Corners.by_region() |> P1.calc()