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.

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

LostKobrakai
This topic is about Day 9 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/le...
New
bismark
Took me a minute to remember my binary math :smile: :grimacing:… import Bitwise __DIR__ |&gt; Path.join("puzzle.txt") |&gt; File.stream...
New
antoine-duchenet
Everything went smoothly today. Nothing to change to solve part 2 because I already used memoization for part 1 (it looked like an AoC e...
New
bjorng
This topic is about Day 16 of the Advent of Code 2021. We have a private leaderboard (shared with users of Erlang Forums): https://adve...
New
New
stevensonmt
Anyone else think the prompt for this challenge is contradictory? The rules for comparing packets include If both values are lists, c...
New
jkwchui
Monkeys fitted squarely as GenServers in my head. My initial problem was using cast instead of call; I imagine impolite monkeys slinging...
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
bjorng
Note: This topic is to talk about Day 4 of the Advent of Code 2019. There is a private leaderboard for elixirforum members. You can join...
New
Aetherus
Don’t know why the regex ~r/[\W && [^\.]]/x does not work in Elixir. It works pretty well in Ruby. Anyway, here is my solution:
New

Other popular topics Top

Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
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
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: https://hexdocs.pm/ecto/Ecto.Schema.html#module-...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
mgjohns61585
Could someone help me? I'm making my first elixir program, number guessing game. I can't figure out how to convert the user's guess from ...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
273 38985 115
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New

We're in Beta

About us Mission Statement