Powered by AppSignal & Oban Pro

Day 8 - Advent of Code 2025

lib/advent_of_code/2025/day-08.livemd

Day 8 - Advent of Code 2025

Mix.install([:kino, :benchee])

Links

Prompt

— Day 8: Playground —

Equipped with a new understanding of teleporter maintenance, you confidently step onto the repaired teleporter pad.

You rematerialize on an unfamiliar teleporter pad and find yourself in a vast underground space which contains a giant playground!

Across the playground, a group of Elves are working on setting up an ambitious Christmas decoration project. Through careful rigging, they have suspended a large number of small electrical junction boxes.

Their plan is to connect the junction boxes with long strings of lights. Most of the junction boxes don’t provide electricity; however, when two junction boxes are connected by a string of lights, electricity can pass between those two junction boxes.

The Elves are trying to figure out which junction boxes to connect so that electricity can reach every junction box. They even have a list of all of the junction boxes’ positions in 3D space (your puzzle input).

For example:

162,817,812
57,618,57
906,360,560
592,479,940
352,342,300
466,668,158
542,29,236
431,825,988
739,650,466
52,470,668
216,146,977
819,987,18
117,168,530
805,96,715
346,949,466
970,615,88
941,993,340
862,61,35
984,92,344
425,690,689

This list describes the position of 20 junction boxes, one per line. Each position is given as X,Y,Z coordinates. So, the first junction box in the list is at X=162, Y=817, Z=812.

To save on string lights, the Elves would like to focus on connecting pairs of junction boxes that are as close together as possible according to straight-line distance. In this example, the two junction boxes which are closest together are 162,817,812 and 425,690,689.

By connecting these two junction boxes together, because electricity can flow between them, they become part of the same circuit. After connecting them, there is a single circuit which contains two junction boxes, and the remaining 18 junction boxes remain in their own individual circuits.

Now, the two junction boxes which are closest together but aren’t already directly connected are 162,817,812 and 431,825,988. After connecting them, since 162,817,812 is already connected to another junction box, there is now a single circuit which contains three junction boxes and an additional 17 circuits which contain one junction box each.

The next two junction boxes to connect are 906,360,560 and 805,96,715. After connecting them, there is a circuit containing 3 junction boxes, a circuit containing 2 junction boxes, and 15 circuits which contain one junction box each.

The next two junction boxes are 431,825,988 and 425,690,689. Because these two junction boxes were already in the same circuit, nothing happens!

This process continues for a while, and the Elves are concerned that they don’t have enough extension cables for all these circuits. They would like to know how big the circuits will be.

After making the ten shortest connections, there are 11 circuits: one circuit which contains 5 junction boxes, one circuit which contains 4 junction boxes, two circuits which contain 2 junction boxes each, and seven circuits which each contain a single junction box. Multiplying together the sizes of the three largest circuits (5, 4, and one of the circuits of size 2) produces 40.

Your list contains many junction boxes; connect together the 1000 pairs of junction boxes which are closest together. Afterward, what do you get if you multiply together the sizes of the three largest circuits?

To begin, get your puzzle input.

— Part Two —

The Elves were right; they definitely don’t have enough extension cables. You’ll need to keep connecting junction boxes together until they’re all in one large circuit.

Continuing the above example, the first connection which causes all of the junction boxes to form a single circuit is between the junction boxes at 216,146,977 and 117,168,530. The Elves need to know how far those junction boxes are from the wall so they can pick the right extension cable; multiplying the X coordinates of those two junction boxes (216 and 117) produces 25272.

Continue connecting the closest unconnected pairs of junction boxes together until they’re all in the same circuit. What do you get if you multiply together the X coordinates of the last two junction boxes you need to connect?

Although it hasn’t changed, you can still get your puzzle input.

Input

input = Kino.Input.textarea("Please paste your input file:")
input = input |> Kino.Input.read()
"162,817,812\n57,618,57\n906,360,560\n592,479,940\n352,342,300\n466,668,158\n542,29,236\n431,825,988\n739,650,466\n52,470,668\n216,146,977\n819,987,18\n117,168,530\n805,96,715\n346,949,466\n970,615,88\n941,993,340\n862,61,35\n984,92,344\n425,690,689"

Solution

defmodule Day08 do
  defdelegate parse(input), to: __MODULE__.Input

  def part1(input, num_circuits \\ 1000) do
    points = input |> parse() |> Enum.to_list()
    index = points |> Enum.map(&{&1, &1}) |> Map.new()
    circuits = points |> Enum.map(&{&1, [&1]}) |> Map.new()

    points
    |> all_pairs()
    |> List.flatten()
    |> Enum.sort_by(&calc_distance/1)
    |> Enum.take(num_circuits)
    |> Enum.reduce({index, circuits}, &join_circuits/2)
    |> then(fn {_index, circuits} ->
      circuits
      |> Map.values()
      |> Enum.map(&length/1)
      |> Enum.sort(:desc)
      |> Enum.take(3)
      |> Enum.product()
    end)
  end

  def part2(input) do
    points = input |> parse() |> Enum.to_list()
    index = points |> Enum.map(&{&1, &1}) |> Map.new()
    circuits = points |> Enum.map(&{&1, [&1]}) |> Map.new()

    points
    |> all_pairs()
    |> List.flatten()
    |> Enum.sort_by(&calc_distance/1)
    |> Enum.reduce_while({index, circuits}, fn pair, acc ->
      {index, circuits} = join_circuits(pair, acc)

      calc_product = fn {{x1, _, _}, {x2, _, _}} -> x1 * x2 end

      if map_size(circuits) == 1,
        do: {:halt, calc_product.(pair)},
        else: {:cont, {index, circuits}}
    end)
  end

  defp all_pairs([h | [t]]), do: {h, t}

  defp all_pairs([h | tail]) do
    [
      Enum.map(tail, &{h, &1}),
      all_pairs(tail)
    ]
  end

  defp calc_distance({{x1, y1, z1}, {x2, y2, z2}}) do
    [
      abs(x1 - x2) |> :math.pow(2),
      abs(y1 - y2) |> :math.pow(2),
      abs(z1 - z2) |> :math.pow(2)
    ]
    |> Enum.sum()
    |> :math.sqrt()
  end

  defp join_circuits({circuit_a, circuit_b}, {index, circuits}) do
    new_key = Map.get(index, circuit_a)
    old_key = Map.get(index, circuit_b)

    if new_key == old_key do
      {index, circuits}
    else
      {old_circuit, circuits} = Map.pop!(circuits, old_key)

      new_index =
        Enum.reduce(old_circuit, index, fn circuit_key, acc ->
          Map.put(acc, circuit_key, new_key)
        end)

      new_circuits = Map.update!(circuits, new_key, &(&1 ++ old_circuit))

      {new_index, new_circuits}
    end
  end

  defmodule Input do
    def parse(input) when is_binary(input) do
      input
      |> String.splitter("\n", trim: true)
      |> parse()
    end

    def parse(input) do
      Stream.map(input, &parse_line/1)
    end

    def parse_line(line) do
      line
      |> String.split(",")
      |> Enum.map(&String.to_integer/1)
      |> List.to_tuple()
    end
  end
end
{:module, Day08, <<70, 79, 82, 49, 0, 0, 25, ...>>,
 {:module, Day08.Input, <<70, 79, 82, ...>>, {:parse_line, 1}}}

Your list contains many junction boxes; connect together the 1000 pairs of junction boxes which are closest together. Afterward, what do you get if you multiply together the sizes of the three largest circuits?

Your puzzle answer was 75582.

Day08.part1(input, 10)
40

Continue connecting the closest unconnected pairs of junction boxes together until they’re all in the same circuit. What do you get if you multiply together the X coordinates of the last two junction boxes you need to connect?

Your puzzle answer was 59039696.

Day08.part2(input)
25272

Both 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 Day08Test do
  use ExUnit.Case, async: false

  setup_all do
    [
      input: "162,817,812\n57,618,57\n906,360,560\n592,479,940\n352,342,300\n466,668,158\n542,29,236\n431,825,988\n739,650,466\n52,470,668\n216,146,977\n819,987,18\n117,168,530\n805,96,715\n346,949,466\n970,615,88\n941,993,340\n862,61,35\n984,92,344\n425,690,689"
    ]
  end

  describe "part1/1" do
    test "returns expected value", %{input: input} do
      assert Day08.part1(input, 10) == 40
    end
  end

  describe "part2/1" do
    test "returns expected value", %{input: input} do
      assert Day08.part2(input) == 25272
    end
  end
end

ExUnit.run()
Running ExUnit with seed: 938843, max_cases: 28

..
Finished in 0.00 seconds (0.00s async, 0.00s sync)
2 tests, 0 failures
%{total: 2, excluded: 0, failures: 0, skipped: 0}

Benchmarking

defmodule Benchmarking do
  # https://github.com/bencheeorg/benchee
  def run(input) do
    Benchee.run(
      %{
        "Part 1" => fn -> Day08.part1(input) end,
        "Part 2" => fn -> Day08.part2(input) end
      },
      memory_time: 2,
      reduction_time: 2
    )

    nil
  end
end
{:module, Benchmarking, <<70, 79, 82, 49, 0, 0, 8, ...>>, {:run, 1}}
Benchmarking.run(input)
Operating System: macOS
CPU Information: Apple M4 Pro
Number of Available Cores: 14
Available memory: 48 GB
Elixir 1.18.3
Erlang 27.3
JIT enabled: true

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
Excluding outliers: false

Benchmarking Part 1 ...
Benchmarking Part 2 ...
Calculating statistics...
Formatting results...

Name             ips        average  deviation         median         99th %
Part 1          6.62      151.06 ms    ±11.49%      145.92 ms      202.92 ms
Part 2          6.40      156.20 ms    ±10.98%      150.35 ms      215.76 ms

Comparison: 
Part 1          6.62
Part 2          6.40 - 1.03x slower +5.14 ms

Memory usage statistics:

Name           average  deviation         median         99th %
Part 1       202.46 MB     ±0.00%      202.46 MB      202.46 MB
Part 2       218.78 MB     ±0.00%      218.78 MB      218.78 MB

Comparison: 
Part 1       202.46 MB
Part 2       218.78 MB - 1.08x memory usage +16.32 MB

Reduction count statistics:

Name           average  deviation         median         99th %
Part 1         19.43 M     ±0.01%        19.43 M        19.43 M
Part 2         19.65 M     ±0.01%        19.65 M        19.65 M

Comparison: 
Part 1         19.43 M
Part 2         19.65 M - 1.01x reduction count +0.22 M
nil