Day 20
Mix.install([
{:req, "~> 0.4.5"},
{:kino, "~> 0.11.3"}
])
Input
input =
Req.get!(
"https://adventofcode.com/2023/day/20/input",
headers: [{"Cookie", ~s"session=#{System.fetch_env!("LB_AOC_SESSION")}"}]
).body
Kino.Text.new(input, terminal: true)
Part 1
defmodule Part1 do
def parse(input) do
for line <- String.split(input, "\n", trim: true),
reduce: {%{}, %{}} do
{inputs, outputs} ->
[input, output] = String.split(line, " -> ")
output = String.split(output, ", ")
{type, input} =
case input do
"broadcaster" -> {:broadcast, input}
<<"%", name::binary>> -> {:flip_flop, name}
<<"&", name::binary>> -> {:conjunction, name}
end
inputs =
output
|> Enum.reduce(inputs, fn output, inputs ->
Map.update(inputs, output, [input], fn existing -> [input | existing] end)
end)
outputs = Map.put(outputs, input, {type, output})
{inputs, outputs}
end
end
def graph(inputs, outputs, state \\ %{}) do
outputs = Map.put(outputs, "button", {:button, ["broadcaster"]})
graph(inputs, outputs, state, ["button"], MapSet.new(), [])
end
def graph(_, outputs, state, [], _, edges) do
meta =
Enum.map(outputs, fn {sender, {type, _}} ->
" #{sender}[\"#{sender} (#{type})\"]"
end)
styles =
Enum.flat_map(outputs, fn {name, {type, _}} ->
case type do
:flip_flop ->
internal = Map.get(state, name, 0)
fill = if internal == 1, do: "#0f0", else: "#f00"
[" style #{name} fill:#{fill}"]
_ ->
[]
end
end)
lines = ["graph LR"] ++ meta ++ edges ++ styles
graph =
lines
|> Enum.uniq()
|> Enum.join("\n")
Kino.Mermaid.new(graph)
end
def graph(inputs, outputs, state, [sender | rest], seen, acc) do
{_, receivers} = Map.get(outputs, sender, {nil, []})
seen = MapSet.put(seen, sender)
edges =
receivers
|> Enum.map(fn receiver ->
" #{sender} --> #{receiver}"
end)
next =
receivers
|> Enum.reject(fn receiver -> MapSet.member?(seen, receiver) end)
acc = acc ++ edges
rest = rest ++ next
graph(inputs, outputs, state, rest, seen, acc)
end
def run(inputs, outputs, opts \\ []) do
initial = Keyword.get(opts, :initial, %{})
debug = Keyword.get(opts, :debug, false)
render = Keyword.get(opts, :render, false)
frame = if render, do: Kino.Frame.new() |> Kino.render(), else: nil
run(%{
inputs: inputs,
outputs: outputs,
state: initial,
debug: debug,
frame: frame,
lows: 1,
highs: 0,
pulses: [{"button", "broadcaster", 0}]
})
end
def run(%{
inputs: inputs,
outputs: outputs,
state: state,
lows: lows,
highs: highs,
frame: frame,
pulses: []
}) do
if frame do
term = graph(inputs, outputs, state)
Kino.Frame.render(frame, term)
end
{lows, highs, state}
end
def run(
%{
inputs: inputs,
outputs: outputs,
state: state,
debug: debug,
lows: lows,
highs: highs,
pulses: [{sender, receiver, pulse} | rest]
} = proc
) do
{type, receivers} = Map.get(outputs, receiver, {nil, []})
{pulse, state} =
recv(%{
inputs: inputs,
sender: sender,
receiver: receiver,
pulse: pulse,
type: type,
state: state
})
{lows, highs, pulses} =
case pulse do
nil ->
{lows, highs, []}
0 ->
{lows + length(receivers), highs, Enum.map(receivers, &{receiver, &1, 0})}
1 ->
{lows, highs + length(receivers), Enum.map(receivers, &{receiver, &1, 1})}
end
if debug do
for {sender, receiver, pulse} <- pulses do
pulse = if pulse == 0, do: "low", else: "high"
IO.puts("#{sender} -#{pulse}-> #{receiver}")
end
end
run(%{proc | state: state, pulses: rest ++ pulses, lows: lows, highs: highs})
end
def recv(%{type: nil, receiver: receiver, pulse: pulse, state: state}),
do: {nil, Map.put(state, receiver, pulse)}
def recv(%{
type: :broadcast,
pulse: pulse,
state: state
}),
do: {pulse, state}
def recv(%{type: :flip_flop, pulse: 1, state: state}), do: {nil, state}
def recv(%{
type: :flip_flop,
receiver: receiver,
pulse: 0,
state: state
}) do
prev = Map.get(state, receiver, 0)
next = 1 - prev
state = Map.put(state, receiver, next)
{next, state}
end
def recv(%{
type: :conjunction,
inputs: inputs,
sender: sender,
receiver: receiver,
pulse: pulse,
state: state
}) do
prev = Map.get(state, receiver, %{})
next = Map.put(prev, sender, pulse)
pulse = if Enum.all?(inputs[receiver], fn input -> next[input] == 1 end), do: 0, else: 1
state = Map.put(state, receiver, next)
{pulse, state}
end
end
{inputs, outputs} = Part1.parse(input)
{lows, highs, _} =
for _ <- 1..1000, reduce: {0, 0, %{}} do
{lows, highs, state} ->
{new_lows, new_highs, state} = Part1.run(inputs, outputs, initial: state)
{lows + new_lows, highs + new_highs, state}
end
lows * highs
Part 2
Part1.graph(inputs, outputs)
state =
for _ <- 1..0//1, reduce: %{} do
state ->
{_, _, state} = Part1.run(inputs, outputs, initial: state)
state
end
{_, _, state} = Part1.run(inputs, outputs, initial: state, render: true)