I probably over complicated parsing the input, but it worked really well. One thing that got highlighted (for me again) was that the :ets
module is kind of non-ergonomic. Like there’s no update_or_insert
or insert_new
type of functions. I was feeling pretty clever about how easy it was to add the smudge factor to part 1 and make it reusable.
defmodule Day13 do
@moduledoc """
Day13 AoC Solutions
"""
alias AocToolbox.Input
def solve(1, mode) do
__MODULE__.Part1.solve(input(mode))
end
def solve(2, mode) do
__MODULE__.Part2.solve(input(mode))
end
defmodule Part1 do
def solve(input, smudges \\ 0) do
:ets.new(:grids, [:set, :protected, :named_table])
solution =
input
|> parse()
|> grids_to_ets()
|> find_symmetry(smudges)
:ets.delete(:grids)
solution
end
def parse(input) do
input
|> String.split("\n\n")
|> Enum.map(&Input.lines/1)
|> Enum.with_index()
|> Enum.flat_map(&stream_to_grid/1)
end
defp stream_to_grid({stream, grid}) do
stream
|> Enum.with_index()
|> Enum.flat_map(fn {line, row} ->
line
|> String.graphemes()
|> Enum.with_index()
|> Enum.map(fn {g, col} -> {{grid, row, col}, g} end)
end)
end
defp grids_to_ets(grids) do
for {{g, r, c}, v} <- grids, reduce: 0 do
gg ->
:ets.insert(:grids, {{g, r, c}, v})
[{:max_row, r}, {:max_col, c}]
|> Enum.each(fn {max, i} ->
if :ets.member(:grids, {g, max}) do
:ets.update_element(
:grids,
{g, max},
{2, max(i, :ets.lookup_element(:grids, {g, max}, 2))}
)
else
:ets.insert(:grids, {{g, max}, i})
end
end)
max(g, gg)
end
end
defp find_symmetry(max_grid, smudges) do
0..max_grid
|> Task.async_stream(fn g ->
max_row = :ets.lookup_element(:grids, {g, :max_row}, 2)
max_col = :ets.lookup_element(:grids, {g, :max_col}, 2)
grid = get_grid(g, max_row, max_col)
Enum.find_value([{1, max_row}, {2, max_col}], fn {orientation, max} ->
find_axis(grid, max, orientation, smudges)
end)
end)
|> Enum.reduce({0, 0}, fn {:ok, {axis, n}}, {verts, horizs} ->
case axis do
:vertical -> {verts + n, horizs}
:horizontal -> {verts, horizs + n * 100}
end
end)
|> Tuple.to_list()
|> Enum.sum()
end
defp get_grid(grid, max_row, max_col) do
for r <- 0..max_row,
c <- 0..max_col do
:ets.lookup(:grids, {grid, r, c})
end
end
defp find_axis(grid, max, orientation, smudges) when orientation in [1, 2] do
grid
|> Enum.group_by(fn [{k, _v}] ->
elem(k, orientation)
end)
|> do_find_axis(max, 1, elem({:horizontal, :vertical}, orientation - 1), smudges)
end
defp do_find_axis(rows_or_cols, max, maybe_axis \\ 1, orientation, smudges)
defp do_find_axis(_rows_or_cols, max, maybe_axis, _orientation, _smudges)
when max < maybe_axis,
do: false
defp do_find_axis(rows_or_cols, max, maybe_axis, orientation, smudges) do
case 0..(maybe_axis - 1)
|> Enum.map(fn i -> {i, 2 * maybe_axis - 1 - i} end)
|> Enum.filter(fn {l, r} -> r <= max end)
|> Enum.reduce_while(smudges, fn {l, r}, acc ->
case acc - count_diffs(l, r, rows_or_cols, orientation) do
n when n < 0 -> {:halt, false}
n -> {:cont, n}
end
end) do
0 ->
{orientation, maybe_axis}
_ ->
do_find_axis(rows_or_cols, max, maybe_axis + 1, orientation, smudges)
end
end
defp count_diffs(l, r, groups, orientation) do
[l, r] =
[l, r]
|> Enum.map(fn i ->
groups[i]
|> Enum.map(fn [{k, v}] ->
case orientation do
:horizontal -> {elem(k, 2), v}
:vertical -> {elem(k, 1), v}
end
end)
end)
|> Enum.map(fn elems -> MapSet.new(elems) end)
MapSet.difference(l, r) |> MapSet.size()
end
end
defmodule Part2 do
def solve(input) do
input
|> Part1.solve(1)
end
end
end
One of my favorite things about AoC is seeing the various different approaches. What I’ve learned is that the way the data gets structured really dictates how you are able to approach the logic. Often when I’m trying to understand the solutions from others I find I’m struggling because I set up the data so differently that I can’t shift my head around to the flow of the data through their solution.