Advent of Code 2023 - Day 20
Mix.install([
{:req, "~> 0.4.0"},
{:kino, "~> 0.12.0"}
])
Input
opts = [headers: [{"cookie", "session=#{System.fetch_env!("LB_AOC_SESSION")}"}]]
puzzle_input = Req.get!("https://adventofcode.com/2023/day/20/input", opts).body
import String, only: [to_atom: 1]
parents = fn machine, child ->
machine
|> Enum.filter(fn {_, module} ->
module.destinations
|> Enum.member?(child)
end)
end
init_state = fn
_m, _n, :flipflop -> false
m, n, :conjunction -> for {_, %{name: n}} <- parents.(m, n), into: %{}, do: {n, 0}
_m, _n, _t -> :ok
end
input =
puzzle_input
|> String.split("\n", trim: true)
|> Enum.map(&String.split(&1, [" -> ", ", "]))
|> Enum.map(fn
["broadcaster" | d] -> {:broadcaster, :broadcaster, Enum.map(d, &to_atom/1)}
["%" <> name | d] -> {to_atom(name), :flipflop, Enum.map(d, &to_atom/1)}
["&" <> name | d] -> {to_atom(name), :conjunction, Enum.map(d, &to_atom/1)}
end)
|> Kernel.++([
{:button, :button, [:broadcaster]}
])
|> Enum.map(fn {name, type, destinations} ->
{name, %{name: name, type: type, destinations: destinations, state: nil, pulses: []}}
end)
|> Enum.into(%{})
|> then(fn machine ->
machine
|> Enum.reduce(machine, fn {name, %{type: type}}, acc ->
update_in(acc[name][:state], fn _ -> init_state.(acc, name, type) end)
end)
end)
for {from, %{type: type, destinations: destinations}} <- input,
to <- destinations,
reduce: "graph LR;\n" do
acc ->
case type do
:broadcaster -> acc <> "#{from}((#{from})) --> #{to}\n"
:conjunction -> acc <> "#{from}{#{from}} --> #{to}\n"
_type -> acc <> "#{from} --> #{to}\n"
end
end
|> Kino.Mermaid.new()
defmodule Machine do
def aptly(machine) do
q = fifo_push({:button, 0, :broadcaster})
work({q, machine})
end
defp work({q, machine}) do
case fifo_pop(q) do
{{:value, {from, pulse, to}}, rest} -> do_work(rest, pulse, from, to, machine)
{:empty, _} -> machine
end
end
defp do_work(q, pulse, from, to, machine) do
machine = update_in(machine, [from, :pulses], fn x -> [pulse | x] end)
pulses(q, pulse, machine[from], machine[to], machine)
|> work()
end
defp pulses(q, _pulse, _from, nil, machine), do: {q, machine}
defp pulses(q, pulse, _from, %{type: :broadcaster} = to, machine) do
%{name: name, destinations: destinations} = to
do_pulses(q, pulse, name, destinations, machine)
end
defp pulses(q, 1, _from, %{type: :flipflop} = _to, machine), do: {q, machine}
defp pulses(q, 0, _from, %{type: :flipflop} = to, machine) do
%{name: name, destinations: destinations, state: state} = to
machine = update_in(machine, [name, :state], fn x -> not x end)
next = if state, do: 0, else: 1
do_pulses(q, next, name, destinations, machine)
end
defp pulses(q, pulse, from, %{type: :conjunction} = to, machine) do
%{name: sender} = from
%{name: name, destinations: destinations} = to
machine = update_in(machine, [name, :state, sender], fn _ -> pulse end)
next = if Enum.all?(machine[name][:state], &(elem(&1, 1) == 1)), do: 0, else: 1
do_pulses(q, next, name, destinations, machine)
end
defp do_pulses(q, pulse, name, destinations, machine) do
destinations
|> Enum.reduce(q, fn d, acc ->
fifo_push(acc, {name, pulse, d})
end)
|> then(fn newq -> {newq, machine} end)
end
defp fifo_push(q \\ :queue.new(), item), do: :queue.in(item, q)
defp fifo_pop(q), do: :queue.out(q)
end
Part 1
1..1000
|> Enum.reduce(input, fn _i, acc -> Machine.aptly(acc) end)
|> Stream.flat_map(fn {_, %{pulses: pulses}} -> pulses end)
|> Enum.frequencies()
|> Map.values()
|> Enum.product()
Part 2
init_highs =
for {parent, _} <- parents.(input, :rx),
{gparent, _} <- parents.(input, parent),
into: %{},
do: {gparent, nil}
update_highs = fn machine, highs, i ->
highs
|> Enum.reduce(highs, fn {name, _}, acc ->
Map.update!(acc, name, fn
nil -> if Enum.member?(machine[name][:pulses], 1), do: i, else: nil
value -> value
end)
end)
end
find_highs = fn i, {acc_machine, acc_highs} ->
if Enum.any?(acc_highs, fn {_name, index} -> is_nil(index) end) do
{:cont, {Machine.aptly(acc_machine), update_highs.(acc_machine, acc_highs, i)}}
else
{:halt, acc_highs}
end
end
Stream.unfold(0, fn x -> {x, x + 1} end)
|> Enum.reduce_while({input, init_highs}, find_highs)
|> Map.values()
|> Enum.reduce(&div(&1 * &2, Integer.gcd(&1, &2)))