lud

lud

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

Showing Posts 1 to 6

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)
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

woojiahao

woojiahao

Very tricky day and it took me a while to clean up the code to remove the use of ETS entirely.

Key Observations for Part 2
  1. There exists only one conjunction connected to rx
  2. For this conjunction &c, it has inputs from m other conjunctions
  3. To find when &c sends a low pulse to rx, we need to find the LCM of the first times that every connected conjunction sends a high pulse to &c since that means all of them sent a high pulse at once, causing &c to send a low pulse to rx

Maybe not the cleanest but I’m happy it works:

https://github.com/woojiahao/aoc/blob/main/lib/aoc/y2023/day_20.ex

seeplusplus

seeplusplus

Edit I fixed this by calling the protocol method directly, will share more when finished. I can’t delete this post.

Apologies if this isn’t the right place to ask for help, but since everyone here is already familiar with the problem, I figured it would be the best place to ask.

I wanted to have each node type (broadcaster, flipflop, conjunction) etc as its own module and store them together in a map (code attached). The issue I’m running into is that the map is a homogeneous collection of nodes, so how can I call the underlying send_signal method for the correct nodes? I thought implementing the same protocol for each would help, but I’m missing something. Here’s my attached code with comments to clarify my question:

midouest

midouest

Part 2 thoroughly stumped me. I finally cracked it by rendering the graph and the internal flip flop states with Kino.Mermaid.

After looking at how the flip-flop states were changing over several iterations, it became clear that the machine consisted of 4 independent 12-bit number channels. Each 12-bit number channel had an arbitrary number of bits connected to a NAND gate that fed to both rx and the lowest bit. I figured out the order of the bits and which ones were connected to rx and then used that to find the LCMs of each channel. Then I found the LCM of the 4 channels!

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

You can see I used the for-loop here to explore the graph at different iterations.

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)
pehbehbeh

pehbehbeh

Part 1: Implementing a working queue system for the machine and press it 1000 times.

Part 2: Press the button as long as the grand children (conjunction) modules of rx each sent a high. Then get the LCM of these highs. Some hints from Reddit and rendering the graph really helped…

https://github.com/pehbehbeh/adventofcode/blob/main/2023/20.livemd

— All posts loaded —

Where Next? Top

Trending in Challenges Top

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews