Powered by AppSignal & Oban Pro

Advent of code 2025 day 8

aoc2025day8.livemd

Advent of code 2025 day 8

Mix.install([
  {:kino, "~> 0.18"}
])

Part 1

https://adventofcode.com/2025/day/8

input = Kino.Input.textarea("Please give me input:")
points =
  Kino.Input.read(input)
  |> String.split("\n", trim: true)
  |> Enum.map(fn xyz ->
    [x, y, z] =
      String.split(xyz, ",")
      |> Enum.map(&String.to_integer/1)

    {x, y, z}
  end)

length(points)
defmodule StringsOfLight do
  defp calc_distances(points) do
    # Make a distance table, requires (N^2 - N) / 2 calculations
    # Double loop, where inner loop p-inner <> p-outer
    # To prevent too much double calculations, p-inner < p-outer
    # output: [{distance1, p1, p2}, ...]
    for p1 = {x1, y1, z1} <- points,
        p2 = {x2, y2, z2} <- points,
        p1 < p2 do
      dx = x2 - x1
      dy = y2 - y1
      dz = z2 - z1

      distance = dx * dx + dy * dy + dz * dz
      {distance, p1, p2}
    end
    |> List.keysort(0)
  end

  defp init_circuits(points) do
    # fill single boxes
    Enum.reduce(points, {0, %{}, %{}}, fn p, {circuit_next_val, circuits, boxes_in_circuits} ->
      c_new =
        circuits
        |> Map.put(circuit_next_val, [p])

      box_new =
        boxes_in_circuits
        |> Map.put(p, circuit_next_val)

      {circuit_next_val + 1, c_new, box_new}
    end)
  end

  defp calc_final_circuits_or_end_points(points, distances, stop_at_circuit_total) do
    {_, init_circuits, init_boxes_in_circuits} = init_circuits(points)

    Enum.reduce_while(distances, {init_circuits, init_boxes_in_circuits, nil}, fn {_distance, p1,
                                                                                   p2},
                                                                                  {circuits,
                                                                                   boxes_in_circuits,
                                                                                   _} ->
      p1_circuit_index = Map.get(boxes_in_circuits, p1)
      p2_circuit_index = Map.get(boxes_in_circuits, p2)

      if p1_circuit_index == p2_circuit_index do
        {:cont, {circuits, boxes_in_circuits, nil}}
      else
        boxes1 = Map.get(circuits, p1_circuit_index)
        boxes2 = Map.get(circuits, p2_circuit_index)

        c_new =
          Map.put(circuits, p1_circuit_index, boxes1 ++ boxes2)
          |> Map.delete(p2_circuit_index)

        box_new =
          Enum.reduce(boxes2, boxes_in_circuits, fn box, change_boxes_in_circuits ->
            Map.put(change_boxes_in_circuits, box, p1_circuit_index)
          end)

        if stop_at_circuit_total >= 1 and Enum.count(Map.keys(c_new)) == stop_at_circuit_total do
          {:halt, {c_new, box_new, {p1, p2}}}
        else
          {:cont, {c_new, box_new, nil}}
        end
      end
    end)
  end

  def calc_final_circuits(points, distances) do
    {final_circuits, _, _} = calc_final_circuits_or_end_points(points, distances, -1)

    final_circuits
  end

  def calc_last_points(points, all_distances) do
    {_circuits, _, last_points} =
      calc_final_circuits_or_end_points(points, all_distances, 1)

    last_points
  end

  def part1(points, nr_of_shortest_connects) do
    distances =
      calc_distances(points)
      |> Enum.take(nr_of_shortest_connects)

    calc_final_circuits(points, distances)
    |> Enum.map(&amp;elem(&amp;1, 1))
    |> Enum.sort_by(&amp;length/1, :desc)
    |> Enum.take(3)
    |> Enum.product_by(&amp;length/1)
  end

  def part2(points) do
    distances =
      calc_distances(points)

    {{x1, _y1, _z1}, {x2, _y2, _z2}} = calc_last_points(points, distances)

    x1 * x2
  end
end

IO.inspect StringsOfLight.part1(points, 1000), label: "part1"
IO.inspect StringsOfLight.part2(points), label: "part2"