Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Advent of Code 2023 - Day 20

2023/20.livemd

Advent of Code 2023 - Day 20

Mix.install([
  {:req, "~> 0.4.0"},
  {:kino, "~> 0.12.0"}
])

Input

opts = [headers: [{"cookie", "session=#{System.fetch_env!("LB_AOC_SESSION")}"}]]
puzzle_input = Req.get!("https://adventofcode.com/2023/day/20/input", opts).body
import String, only: [to_atom: 1]

parents = fn machine, child ->
  machine
  |> Enum.filter(fn {_, module} ->
    module.destinations
    |> Enum.member?(child)
  end)
end

init_state = fn
  _m, _n, :flipflop -> false
  m, n, :conjunction -> for {_, %{name: n}} <- parents.(m, n), into: %{}, do: {n, 0}
  _m, _n, _t -> :ok
end

input =
  puzzle_input
  |> String.split("\n", trim: true)
  |> Enum.map(&amp;String.split(&amp;1, [" -> ", ", "]))
  |> Enum.map(fn
    ["broadcaster" | d] -> {:broadcaster, :broadcaster, Enum.map(d, &amp;to_atom/1)}
    ["%" <> name | d] -> {to_atom(name), :flipflop, Enum.map(d, &amp;to_atom/1)}
    ["&" <> name | d] -> {to_atom(name), :conjunction, Enum.map(d, &amp;to_atom/1)}
  end)
  |> Kernel.++([
    {:button, :button, [:broadcaster]}
  ])
  |> Enum.map(fn {name, type, destinations} ->
    {name, %{name: name, type: type, destinations: destinations, state: nil, pulses: []}}
  end)
  |> Enum.into(%{})
  |> then(fn machine ->
    machine
    |> Enum.reduce(machine, fn {name, %{type: type}}, acc ->
      update_in(acc[name][:state], fn _ -> init_state.(acc, name, type) end)
    end)
  end)
for {from, %{type: type, destinations: destinations}} <- input,
    to <- destinations,
    reduce: "graph LR;\n" do
  acc ->
    case type do
      :broadcaster -> acc <> "#{from}((#{from})) --> #{to}\n"
      :conjunction -> acc <> "#{from}{#{from}} --> #{to}\n"
      _type -> acc <> "#{from} --> #{to}\n"
    end
end
|> Kino.Mermaid.new()
defmodule Machine do
  def aptly(machine) do
    q = fifo_push({:button, 0, :broadcaster})
    work({q, machine})
  end

  defp work({q, machine}) do
    case fifo_pop(q) do
      {{:value, {from, pulse, to}}, rest} -> do_work(rest, pulse, from, to, machine)
      {:empty, _} -> machine
    end
  end

  defp do_work(q, pulse, from, to, machine) do
    machine = update_in(machine, [from, :pulses], fn x -> [pulse | x] end)

    pulses(q, pulse, machine[from], machine[to], machine)
    |> work()
  end

  defp pulses(q, _pulse, _from, nil, machine), do: {q, machine}

  defp pulses(q, pulse, _from, %{type: :broadcaster} = to, machine) do
    %{name: name, destinations: destinations} = to

    do_pulses(q, pulse, name, destinations, machine)
  end

  defp pulses(q, 1, _from, %{type: :flipflop} = _to, machine), do: {q, machine}

  defp pulses(q, 0, _from, %{type: :flipflop} = to, machine) do
    %{name: name, destinations: destinations, state: state} = to
    machine = update_in(machine, [name, :state], fn x -> not x end)
    next = if state, do: 0, else: 1

    do_pulses(q, next, name, destinations, machine)
  end

  defp pulses(q, pulse, from, %{type: :conjunction} = to, machine) do
    %{name: sender} = from
    %{name: name, destinations: destinations} = to

    machine = update_in(machine, [name, :state, sender], fn _ -> pulse end)
    next = if Enum.all?(machine[name][:state], &amp;(elem(&amp;1, 1) == 1)), do: 0, else: 1

    do_pulses(q, next, name, destinations, machine)
  end

  defp do_pulses(q, pulse, name, destinations, machine) do
    destinations
    |> Enum.reduce(q, fn d, acc ->
      fifo_push(acc, {name, pulse, d})
    end)
    |> then(fn newq -> {newq, machine} end)
  end

  defp fifo_push(q \\ :queue.new(), item), do: :queue.in(item, q)
  defp fifo_pop(q), do: :queue.out(q)
end

Part 1

1..1000
|> Enum.reduce(input, fn _i, acc -> Machine.aptly(acc) end)
|> Stream.flat_map(fn {_, %{pulses: pulses}} -> pulses end)
|> Enum.frequencies()
|> Map.values()
|> Enum.product()

Part 2

init_highs =
  for {parent, _} <- parents.(input, :rx),
      {gparent, _} <- parents.(input, parent),
      into: %{},
      do: {gparent, nil}
update_highs = fn machine, highs, i ->
  highs
  |> Enum.reduce(highs, fn {name, _}, acc ->
    Map.update!(acc, name, fn
      nil -> if Enum.member?(machine[name][:pulses], 1), do: i, else: nil
      value -> value
    end)
  end)
end

find_highs = fn i, {acc_machine, acc_highs} ->
  if Enum.any?(acc_highs, fn {_name, index} -> is_nil(index) end) do
    {:cont, {Machine.aptly(acc_machine), update_highs.(acc_machine, acc_highs, i)}}
  else
    {:halt, acc_highs}
  end
end

Stream.unfold(0, fn x -> {x, x + 1} end)
|> Enum.reduce_while({input, init_highs}, find_highs)
|> Map.values()
|> Enum.reduce(&amp;div(&amp;1 * &amp;2, Integer.gcd(&amp;1, &amp;2)))

Run in Livebook