I feel like there were probably some really clever ways to approach this problem but my solution is just kind of basic. Still nice for it to feel easy after all my false starts with day 7.
defmodule Day8 do
@test """
............
........0...
.....0......
.......0....
....0.......
......A.....
............
............
........A...
.........A..
............
............
"""
@real File.read!(__DIR__ <> "/input.txt")
def run(mode) do
{antennae, max_row, max_col} =
mode
|> input()
|> parse()
part_1(antennae, max_row, max_col) |> IO.inspect(label: :part1)
part_2(antennae, max_row, max_col) |> IO.inspect(label: :part2)
end
defp input(:test), do: @test
defp input(:real), do: @real
defp input(_), do: raise("Please use :test or :real as the mode to run.")
defp parse(data) do
data
|> grid()
end
defp grid(data) do
data
|> lines()
|> Enum.with_index()
|> Enum.reduce({%{}, 0, 0}, fn {row, r}, {map, max_r, max_c} ->
row
|> String.graphemes()
|> Enum.with_index()
|> Enum.reduce({map, max_r, max_c}, fn
{".", c}, {m, m_r, m_c} ->
{m, max(m_r, r), max(m_c, c)}
{ant, c}, {m, m_r, m_c} ->
{Map.update(m, ant, [{r, c}], fn curr -> [{r, c} | curr] end), max(m_r, r), max(m_c, c)}
end)
end)
end
defp lines(data) do
data
|> String.split("\n", trim: true)
end
defp part_1(antennae, max_row, max_col) do
antennae
|> Task.async_stream(fn {_freq, ants} ->
antinodes(ants, [], max_row, max_col)
end)
|> Enum.map(&elem(&1, 1))
|> Enum.reduce(MapSet.new(), fn nodes, ms ->
nodes |> Enum.reduce(ms, fn n, m -> MapSet.put(m, n) end)
end)
|> MapSet.size()
end
defp antinodes([_], antinodes, _, _), do: antinodes
defp antinodes([a, b | rest], antinodes, max_row, max_col) do
antinodes([a | rest], antinodes, max_row, max_col) ++
antinodes([b | rest], antinodes, max_row, max_col) ++ find_antinodes(a, b, max_row, max_col)
end
defp find_antinodes({a, b}, {c, d}, max_r, max_c) do
d_r = a - c
d_c = b - d
{i, j} = {a + d_r, b + d_c}
{k, l} = {c - d_r, d - d_c}
[{i, j}, {k, l}]
|> Enum.filter(fn {x, y} -> x <= max_r and y <= max_c and x >= 0 and y >= 0 end)
end
defp part_2(antennae, max_row, max_col) do
antennae
|> Task.async_stream(fn {_freq, ants} -> harmonic_antinodes(ants, max_row, max_col) end)
|> Enum.map(&elem(&1, 1))
|> Enum.reduce(MapSet.new(), fn nodes, ms ->
nodes |> Enum.reduce(ms, fn n, m -> MapSet.put(m, n) end)
end)
|> MapSet.size()
end
defp harmonic_antinodes(ants, max_r, max_c) do
for ant1 <- ants,
ant2 <- ants -- [ant1],
reduce: [] do
acc ->
acc ++ find_harmonics(ant1, ant2, max_r, max_c)
end
end
defp find_harmonics({a, b}, {c, d}, max_r, max_c) do
for i <- 0..max_r,
j <- 0..max_c,
slope({a, b}, {c, d}) == slope({a, b}, {i, j}),
reduce: [{a, b}, {c, d}] do
acc ->
[{i, j} | acc]
end
end
defp slope({_a, b}, {_c, b}), do: :infinity
defp slope({a, b}, {c, d}) do
(a - c) / (b - d)
end
end