lud

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

bjorng

Erlang Core Team

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.

https://github.com/bjorng/advent-of-code-2023/blob/main/day20/lib/day20.ex

exists

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)

Where Next?

Popular in Challenges Top

woolfred
It is that time of the year again: Advent of Code 2022 :christmas_tree: Day 1 Leaderboard:
New
ehayun
I have 2 arrays: a1 can be any combination of value or nil like that a1 = [1,nil,3] and array 2 the same a2 = [4,2, nil] How do I com...
New
coen.bakker
Since I started using Elixir, I have benefited greatly from being able to study various open-source projects. The codebase of LiveBook, i...
New
liamcmitchell
A frustrating one for me. I spent a long time trying to understand why some combinations resulted in fewer presses and struggled to keep ...
New
bjorng
Note: This topic is to talk about Day 25 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can joi...
New
adamu
Nobody’s doing Advent of Code this year? :grinning_face_with_smiling_eyes: I might do the first week or so. For Day 1, first I solved i...
New
Aetherus
This topic is about Day 16 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/l...
New
code-shoily
Just did part 1. Part 2 seems to be demanding too much of my reading time so will get to that after I am done with some chores. Oh here ...
New
Aetherus
This topic is about Day 7 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/le...
New
christhekeele
Thought I’d kick today’s thread off! Parsing Enum rocks, so most of my code was actually in parsing input. ▶ Preprocessing input Part 1...
New

Other popular topics Top

Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
RisingFromAshes
I’ve read in another post that it may be possible with a router helper - but I couldn’t find an appropriate one, and tbh, I’m still just ...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

We're in Beta

About us Mission Statement