lud
Advent of Code 2023 - Day 20
This one lost me at the beginning, but after a little inspection of the input, there is a clear path to the answer.
Explainations in part_two/1
defmodule AdventOfCode.Y23.Day20 do
alias AoC.Input, warn: false
def read_file(file, _part) do
Input.stream!(file, trim: true)
end
def parse_input(input, _part) do
{modules, out_from} = Enum.map_reduce(input, _out_from = %{}, &parse_line/2)
# now for each inverter we need to initialize state will all its possible inputs
modules =
modules
|> Enum.map(fn
{key, {:conj, :uninitialized, outs}} ->
state = Map.new(Map.fetch!(out_from, key), fn k -> {k, 0} end)
{key, {:conj, state, outs}}
other ->
other
end)
|> Map.new()
{modules, out_from}
end
defp parse_line(line, out_from) do
[name, outs] = String.split(line, " -> ")
{name, kind, state} =
case name do
"broadcaster" -> {"broadcaster", :bcast, nil}
"%" <> name -> {name, :flip, :off}
"&" <> name -> {name, :conj, :uninitialized}
end
outs = String.split(outs, ", ")
out_from = Enum.reduce(outs, out_from, fn out, acc -> Map.update(acc, out, [name], &[name | &1]) end)
module = {name, {kind, state, outs}}
{module, out_from}
end
def part_one({modules, _}) do
{count_low, count_high, _} =
Enum.reduce(1..1000, {0, 0, modules}, fn _, {count_low, count_high, modules} ->
{count_low_add, count_high_add, modules} = push_button(modules)
{count_low + count_low_add, count_high + count_high_add, modules}
end)
count_low * count_high
end
def part_two({modules, out_from}) do
# modules =
# Map.new(modules, fn
# {key, {:flip, _, _}} -> {key, :flip}
# {key, {:conj, _, _}} -> {key, :conj}
# {key, {:bcast, _, _}} -> {key, :bcast}
# end)
# Rule:
#
# &jm -> rx
#
# For rx to receive a low pulse, &jm must remember a high pulse for all its
# inputs
#
# Then we have that:
#
# &sg -> jm
# &lm -> jm
# &dh -> jm
# &db -> jm
#
# So we need all of them to send a high input in the same cycle.
#
# The parents are those. Note that sg, lm, dh and db have each one a single
# input, so they are actually regular not gates, or "%" modules.
#
# &bc -> _, _, _, _, dh, _, _
# &bx -> _, _, db
# &qq -> lm, _, _, _, _, _, _
# &gj -> _, _, sg, _
#
# For sg, lm, dh and db to send a high pulse in the same time, we need bc,
# bx, qq and qj to send a low pulse in the same time.
#
# So we count how much cycles it takes for each one to send a low pulse, and
# the LCM of those cycle numbers is the answer.
#
# Though I have a feeling that the input is very tailored for that solution
# because any input would not guarantee that if bc, bx, qq and qj send a low
# pulse after N first cycles, they would acutally send a low pulse every
# other N cycles.
#
cyclics = Enum.flat_map(["rx"], &Map.fetch!(out_from, &1))
cyclics = Enum.flat_map(cyclics, &Map.fetch!(out_from, &1))
cyclics = Enum.flat_map(cyclics, &Map.fetch!(out_from, &1))
counts = count_cycles_until_low_pulse(modules, cyclics)
counts |> Map.values() |> Enum.reduce(fn a, b -> trunc(lcm(a, b)) end)
end
defp count_cycles_until_low_pulse(modules, watch_list) do
infinite_ints = Stream.iterate(1, &(&1 + 1))
cycle_counts = Map.new(watch_list, &{&1, false})
Enum.reduce(infinite_ints, {modules, cycle_counts}, fn i, {modules, cycle_counts} ->
# We cannot inspect the states after the button is pushed because the
# modules we are looking for are resetting before the modules are
# returned.
#
# So we need to inspect the emitted pulses and return from that.
{modules, cycle_counts} =
push_button(modules, cycle_counts, fn pulses, counts ->
Enum.reduce(pulses, counts, fn
{_, 1, _}, counts ->
counts
{from, 0, _}, counts ->
case Map.get(counts, from) do
false -> Map.put(counts, from, i)
_ -> counts
end
end)
end)
if Enum.all?(cycle_counts, fn {_, count} -> count end) do
throw({:counts, cycle_counts})
end
{modules, cycle_counts}
end)
catch
{:counts, counts} -> counts
end
defp push_button(modules) do
init_pulse = {"button", 0, "broadcaster"}
{_count_low, _count_high, _modules} = reduce([init_pulse], modules, 0, 0)
end
defp reduce([], modules, count_low, count_high) do
{count_low, count_high, modules}
end
defp reduce(pulses, modules, count_low, count_high) do
{count_low, count_high} = count_pulses(pulses, count_low, count_high)
{new_pulses, new_modules} =
Enum.flat_map_reduce(pulses, modules, fn {_, _, to} = p, modules ->
case Map.fetch(modules, to) do
{:ok, module} ->
{next_pulses, new_module} = handle_pulse(p, module)
modules = Map.put(modules, to, new_module)
{next_pulses, modules}
:error ->
{[], modules}
end
end)
reduce(new_pulses, new_modules, count_low, count_high)
end
defp push_button(modules, acc, f) do
init_pulse = {"button", 0, "broadcaster"}
{_modules, _acc} = run([init_pulse], modules, acc, f)
end
defp run([], modules, acc, _f) do
{modules, acc}
end
defp run(pulses, modules, acc, f) do
{new_pulses, new_modules} =
Enum.flat_map_reduce(pulses, modules, fn {_, _, to} = p, modules ->
case Map.fetch(modules, to) do
{:ok, module} ->
{next_pulses, new_module} = handle_pulse(p, module)
modules = Map.put(modules, to, new_module)
{next_pulses, modules}
:error ->
{[], modules}
end
end)
new_acc = f.(new_pulses, acc)
run(new_pulses, new_modules, new_acc, f)
end
defp handle_pulse({_, kind, me}, {:bcast, _, outs} = this) do
# There is a single broadcast module (named broadcaster). When it receives a
# pulse, it sends the same pulse to all of its destination modules.
sends = send_all(outs, me, kind)
{sends, this}
end
defp handle_pulse({_, 0, me}, {:flip, state, outs}) do
# if a flip-flop module receives a low pulse, it flips between on and off.
# If it was off, it turns on and sends a high pulse. If it was on, it turns
# off and sends a low pulse.
{new_state, send_kind} =
case state do
:off -> {:on, 1}
:on -> {:off, 0}
end
sends = send_all(outs, me, send_kind)
this = {:flip, new_state, outs}
{sends, this}
end
defp handle_pulse({_, 1, _}, {:flip, _, _} = this) do
# If a flip-flop module receives a high pulse, it is ignored and nothing
# happens.
{[], this}
end
defp handle_pulse({from, kind, me}, {:conj, state, outs}) do
# Conjunction modules (prefix &) remember the type of the most recent pulse
# received from each of their connected input modules; they initially
# default to remembering a low pulse for each input. When a pulse is
# received, the conjunction module first updates its memory for that input.
# Then, if it remembers high pulses for all inputs, it sends a low pulse;
# otherwise, it sends a high pulse.
state = Map.replace!(state, from, kind)
send_kind = if all_high?(state), do: 0, else: 1
sends = send_all(outs, me, send_kind)
this = {:conj, state, outs}
{sends, this}
end
defp all_high?(map) do
Enum.all?(map, fn
{_, 1} -> true
_ -> false
end)
end
defp send_all(outs, me, kind) do
Enum.map(outs, &{me, kind, &1})
end
defp count_pulses([{_, 0, _} | pulses], count_low, count_high) do
count_pulses(pulses, count_low + 1, count_high)
end
defp count_pulses([{_, 1, _} | pulses], count_low, count_high) do
count_pulses(pulses, count_low, count_high + 1)
end
defp count_pulses([], count_low, count_high) do
{count_low, count_high}
end
defp lcm(0, 0), do: 0
defp lcm(a, b), do: a * b / Integer.gcd(a, b)
end
Most Liked
bjorng
It took me a while to wrap my head abut the rules for the modules in part 1 and do the actual implementation.
For part 2 I suspected that it was possible to somehow divide-and-concur the problem, but I found that some modules were involved in when I tried to convert the input to a tree. It turns out that the cycles don’t matter, because the input can still be cleanly partitioned into separate parts, which each has a broadcaster with a single destination. By partitioning the input data into separate parts I could re-use most of my code from part 1 for part 2.
exists
Part 1 was essentially just parsing and implementing the instructions from the question.
I must say that my solution to part 2 is not quite a general solution, but depends on the shape of the input graph…
Spoilers
I used dot from graphviz to plot the input graph, and I noticed that there are 4 distinct components: branching directly from the broadcaster, and merging into a single “conjunction” vertex than then feeds into rx. Each of the components by themselves can be processed by just running the same thing as in part 1 - for me they produce the desired signal as their output in about ~3900 steps. Then the desired number is just the least common multiple of those (curiously, all 4 were primes in my case, so I could just multiply them).
Code
defmodule Main do
def run() do
get_input()
|> parse() |> add_sinks() |> calc_incoming()
# |> solve1()
# |> solve2()
|> mess_around()
end
def get_input() do
# "testinput20.1"
# "testinput20.2"
"input20"
|> File.read!() |> String.trim() |> String.split("\n")
end
# parse input into %{}, a "graph"
def parse(ls) do
for l <- ls, into: %{} do
[labl,tos] = String.split(l," -> ")
case labl |> String.to_charlist() |> Enum.at(0) do
?b -> {"B", {nil, tos |> String.split(", ")} }
c -> {String.slice(labl,1..-1), {c, tos |> String.split(", ")} }
end
end
end
# prep: for each "vertex", remember also the ones that point to it
def calc_incoming(gr) do
edges = for {l,{_,outedges}} <- gr, e <- outedges, into: [], do: {l,e}
for {l,{t,outs}} <- gr, into: %{}, do: {l,{t,outs, Enum.filter(edges,&(elem(&1,1)==l))|>Enum.map(&elem(&1,0))}}
end
# prep: add vertices that have no outgoing arrows
def add_sinks(gr) do
incs = gr |> Map.values() |> Enum.map(&elem(&1,1)) |> Enum.concat()
missing = MapSet.difference(MapSet.new(incs), MapSet.new(Map.keys(gr)))
for m <- missing, into: gr, do: {m,{nil,[]}}
end
# generate %{} representing the "base" state before anything happens
def base_state(gr) do
for {l,{t,_,incs}} <- gr, into: %{} do
case t do
?% -> {l, false}
?& -> {l, incs |> Enum.map(fn i -> {i,false} end) |> Enum.into(%{}) }
_ -> {l, nil}
end
end
end
# implementing the pulse rules (single pulse)
def send(lvl,from,outs), do: outs |> Enum.map(fn oo -> {lvl,from,oo} end)
def do_one_node({nil,outs,_},{lvl,_,"B"}, _), do: {nil,send(lvl,"B",outs)}
def do_one_node({nil, _ ,_}, {_ ,_, _ }, _), do: {nil,[]}
def do_one_node({?%, _ ,_}, {true, _,_}, stt), do: {stt,[]}
def do_one_node({?%,outs,_}, {false,_,i}, stt), do: {not stt,send(not stt,i,outs)}
def do_one_node({?&,outs,_}, {lvl, o,i}, stt) do
nstt = Map.put(stt,o,lvl)
if Map.values(nstt)|>Enum.all?, do: {nstt,send(false,i,outs)}, else: {nstt,send(true,i,outs)}
end
# implementing pulse rules (one whole "push button" run)
def run_pulses(st, [], _, {hs,ls}), do: {st, hs, ls}
def run_pulses(st, [p|ps], gr, {hs,ls}) do
{lvl,_,i} = p
{nlst, nps} = do_one_node(gr[i],p,st[i])
run_pulses(Map.put(st,i,nlst), ps++nps, gr, (if lvl, do: {hs+1,ls}, else: {hs,ls+1}))
end
def solve1(gr) do
bst = gr |> base_state()
res = 1..1000 |> Enum.reduce({bst,0,0}, fn _,{st,hs,ls} ->
run_pulses(st,[{false,nil,"B"}],gr,{hs,ls}) end)
elem(res,1)*elem(res,2)
end
def run_pulses2(st, [], _, _, f), do: {st, f}
def run_pulses2(st, [p|ps], gr, tst, f) do
{lvl,_,i} = p
{nlst, nps} = do_one_node(gr[i],p,st[i])
nf = if {lvl,i} == tst, do: f+1, else: f
run_pulses2(Map.put(st,i,nlst), ps++nps, gr, tst, nf)
end
def solve2(_) do
"sorry, no general solution"
end
def find_first(cond,gr) do
bst = gr |> base_state()
1..100_000 |> Enum.reduce_while({bst,0}, fn i, {st,f} ->
{nst,nf} = run_pulses2(st,[{false,nil,"B"}],gr,cond,f)
if nf>0, do: {:halt, i}, else: {:cont, {nst,nf}}
end)
end
def lcm(ls), do: Enum.reduce(ls, Enum.at(ls,0), fn n, lcm -> div(n*lcm, Integer.gcd(n,lcm)) end)
def mess_around(gr) do
# OK so the way I did this is that I used "dot" to draw the graph, and noticed
# a structure: the vtx just before rx (&vd) has 4 inputs, and each of those come
# from independent components, branched right from the broadcaster.
# So: did each of the components separately. and then did lcm of them
# (interestingly, each of them 4 were primes, in the range of ~3900.)
watchers = for vtx1 <- elem(gr["rx"],2), vtx2 <- elem(gr[vtx1],2), into: [], do: vtx2
watchers |> Enum.map(&find_first({false,&1},gr)) |> lcm()
end
end
:timer.tc(&Main.run/0)
|> IO.inspect(charlists: :as_lists)







