Day 20 - Advent of Code 2023
Mix.install([
  {:kino, "~> 0.12.0"},
  {:benchee, "~> 1.2"},
  {:math, "~> 0.7.0"}
])Links
Input
input = Kino.Input.textarea("Please paste your input file:")input = input |> Kino.Input.read()"%gv -> lq, pm\n%rv -> jd, nh\n%nh -> rs, jd\n&vt -> tj\n%zv -> pm, gv\n%gh -> jd, vd\n%hh -> bf, qm\n%kx -> nf\n%st -> pm, zc\n%bh -> qm, pv\n&sk -> tj\n%hl -> nf, pn\n%mt -> st, pm\n&jd -> ts, gh, vd, dc, xc\n%zm -> hm\n%pv -> vv\n%zf -> nf, cz\n&xc -> tj\n%bf -> qm\n%ts -> sg\n%ht -> ch, nf\n%pb -> rv, jd\n%nx -> fc\n%mb -> mt\n%mh -> jd, pb\n%lc -> bh\n%xg -> mb, pm\n%vd -> dc\nbroadcaster -> gh, dl, xg, fb\n%sg -> mh, jd\n%qq -> ts, jd\n%dl -> nf, sv\n%vv -> sm, qm\n%zc -> tb\n%sr -> zv, pm\n%dc -> gb\n%cz -> nf, zm\n%rs -> jd\n%hm -> nf, hl\n%gd -> sr\n&qm -> lc, pv, nx, fb, kk\n&tj -> rx\n%gb -> qq, jd\n%xf -> zf\n%tb -> lg\n%sm -> qm, hh\n%fb -> dr, qm\n%lq -> pm\n&nf -> zm, dl, ch, xf, vt\n&pm -> sk, zc, tb, gd, mb, xg\n%pn -> nf, kx\n%fc -> xb, qm\n%ch -> xf\n&kk -> tj\n%lg -> pm, gd\n%sv -> nf, ht\n%xb -> qm, lc\n%dr -> nx, qm"Solution
defmodule Day20 do
  defdelegate parse(input), to: __MODULE__.Input
  def part1(input) do
    input
    |> parse()
    |> press_button(1000)
    |> then(fn %{high: high, low: low} -> high * low end)
  end
  def part2(input) do
    state = parse(input)
    high_cycles = for k <- get_sources(state, "rx"), into: %{}, do: {k, nil}
    state
    |> Map.merge(%{high_cycles: high_cycles, button_count: 0})
    |> find_high_cycle_counts()
    |> Map.values()
    |> Enum.reduce(1, &Math.lcm/2)
  end
  defp get_sources(state, dest_key) do
    Enum.find_value(state, fn
      {_, {:conj, dests, sources_memory}} ->
        if dest_key in dests, do: Map.keys(sources_memory)
      _ ->
        nil
    end)
  end
  defp find_high_cycle_counts(state) do
    if Enum.all?(state.high_cycles, fn {_, v} -> v end) do
      state.high_cycles
    else
      state
      |> press_button(1)
      |> Map.update!(:button_count, &(&1 + 1))
      |> find_high_cycle_counts()
    end
  end
  defp press_button(state, count \\ 0, max_count)
  defp press_button(state, count, max_count) when count < max_count do
    [{:low, "button", "broadcaster"}]
    |> send_pulses(state)
    |> press_button(count + 1, max_count)
  end
  defp press_button(state, _, _), do: state
  defp send_pulses([], state), do: state
  defp send_pulses([h | tail], state) do
    {new_pulses, new_state} = send_pulse(h, state)
    send_pulses(tail ++ new_pulses, new_state)
  end
  defp send_pulse({pulse, source_module, module}, state) do
    new_state = Map.update(state, pulse, 1, &(&1 + 1))
    do_send_pulse(pulse, source_module, module, state[module], new_state)
  end
  # 'output' for example testing
  defp do_send_pulse(_pulse, _source, _module, nil, state) do
    {[], state}
  end
  defp do_send_pulse(pulse, _source, module, {:broadcast, dests}, state) do
    {
      Enum.map(dests, &{pulse, module, &1}),
      state
    }
  end
  defp do_send_pulse(:high, _source, _module, {:flip, _, _}, state) do
    {[], state}
  end
  defp do_send_pulse(:low, _source, module, {:flip, dests, :off}, state) do
    new_state = Map.put(state, module, {:flip, dests, :on})
    {
      Enum.map(dests, &{:high, module, &1}),
      new_state
    }
  end
  defp do_send_pulse(:low, _source, module, {:flip, dests, :on}, state) do
    new_state = Map.put(state, module, {:flip, dests, :off})
    {
      Enum.map(dests, &{:low, module, &1}),
      new_state
    }
  end
  defp do_send_pulse(pulse, source, module, {:conj, dests, memory}, state) do
    state =
      case state do
        %{high_cycles: %{^source => nil}} when pulse == :high ->
          Map.update!(state, :high_cycles, fn cycles ->
            Map.put(cycles, source, state.button_count + 1)
          end)
        state ->
          state
      end
    new_memory = Map.put(memory, source, pulse)
    new_state = Map.put(state, module, {:conj, dests, new_memory})
    next_pulse = if Enum.all?(new_memory, &(elem(&1, 1) == :high)), do: :low, else: :high
    {
      Enum.map(dests, &{next_pulse, module, &1}),
      new_state
    }
  end
  defmodule Input do
    def parse(input) do
      input
      |> String.splitter("\n", trim: true)
      |> Enum.map(&parse_line/1)
      |> Enum.into(%{})
      |> setup_conjunctions()
    end
    defp setup_conjunctions(modules) do
      for {k, {_, dests, _}} <- modules, reduce: %{} do
        acc ->
          Enum.reduce(dests, acc, fn d, acc ->
            case modules[d] do
              {:conj, _, _} -> Map.update(acc, d, [k], &[k | &1])
              _ -> acc
            end
          end)
      end
      |> Enum.reduce(modules, fn {k, sources}, acc ->
        Map.update!(acc, k, fn
          {:conj, d, %{}} ->
            {:conj, d, sources |> Enum.map(&{&1, :low}) |> Enum.into(%{})}
          v ->
            v
        end)
      end)
    end
    def parse_line(line) do
      dest = &String.split(&1, ", ")
      case String.split(line, " -> ") do
        ["%" <> module, d] ->
          {module, {:flip, dest.(d), :off}}
        ["&" <> module, d] ->
          {module, {:conj, dest.(d), %{}}}
        [module, d] ->
          {module, {:broadcast, dest.(d)}}
      end
    end
  end
end{:module, Day20, <<70, 79, 82, 49, 0, 0, 28, ...>>,
 {:module, Day20.Input, <<70, 79, 82, ...>>, {:parse_line, 1}}}Part 1:
Consult your module configuration; determine the number of low pulses and high pulses that would be sent after pushing the button 1000 times, waiting for all pulses to be fully handled after each push of the button. What do you get if you multiply the total number of low pulses sent by the total number of high pulses sent?
Your puzzle answer was 818649769.
Day20.part1(input)818649769Part 2:
The final machine responsible for moving the sand down to Island Island has a module attached named rx. The machine turns on when a single low pulse is sent to rx.
Reset all modules to their default states. Waiting for all pulses to be fully handled after each button press, what is the fewest number of button presses required to deliver a single low pulse to the module named rx?
Your puzzle answer was 246313604784977.
Day20.part2(input)246313604784977Both parts of this puzzle are complete! They provide two gold stars: **
At this point, you should return to your Advent calendar and try another puzzle.
If you still want to see it, you can get your puzzle input.
Tests
ExUnit.start(auto_run: false)
defmodule Day20Test do
  use ExUnit.Case, async: false
  setup_all do
    [
      input1: "broadcaster -> a, b, c\n%a -> b\n%b -> c\n%c -> inv\n&inv -> a",
      input2: "broadcaster -> a\n%a -> inv, con\n&inv -> b\n%b -> con\n&con -> output"
    ]
  end
  describe "part1/1" do
    test "returns expected value", %{input1: input1, input2: input2} do
      assert Day20.part1(input1) == 32_000_000
      assert Day20.part1(input2) == 11_687_500
    end
  end
end
ExUnit.run().
Finished in 0.00 seconds (0.00s async, 0.00s sync)
1 test, 0 failures
Randomized with seed 432984%{total: 1, failures: 0, excluded: 0, skipped: 0}Benchmarking
defmodule Benchmarking do
  # https://github.com/bencheeorg/benchee
  def run(input) do
    Benchee.run(
      %{
        "Part 1" => fn -> Day20.part1(input) end,
        "Part 2" => fn -> Day20.part2(input) end
      },
      memory_time: 2,
      reduction_time: 2
    )
    nil
  end
end{:module, Benchmarking, <<70, 79, 82, 49, 0, 0, 7, ...>>, {:run, 1}}Benchmarking.run(input)Operating System: macOS
CPU Information: Apple M1 Pro
Number of Available Cores: 10
Available memory: 32 GB
Elixir 1.15.7
Erlang 26.1
Benchmark suite executing with the following configuration:
warmup: 2 s
time: 5 s
memory time: 2 s
reduction time: 2 s
parallel: 1
inputs: none specified
Estimated total run time: 22 s
Benchmarking Part 1 ...
Benchmarking Part 2 ...
Name             ips        average  deviation         median         99th %
Part 1         74.99       13.34 ms     ±1.39%       13.35 ms       13.98 ms
Part 2         17.15       58.32 ms     ±2.99%       57.91 ms       67.32 ms
Comparison: 
Part 1         74.99
Part 2         17.15 - 4.37x slower +44.98 ms
Memory usage statistics:
Name           average  deviation         median         99th %
Part 1        41.53 MB     ±0.01%       41.53 MB       41.53 MB
Part 2       170.40 MB     ±0.01%      170.40 MB      170.42 MB
Comparison: 
Part 1        41.53 MB
Part 2       170.40 MB - 4.10x memory usage +128.87 MB
Reduction count statistics:
Name   Reduction count
Part 1          1.51 M
Part 2          6.32 M - 4.18x reduction count +4.81 M
**All measurements for reduction count were the same**nil