[–] 3 points 11 months ago (1 child)

C

Got stuck for a bit on part 1 on a silly mistake in the group (circuit) merging code, where the nodes from one group are reassigned to the other:

for (i=0; i < nnodes; i++)
        if (nodes[i].group == pair->b->group)
                nodes[i].group = pair->a->group;

At some point in the loop pair->b.group itself is updated, from then on the check is against the new group value. Oops.

In the end, my solution's runtime on my (10 year old!) PC is about 160 ms for both parts, which is more than I would like, so maybe I'll look into better set representations.

Code

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <assert.h>

#define LEN(a)		(sizeof(a)/sizeof(*(a)))
#define NPAIRS(x)	((x)*((x)-1)/2)

#define MAXN	1024

struct node { int x,y,z, group; };
struct pair { struct node *a, *b; int64_t dist_sq; };
struct group { int count; };

static struct node nodes[MAXN];
static struct pair pairs[NPAIRS(MAXN)];
static struct group groups[MAXN];

static int nnodes;
static int ngroups;

static int64_t
node_dist_sq(const struct node *a, const struct node *b)
{
	return
	    (int64_t)(a->x - b->x) * (a->x - b->x) +
	    (int64_t)(a->y - b->y) * (a->y - b->y) +
	    (int64_t)(a->z - b->z) * (a->z - b->z);
}

static int
cmp_pairs(const void *va, const void *vb)
{
	const struct pair *a = va;
	const struct pair *b = vb;

	return
	    a->dist_sq < b->dist_sq ? -1 :
	    a->dist_sq > b->dist_sq ?  1 : 0;
}

static int
cmp_groups_asc(const void *va, const void *vb)
{
	const struct group *a = va;
	const struct group *b = vb;

	return b->count - a->count;
}

static void
merge_groups(int group_a, int group_b)
{
	int i;

	if (group_a == group_b)
		return;

	groups[group_a].count += groups[group_b].count;
	groups[group_b].count = 0;

	for (i=0; i<nnodes; i++)
		if (nodes[i].group == group_b)
			nodes[i].group = group_a;
	
	ngroups--;
}

int
main()
{
	int p1=0,p2=0, p1_limit, i,j, n,p;

	for (; ; nnodes++) {
		assert(nnodes < MAXN);
		n = scanf(" %d,%d,%d",
		    &nodes[nnodes].x,
		    &nodes[nnodes].y,
		    &nodes[nnodes].z);
		if (n < 3)
			break;
		nodes[nnodes].group = nnodes;
		groups[nnodes].count = 1;
	}

	ngroups = nnodes;

	for (p=0, i=0; i<nnodes-1; i++)
	for (j=i+1; j<nnodes; j++, p++) {
		pairs[p].a = &nodes[i];
		pairs[p].b = &nodes[j];
		pairs[p].dist_sq = node_dist_sq(&nodes[i], &nodes[j]);
	}

	qsort(pairs, NPAIRS(nnodes), sizeof(*pairs), cmp_pairs);

	p1_limit = nnodes <= 100 ? 10 : 1000;

	for (i=0; ngroups > 1; i++) {
		merge_groups(pairs[i].a->group, pairs[i].b->group);

		if (ngroups == 1)
			p2 = pairs[i].a->x * pairs[i].b->x;

		if (i == p1_limit) {
			qsort(groups, LEN(groups), sizeof(*groups),
			    cmp_groups_asc);

			p1 = groups[0].count *
			     groups[1].count *
			     groups[2].count;
		}
	}

	printf("08: %d %d\n", p1, p2);
}

  • source
  • [–] 2 points 11 months ago* (last edited 11 months ago) (1 child)

    If I understand correctly, your visualization shows up to to 256 unique paths then?

    In this one, every time the beam passes a splitter, the other side is put on a queue, so eventually all possible paths are traced, breadth first. Initially I worked through the options recursively, but that depth-first filling out was boring to look at.

  • source
  • parent
  • context
  • [–] 4 points 11 months ago

    Futhark

    As always, futhark does not support arbitrary inputs, so I have a sed script to transform the input to something readable.

    input transformerit produces a textual representation of [][3]u32, try it on your example or input :]

    1i [
    1,$ {
    	s/^/[/
    	s/$/]/
    }
    2,$i,
    $i ]
    $d
    

    Calculate all the distances (even the redundant ones, I had no idea on how to filter them out). Sort them, keep only the first 1000 for part 1. Keep all for part two. Initialize all boxes to be in no component. Add them to components as time goes on. When connecting two boxes already in a component. Mark all boxes in the second component as part of the first one. Stop when everything is connected.

    After improving my implementation of concatMap (preallocate the entire array), the overall performance improved greatly. My end stats are

    • Time: 7s -> 0.35s
    • Memory: 2GB -> 66MB

    Program Source

    Basic

    import "lib/github.com/diku-dk/sorts/radix_sort"
    
    type position = (u32, u32, u32)
    def positionFromArray (p: [3]u32): position
      = (p[0], p[1], p[2])
    def pair_iota (n: i64): [n](i64, i64)
      = map (\ j -> (n, j)) (iota n)
    def gaussian_sum (n: i64) = n * (n + 1) / 2
    
    def euclidean_distance (a: position) (b: position): f64
      = f64.sqrt 
        ( (f64.u32 a.0 - f64.u32 b.0) ** 2
        + (f64.u32 a.1 - f64.u32 b.1) ** 2
        + (f64.u32 a.2 - f64.u32 b.2) ** 2
        )
    
    def distance_table [n] (positions: [n]position): [n][n]f64
      = let distance_function = \ i j -> euclidean_distance positions[i] positions[j] in
        tabulate_2d n n distance_function
    
    def existsLength 'a 'b (f: a -> ?[l].[l]b) (x: a): i64
      = length (f x)
    
    def concatMap [n] 'a 'b (f: a -> ?[l].[l]b) (placeholder: b) (xs: [n]a): *[]b
      = let totalLength = reduce (+) 0 <| map (\ x -> length (f x)) xs in
        ( loop (results, offset) = (replicate totalLength placeholder, 0)
          for x in xs
          do
            let bs = f x in
            let scatterIndices = indices bs |> map (+offset) in
            (scatter results scatterIndices bs, offset + length bs)
        ).0
    
    def distance_array [n] (positions: [n]position): []((i64, i64), f64)
      = let table = distance_table positions in
        let triangle_indices = concatMap pair_iota (i64.highest, i64.highest) (iota n |> drop 1) in
        map (\ (i, j) -> ((i, j), table[i, j])) triangle_indices
    
    def sort_distances (distances: []((i64, i64), f64)): []((i64, i64), f64)
      = radix_sort_float_by_key (.1) f64.num_bits f64.get_bit distances
    
    type option 'a
      = #Empty
      | #Present a
    
    def empty 'a : option a = #Empty
    
    def overrideWith (old: u16) (new: u16) (x: option u16): option u16
      = match x
          case #Empty -> #Empty
          case #Present inner -> 
            if inner == old
            then #Present new
            else #Present inner
    
    def orElse 'a (o: option a) (d: a): a
      = match o
          case #Empty -> d
          case #Present x -> x
    
    def is_present 'a (o: option a): bool
      = match o
          case #Empty -> false
          case #Present _ -> true
    
    def connect (circuits: *[](option u16)) (newCircuitId: u16) (connection: (i64, i64)): (u16, *[](option u16))
      = let circuitA = circuits[connection.0] in
        let circuitB = circuits[connection.1] in
        match (circuitA, circuitB)
          case (#Empty, #Empty) -> 
            ( newCircuitId + 1
            , scatter circuits [connection.0, connection.1] (rep (#Present newCircuitId))
            )
          case (#Present a, #Empty) -> 
            ( newCircuitId
            , scatter circuits [connection.1] [#Present a]
            )
          case (#Empty, #Present b) -> 
            ( newCircuitId
            , scatter circuits [connection.0] [#Present b]
            )
          case (#Present a, #Present b) ->
            ( newCircuitId
            , map (b `overrideWith` a) circuits
            )
    
    def countCircuit (counts: *[]u64) (o: option u16): *[]u64 
      = match o
        case #Empty -> counts 
        case #Present i -> scatter counts [i64.u16 i] [counts[i64.u16 i] + 1]
    
    def countCircuits (c: u16) (circuits: [](option u16)): *[i64.u16 c]u64
      = let circuitCounts = replicate (i64.u16 c) 0 in
        loop counts = circuitCounts
        for circuit in circuits
        do countCircuit counts circuit
    
    def exampleConnectionCount = 10i64
    def inputConnectionCount = 1000i64
    
    def part1 (positions: i64) (connectionCount: i64) (distances: []((i64, i64), f64))
      = let connections = take connectionCount distances |> map (.0) in
        let circuitMap: *[positions](option u16) = replicate positions empty in
        ( loop (circuitCount, circuits) = (0, circuitMap)
          for connection in connections
          do
            connect circuits circuitCount connection
        ) |> uncurry countCircuits 
          |> radix_sort u64.num_bits u64.get_bit
          |> reverse
          |> take 3
          |> foldl (*) 1
    
    def part2 (positionCount: i64) (distances: []((i64, i64), f64)) (positions: []position)
      = let circuitMap: *[positionCount](option u16) = replicate positionCount empty in
        ( loop (circuitCount, connectionIndex, circuits) = (0, 0, circuitMap)
          while not
            ( and (map is_present circuits)
            && and (map (== circuits[0]) circuits)
            )
          do
            let connection = distances[connectionIndex].0 in
            let (newCircuitId, circuits') = connect circuits circuitCount connection in
            (newCircuitId, connectionIndex+1, circuits')
        ).1
        |> \ i -> distances[i-1].0
        |> \ (a, b) -> positions[a].0 * positions[b].0
    
    def main [n] (position_array: [n][3]u32)
      = let positions = map positionFromArray position_array in
        let unsorted_distances = distance_array positions in
        let sorted_distances = sort_distances unsorted_distances in
        ( part1 n inputConnectionCount sorted_distances
        , part2 n sorted_distances positions
        )
    

  • source
  • [–] 3 points 11 months ago

    Go

    God damn it, I thought I would never see the end of part 1. I had a hard time finding a good representation and then I failed at not eliminating valid distances etc. The code is quite messy, but I did it!!!

    day08.go

    package main
    
    import (
    	"aoc/utils"
    	"cmp"
    	"math"
    	"slices"
    	"strconv"
    	"strings"
    )
    
    type pos3D struct {
    	x, y, z int
    }
    
    func (p pos3D) Compare(other pos3D) int {
    	dx := cmp.Compare(p.x, other.x)
    	if dx != 0 {
    		return dx
    	}
    
    	dy := cmp.Compare(p.y, other.y)
    	if dy != 0 {
    		return dy
    	}
    
    	return cmp.Compare(p.z, other.z)
    }
    
    func (p pos3D) distance(other pos3D) float64 {
    	dx := float64(other.x - p.x)
    	dy := float64(other.y - p.y)
    	dz := float64(other.z - p.z)
    
    	d2 := math.Pow(dx, 2.0) + math.Pow(dy, 2.0) + math.Pow(dz, 2.0)
    	return math.Sqrt(d2)
    }
    
    func getPosChannel(input chan string) chan pos3D {
    	ch := make(chan pos3D, cap(input))
    
    	go func() {
    		for line := range input {
    			parts := strings.Split(line, ",")
    			x, _ := strconv.Atoi(parts[0])
    			y, _ := strconv.Atoi(parts[1])
    			z, _ := strconv.Atoi(parts[2])
    			ch <- pos3D{x, y, z}
    		}
    		close(ch)
    	}()
    
    	return ch
    }
    
    type circuits struct {
    	circuits map[pos3D]int
    	nextID   int
    }
    
    func (cc *circuits) newCircuit() (id int) {
    	id = cc.nextID
    	cc.nextID++
    	return id
    }
    
    func (cc *circuits) mergeCircuits(id1, id2 int) {
    	for p, id := range cc.circuits {
    		if id == id2 {
    			cc.circuits[p] = id1
    		}
    	}
    }
    
    func createSingletonCircuits(points []pos3D) (cc circuits) {
    	cc.circuits = make(map[pos3D]int)
    	for _, p := range points {
    		id := cc.newCircuit()
    		cc.circuits[p] = id
    	}
    	return cc
    }
    
    func (cc circuits) reverseMap() (m map[int][]pos3D) {
    	m = make(map[int][]pos3D)
    
    	for p, id := range cc.circuits {
    		if _, ok := m[id]; !ok {
    			m[id] = []pos3D{}
    		}
    
    		m[id] = append(m[id], p)
    	}
    
    	return m
    }
    
    func (cc circuits) sizeMap() map[int]int {
    	circuitSizeMap := make(map[int]int)
    	for _, id := range cc.circuits {
    		circuitSizeMap[id]++
    	}
    	return circuitSizeMap
    }
    
    type targetedDistance struct {
    	distance float64
    	p1, p2   pos3D
    }
    
    func (p pos3D) distanceWithAll(
    	points []pos3D,
    	alreadyVisited *map[pos3D]any,
    ) (tds []targetedDistance) {
    	tds = []targetedDistance{}
    	for _, op := range points {
    		if _, ok := (*alreadyVisited)[op]; ok {
    			continue
    		}
    
    		var td targetedDistance
    		td.distance = math.MaxFloat64
    		td.p1 = p
    		d := p.distance(op)
    		if d < td.distance {
    			td.distance = d
    			td.p2 = op
    		}
    		tds = append(tds, td)
    	}
    	return tds
    }
    
    type pointPair [2]pos3D
    
    func (pp pointPair) equals(other pointPair) bool {
    	pp1 := pp[0]
    	pp2 := pp[1]
    	o1 := other[0]
    	o2 := other[1]
    	return (pp1 == o1 && pp2 == o2) || (pp1 == o2 && pp2 == o1)
    }
    
    var IterationCount = 1000
    
    func stepOne(input chan string) (int, error) {
    	ch := getPosChannel(input)
    
    	points := []pos3D{}
    	for p := range ch {
    		points = append(points, p)
    	}
    	slices.SortFunc(points, pos3D.Compare)
    	cc := createSingletonCircuits(points)
    
    	alreadyVisited := make(map[pos3D]any)
    	tds := []targetedDistance{}
    	for _, p := range points {
    		alreadyVisited[p] = nil
    		dsts := p.distanceWithAll(points, &alreadyVisited)
    		tds = append(tds, dsts...)
    	}
    
    	slices.SortFunc(tds, func(a, b targetedDistance) int {
    		return cmp.Compare(a.distance, b.distance)
    	})
    
    	for idx := range IterationCount {
    		td := tds[idx]
    		cc.mergeCircuits(cc.circuits[td.p1], cc.circuits[td.p2])
    	}
    
    	circuitSizeMap := cc.sizeMap()
    
    	circuitSizes := []int{}
    	for _, v := range circuitSizeMap {
    		circuitSizes = append(circuitSizes, v)
    	}
    	slices.Sort(circuitSizes)
    	largestThree := circuitSizes[len(circuitSizes)-3:]
    
    	product := 1
    	for _, v := range largestThree {
    		product *= v
    	}
    
    	return product, nil
    }
    
    func stepTwo(input chan string) (int, error) {
    	ch := getPosChannel(input)
    
    	points := []pos3D{}
    	for p := range ch {
    		points = append(points, p)
    	}
    	slices.SortFunc(points, pos3D.Compare)
    	cc := createSingletonCircuits(points)
    
    	alreadyVisited := make(map[pos3D]any)
    	tds := []targetedDistance{}
    	for _, p := range points {
    		alreadyVisited[p] = nil
    		dsts := p.distanceWithAll(points, &alreadyVisited)
    		tds = append(tds, dsts...)
    	}
    
    	slices.SortFunc(tds, func(a, b targetedDistance) int {
    		return cmp.Compare(a.distance, b.distance)
    	})
    
    	idx := 0
    	var lastConnection pointPair
    
    	for {
    		td := tds[idx]
    		idx++
    		cc.mergeCircuits(cc.circuits[td.p1], cc.circuits[td.p2])
    
    		circuitSizeMap := cc.sizeMap()
    		if len(circuitSizeMap) == 1 {
    			lastConnection = pointPair{td.p1, td.p2}
    			break
    		}
    	}
    
    	return lastConnection[0].x * lastConnection[1].x, nil
    }
    
    func main() {
    	inputFile := utils.FilePath("day08.txt")
    	utils.RunStep(utils.ONE, inputFile, stepOne)
    	utils.RunStep(utils.TWO, inputFile, stepTwo)
    }
    

  • source
  • [–] 4 points 11 months ago* (last edited 11 months ago) (5 children)

    Kotlin

    First tricky puzzle today, instantly leading me to more or less brute force for part2. I took a lot of time to understand which data structures I needed today, and how to compute my matrix.

    Runtimes:
    part1: 141ms
    part2: 45.9 seconds .-.

    Solution

    class Day08 : Puzzle {
    
        lateinit var boxes: List<Point3D>
        lateinit var edges: List<Pair<List<Point3D>, Double>>
    
        override fun readFile() {
            val input = readInputFromFile(2025, 8, false)
            boxes = input.lines().filter { it.isNotBlank() }
                .map { it.split(",") }
                .map { Point3D(it[0].toInt(), it[1].toInt(), it[2].toInt()) }
            edges = calculateEdges(boxes)
        }
    
        private fun calculateEdges(boxes: List<Point3D>): List<Pair<List<Point3D>, Double>> {
            val edges = mutableListOf<Pair<MutableList<Point3D>, Double>>()
            for (i in boxes.indices) {
                for (j in i + 1 until boxes.size) {
                    edges.add(Pair(mutableListOf(boxes[i], boxes[j]), boxes[i].dist(boxes[j])))
                }
            }
            return edges.sortedBy { it.second }
        }
    
        override fun solvePartOne(): String {
            val connections = buildEmptyConnectionMatrix(boxes.size)
            val mutableEdges = edges.toMutableList()
            for (i in 0..<1000) {
                connectNextEdge(mutableEdges, connections)
            }
    
            val connectionSet = buildConnectionSet(boxes, connections)
    
            return connectionSet.map { it.size }
                .sortedByDescending { it }
                .take(3)
                .reduce(Int::times)
                .toString()
        }
    
        override fun solvePartTwo(): String {
            val connections = buildEmptyConnectionMatrix(boxes.size)
            val mutableEdges = edges.toMutableList()
    
            var result: Long? = null
            while (result == null) {
                result = connectNextEdge(mutableEdges, connections, true)
            }
    
            return result.toString()
        }
    
        // size: width & height of (square) matrix
        private fun buildEmptyConnectionMatrix(size: Int): Array<Array<Boolean>> {
            val connections = Array(size) { Array(size) { false } }
            for (i in connections.indices) {
                connections[i][i] = true
            }
            return connections
        }
    
        private fun connectNextEdge(mutableEdges: MutableList<Pair<List<Point3D>, Double>>, connections: Array<Array<Boolean>>, part2: Boolean = false): Long? {
            if (mutableEdges.isEmpty()) return null
            val next = mutableEdges[0]
    
            val point = next.first[0]
            val other = next.first[1]
            connectAll(boxes.indexOf(point), boxes.indexOf(other), connections)
            mutableEdges.remove(next)
    
            // all nodes are connected, assume that this is happening for the first time
            return if (part2 && connections[0].all { it }) {
                next.first[0].x.toLong() * next.first[1].x.toLong()
            } else {
                null
            }
        }
    
        private fun connectAll(index: Int, other: Int, connections: Array<Array<Boolean>>) {
            fun connectHelper(hIndex: Int) {
                val newConnections = mutableSetOf<Int>()
                for (i in connections[hIndex].indices) {
                    if (connections[hIndex][i]) newConnections.add(i)
                }
                for (boxIndex in newConnections.filter { it != hIndex }) {
                    for (conn in newConnections.filter { it != boxIndex }) {
                        connections[boxIndex][conn] = true
                    }
                }
            }
            connections[index][other] = true
    
            connectHelper(index) // update matrix with all values from node at [index]
            connectHelper(other) // update matrix with all values from node at [other]
        }
    
        // returns 2D-list of all indices of currently active connections
        private fun buildConnectionSet(boxes: List<Point3D>, connections: Array<Array<Boolean>>): Set<List<Int>> {
            val connectionSet = mutableSetOf<List<Int>>()
            val indices = (0 until boxes.size).toMutableList()
            while (indices.isNotEmpty()) {
                val list = mutableListOf<Int>()
                val curr = indices.removeFirst()
                val array = connections[curr]
                for (j in array.indices) {
                    if (array[j]) list.add(j)
                }
                connectionSet.add(list)
            }
            return connectionSet
        }
    
        data class Point3D(val x: Int, val y: Int, val z: Int) {
            fun dist(other: Point3D): Double {
                return sqrt(
                    (x - other.x).toDouble().pow(2) +
                            (y - other.y).toDouble().pow(2) +
                            (z - other.z).toDouble().pow(2)
                )
            }
        }
    }
    

    full code on Codeberg

  • source
  • [–] 4 points 11 months ago

    Uiua

    This was fun :D

    I had a fun experience just throwing the strings with both numbers and spaces at the parse function. In the online pad, everything worked out fine but running the same code on my input locally gave me a "invalid float literal" error.
    I thought I'd missed some edge case in the real input again, like is often the case.
    Turns out that the Uiua version I used locally had a bug that's fixed in the latest build. For once it wasn't directly my fault ^^

    Run with example input

    Code

    $ 123 328  51 64 
    $  45 64  387 23 
    $   6 98  215 314
    $ *   +   *   +  
    
    # &fras "input-6.txt" β—Œ
    
    Calc ← (
      β†˜β‚‚βŠ›βŠ‚"+*"/β—‡βŠ‚
      ≑◇⨬(/+|/Γ—)
      /+
    )
    
    P₁ ← (
      ⊜(βŠœβ–‘βŠΈβ‰ @ )βŠΈβ‰ @\n
      βŠƒβŠ£β†˜β‚‹β‚
      βŠ™(⍉≑₀◇⋕)
      Calc
    )
    
    Pβ‚‚ ← (
      βŠœβˆ˜βŠΈβ‰ @\n
      ⟜⧻
      βŠ“β‰(Λœβ†―@ )
      ⊜(βŠ™(░≑⋕)βœβ‰(βŠƒ(⊒⊣)β†˜β‚‹β‚))Β¬β€šβ‰‘βŒŸβ‰
      Calc
    )
    
    1_2 [βŠƒP₁Pβ‚‚]
    ≑(&p &pf $"Part _: ")
    
    

  • source
  • [–] 3 points 11 months ago

    Rust

    It's getting spicier, luckily part 2 wasn't really much additional complexity this time. There exists a pretty fancy union-find data structure which would have made representing the subcircuits much faster, but I went with a lazy approach.

    View on github

    Code

    use euclid::default::Point3D;
    use euclid::point3;
    
    fn parse_input(input: &str) -> Vec<Point3D<i64>> {
        input
            .lines()
            .map(|l| {
                let mut parts = l.split(',').map(|p| p.parse::<i64>().unwrap());
                let (x, y, z) = (
                    parts.next().unwrap(),
                    parts.next().unwrap(),
                    parts.next().unwrap(),
                );
                point3(x, y, z)
            })
            .collect()
    }
    
    // Distances between all points. Reflexive and symmetric pairs are skipped,
    // so the Vec's have increasing size, starting at 0.
    fn dists(points: &[Point3D<i64>]) -> Vec<Vec<i64>> {
        points
            .iter()
            .enumerate()
            .map(|(idx, &p1)| {
                points
                    .iter()
                    .take(idx)
                    .map(|&p2| (p2 - p1).square_length())
                    .collect::<Vec<i64>>()
            })
            .collect()
    }
    
    fn sorted_distances(dists: &[Vec<i64>]) -> Vec<(usize, usize, i64)> {
        let mut sorted: Vec<(usize, usize, i64)> = dists
            .iter()
            .enumerate()
            .flat_map(|(i, row)| row.iter().enumerate().map(move |(j, d)| (i, j, *d)))
            .collect();
        sorted.sort_by_key(|(_, _, d)| *d);
        sorted
    }
    
    fn part1(input: String) {
        let points = parse_input(&input);
        let d = dists(&points);
        let sorted = sorted_distances(&d);
    
        let mut circuits: Vec<u32> = (0..points.len() as u32).collect();
        for (i, j, _) in sorted.into_iter().take(1000) {
            let new_circuit = circuits[i];
            let old_circuit = circuits[j];
            if new_circuit != old_circuit {
                for c in circuits.iter_mut() {
                    if *c == old_circuit {
                        *c = new_circuit;
                    }
                }
            }
        }
        let mut sizes: Vec<u32> = vec![0; points.len()];
        for c in circuits {
            sizes[c as usize] += 1
        }
        sizes.sort_unstable();
        let result = sizes.iter().rev().take(3).product::<u32>();
        println!("{result}");
    }
    
    fn part2(input: String) {
        let points = parse_input(&input);
        let d = dists(&points);
        let sorted = sorted_distances(&d);
    
        let mut circuits: Vec<u32> = (0..points.len() as u32).collect();
        for (i, j, _) in sorted.into_iter() {
            let new_circuit = circuits[i];
            let old_circuit = circuits[j];
            if new_circuit != old_circuit {
                let mut all_connected = true;
                for c in circuits.iter_mut() {
                    if *c == old_circuit {
                        *c = new_circuit;
                    }
                    if *c != new_circuit {
                        all_connected = false;
                    }
                }
                if all_connected {
                    let result = points[i].x * points[j].x;
                    println!("{result}");
                    return;
                }
            }
        }
    }
    
    util::aoc_main!();
    

  • source
  • [–] 3 points 11 months ago* (last edited 11 months ago) (2 children)

    Uiua

    Just a messy part1 so far. Plus a moment of pure rage when I realised the trap. (I have no idea what algorithm Lemmy uses to highlight Uiua code, but it's always a surprise.)

    (edit: part 2 now added. I had a stupid error earlier. Takes 12s native or 20+s in the pad.)

    Stupid errorI added elements just until the set first hit size 1, rather than continuing until total number of elements equalled number of points.

    Run it here(for what it's worth)

    # AOC 2025 Day 08
    "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"
    N ← 10
    L ← 20
    # &fras"AOC2025day08.txt"β—Œ # Uncomment these three lines,
    # N ← 1000                 # drop your file onto this pane and correct that
    # L ← 1000                 # filename, to run this against your data.
    # You will need to change settings>execution time to 30s or more.
    
    Row ← (
      βŠ™(βŠƒβŠ’β†˜β‚)
      βŠšβ—‘β‰‘βŒŸβ—‡(/+˜∊)
      ⨬(βŠ‚βŠ™β–‘β—Œ                     # Not found: new circuit
      | ⍜⊑(βœΒ°β–‘(β—΄βŠ‚))⊒             # One found: add
      | β–½>β‚€βŠΈβ‰‘β—‡β§»βœ(⊏|βŠ‚{[]}{∘}β—΄/β—‡βŠ‚) # Connect two
      )βŠΈβ§»β—΄
    )
    Parse     ← ⍆⋕°csv
    Sort      ← ⊏⍏/+ⁿ2/-βŠΈβ‰βŠΈβ§…>2
    IndexPrep ← {[°⊟]}βŠΈΒ°βŠ‚βŠΈβ‰‘βŒŸβ‚Λœβ¨‚
    Part₁     ← βŠ™β—Œ/×↙3β‡Œβ†β‰‘β—‡β§»β₯Row(-1N)
    Partβ‚‚     ← /Γ—βŠ’β‰βŠβŠ£β†˜Β―β§»β—Œβ’(Row|<L/+≑◇⧻)
    
    βŠƒ(&p⍜nowPart₁)(&p⍜nowPartβ‚‚) &p⍜now(IndexPrep Sort Parse)
    
  • source
  • [–] 3 points 11 months ago* (last edited 11 months ago)

    I went here for graphs (to find connected components) and binary search (for efficiency)

    #!/usr/bin/env python3
    from pathlib import Path
    
    import numpy as np
    import networkx as nx
    
    cwd = Path(__file__).parent.resolve()
    
    
    def parse_input(file_path):
      with file_path.open("r") as fp:
        coords = list(map(lambda x: list(map(int, x.split(','))), fp.readlines()))
    
      return np.array(coords)
    
    
    def binary_search(G, dist):
    
      Itriu = np.triu_indices(dist.shape[0], k=1)
      dist = dist[Itriu]
    
      components = list(nx.connected_components(G))
      Isort = [(Itriu[0][i], Itriu[1][i]) for i in np.argsort(dist)]
    
      icurrent =  0
      ihist = []
      nhist =  []
    
      while len(ihist)<2 or ihist[-1]!=ihist[-2]:
    
        ihist.append(icurrent)
        G.add_edges_from(Isort[:icurrent])
        components = list(nx.connected_components(G))
        nhist.append(len(components))
    
        G.remove_edges_from(Isort[:icurrent])
    
        if len(components)>1:
          # if >1 component, go between this and closest next index with 1 component
          I = [ind for ind,n in enumerate(nhist) if n==1]
    
          if len(I)==0:
            inext = len(Isort)
          else:
            inext = min(np.array(ihist)[I])
    
          icurrent = icurrent + int(np.ceil((inext-icurrent)/2))
    
        else: 
          # if 1 component, go between nearest previous >1 components and this index
          I = [ind for ind,n in enumerate(nhist) if n>1]
    
          if len(I)==0:
            iprev = 0
          else:
            iprev = max(np.array(ihist)[I])
    
          icurrent = iprev + int(np.ceil((icurrent-iprev)/2))
    
      return Isort[icurrent-1]
    
    
    def solve_problem(file_name, nconnect):
    
      coordinates = parse_input(Path(cwd, file_name))
      G = nx.Graph()
      G.add_nodes_from(range(coordinates.shape[0]))
    
      dist = np.sqrt(np.sum((coordinates[None,:] - coordinates[:,None])**2,
                            axis=-1))
    
      if nconnect != -1: # part 1
        Itriu = np.triu_indices(dist.shape[0], k=1)
        dist = dist[Itriu]
        Isort = [(Itriu[0][i], Itriu[1][i]) for i in np.argsort(dist)[:nconnect]]
    
        G.add_edges_from(Isort)
        components = sorted(list(nx.connected_components(G)), key=len)[::-1]
    
        return np.prod(list(map(len, components[:3])))
      else: # part 2
        indices = binary_search(G, dist)
        return np.prod(coordinates[indices,:],axis=0)[0]
    
    
    if __name__ == "__main__":
    
      assert solve_problem("test_input", 10) == 40
      assert solve_problem("input", 1000) == 115885
      assert solve_problem("test_input", -1) == 25272
      assert solve_problem("input", -1) == 274150525
    
    
  • source
  • [–] [S] 2 points 11 months ago

    Rust

    Not an easy one, mostly because I messed up my merge circuits function (Pop/remove first circuit, load into second, but the pop/remove messed up the indexes, so I was merging the wrong thing).

    After getting that it was all good. Pt2 was trivial once pt1 was solved.

    spoiler

        #[derive(Debug)]
        struct Circuits {
            circuits: Vec<Vec<usize>>,
        }
    
        impl Circuits {
            fn find_circuit(&mut self, i: usize) -> Option<usize> {
                for (j, c) in &mut self.circuits.iter().enumerate() {
                    if c.contains(&i) {
                        return Some(j);
                    }
                }
                None
            }
    
            fn new_circuit(&mut self, i: usize, j: usize) {
                self.circuits.push(vec![i, j]);
            }
    
            fn join_circuits(&mut self, i: usize, j: usize) {
                let mut other = self.circuits.get(j).unwrap().clone();
                self.circuits.get_mut(i).unwrap().append(&mut other);
                self.circuits.remove(j);
            }
    
            fn append_circuit(&mut self, c: usize, x: usize) {
                self.circuits.get_mut(c).unwrap().push(x);
            }
    
            fn top_three(&mut self) -> Vec<usize> {
                let mut sizes = self
                    .circuits
                    .iter()
                    .map(|c| c.len())
                    .collect::<Vec<usize>>();
                sizes.sort();
                sizes.reverse();
                sizes[0..3].to_vec()
            }
        }
    
        type Pole = (isize, isize, isize);
    
        fn dist_poles(p1: &Pole, p2: &Pole) -> f32 {
            sqrt(
                ((p1.0 - p2.0) * (p1.0 - p2.0)) as f32
                    + ((p1.1 - p2.1) * (p1.1 - p2.1)) as f32
                    + ((p1.2 - p2.2) * (p1.2 - p2.2)) as f32,
            )
        }
    
        #[test]
        fn test_y2025_day8_part1() {
            let input = include_str!("../../input/2025/day_8.txt");
            let poles = input
                .lines()
                .map(|l| {
                    let [x, y, z] = l
                        .splitn(3, ",")
                        .map(|c| c.parse::<isize>().unwrap())
                        .collect::<Vec<isize>>()[..]
                    else {
                        panic!();
                    };
                    (x, y, z)
                })
                .collect::<Vec<Pole>>();
            let len = poles.len();
    
            let mut circuits: Circuits = Circuits { circuits: vec![] };
    
            let mut pairs = vec![];
    
            for i in 0..len {
                let first = poles.get(i).unwrap();
                for j in i + 1..len {
                    if i == j {
                        continue;
                    }
                    let second = poles.get(j).unwrap();
                    let dist = dist_poles(first, second);
                    pairs.push((dist, i, j));
                }
            }
    
            pairs.sort_by(|a, b| {
                if a.0 < b.0 {
                    Ordering::Less
                } else {
                    Ordering::Greater
                }
            });
    
            for (dist, a, b) in pairs[0..1000].iter() {
                let first_circuit = circuits.find_circuit(*a);
                let second_circuit = circuits.find_circuit(*b);
    
                match (first_circuit, second_circuit) {
                    (None, None) => {
                        circuits.new_circuit(*a, *b);
                    }
                    (Some(c), None) => {
                        circuits.append_circuit(c, *b);
                    }
                    (None, Some(c)) => {
                        circuits.append_circuit(c, *a);
                    }
                    (Some(c1), Some(c2)) => {
                        if c1 != c2 {
                            circuits.join_circuits(c1, c2);
                        }
                    }
                }
            }
    
            assert_eq!(circuits.top_three().iter().product::<usize>(), 66640)
        }
    

  • source
  • [–] [S] 1 point 11 months ago (2 children)

    I used a u8, one bit per level to do mine, but because not all bits correspond to a splitter, it ends up duplicating a lot of the paths. Which is why I made the lowest bit at the top, so it flip-flops a lot, giving the appearance of multiple different paths :D

  • source
  • parent
  • context
  • [–] 2 points 11 months ago

    Python

    I had a hunch that part 2 would just be part one with more places and immediately factored that code into a separate method. Overall quite happy with the compact code.

    from functools import partial
    from pathlib import Path
    from typing import List
    
    
    def parse_input(input: str) -> List[List[int]]:
        return [list(map(int, l)) for l in input.splitlines()]
    
    
    def find_highest(bank: List[int], n: int) -> int:
        if n == 1:
            return max(bank)
        rest = bank[bank[:-n+1].index(place := max(bank[:-n+1]))+1:]
        return int(f"{place}{find_highest(rest, n-1)}")
    
    
    def part_one(input: str) -> int:
        bat_banks = parse_input(input)
        return sum(map(partial(find_highest, n = 2), bat_banks))
    
    
    def part_two(input: str) -> int:
        bat_banks = parse_input(input)
        return sum(map(partial(find_highest, n = 12), bat_banks))
    
    
    if __name__ == "__main__":
        input = Path("_2025/_3/input").read_text("utf-8")
        print(part_one(input))
        print(part_two(input))
    
  • source
  • [–] 3 points 11 months ago (3 children)

    Well it uses the input of course but I found that if you make it truly random, the lines mostly go down in a straight line, all bunching up in the middle, which isn't very pleasing. So now the beams have a "current direction" which has a 25% of flipping at every splitter

  • source
  • parent
  • context
  • [–] 5 points 11 months ago* (last edited 11 months ago) (1 child)

    Nim

    view code

    type
      AOCSolution[T,U] = tuple[part1: T, part2: U]
      Vec3 = tuple[x,y,z: int]
      Node = ref object
        pos: Vec3
        cid: int
    
    proc dist(a,b: Vec3): float =
      sqrt(float((a.x-b.x)^2 + (a.y-b.y)^2 + (a.z-b.z)^2))
    
    proc solve(input: string, p1_limit: int): AOCSolution[int, int] =
      let boxes = input.splitLines().mapIt:
        let parts = it.split(',')
        let pos = Vec3 (parseInt parts[0], parseInt parts[1], parseInt parts[2])
        Node(pos: pos, cid: -1)
    
      var dists: seq[(float, (Node, Node))]
      for i in 0 .. boxes.high - 1:
        for j in i+1 .. boxes.high:
          dists.add (dist(boxes[i].pos, boxes[j].pos), (boxes[i], boxes[j]))
    
      var curcuits: Table[int, HashSet[Node]]
      var curcuitID = 0
    
      dists.sort(cmp = proc(a,b: (float, (Node, Node))): int = cmp(a[0], b[0]))
    
      for ind, (d, nodes) in dists:
        var (a, b) = nodes
        let (acid, bcid) = (a.cid, b.cid)
    
        if acid == -1 and bcid == -1: # new curcuit
          a.cid = curcuitID
          b.cid = curcuitID
          curcuits[curcuitId] = [a, b].toHashSet
          inc curcuitID
        elif bcid == -1: # add to a
          b.cid = acid
          curcuits[acid].incl b
        elif acid == -1: # add to b
          a.cid = bcid
          curcuits[bcid].incl a
        elif acid != bcid: # merge two curcuits
          for node in curcuits[bcid]:
            node.cid = acid
          curcuits[acid].incl curcuits[bcid]
          curcuits.del bcid
    
        if ind+1 == p1_limit:
          result.part1 = curcuits.values.toseq.map(len).sorted()[^3..^1].prod
    
        if not(acid == bcid and acid != -1): result.part2 = a.pos.x * b.pos.x
    

    Runtime: 364 ms

    Part 1:
    I compute all pairs of Euclidean distances between 3D points, sort them, then connect points into circuits, using, what I think is called a Union‑Find algorithm (circuits grow or merge). After exactly 1000 connections (including redundant ones), I take the three largest circuits and multiply their sizes.

    Part 2:
    While iterating through the sorted connections, I also calculate the product of each pair x‑coordinates. The last product is a result for part 2.

    Problems I encountered while doing this puzzle:

    • I've changed a dozen of data structures, before settled on curcuitIDs and ref objects stored in a HashTable (~ 40 min)
    • I did a silly mistake of mutating the fields of an object and then using new fields as keys for the HashTable (~ 20 min)
    • I am stil confused and don't understand why do elves count already connected junction boxes (~ 40 min, had to look it up, otherwise it would be a lot more)

    Time to solve Part 1: 1 hour 56 minutes
    Time to solve Part 2: 4 minutes

    Full solution at Codeberg: solution.nim

  • source
  • [–] 2 points 11 months ago

    Python

    I love it when I can just slap a problem with a regex and be done with it.

    import re
    
    from pathlib import Path
    from typing import Generator
    
    def parse_input(input: str) -> Generator[range, None, None]:
        for r in input.split(","):
            v = r.split("-")
            yield range(int(v[0]), int(v[1]) + 1)
    
    
    def solve(input: str, pattern: str) -> int:
        exp = re.compile(pattern)
        return sum(
            map(
                lambda m: int(m.group()),
                filter(
                    None,
                    map(lambda s: exp.match(s), [n for r in parse_input(input) for n in map(str, r)])
                )
            )
        )
    
    
    if __name__ == "__main__":
        input = Path("_2025/_2/input").read_text("utf-8")
        print(solve(input, r"^(\d+)\1$"))
        print(solve(input, r"^(\d+)\1+$"))
    
  • source
  • [–] 2 points 11 months ago*

    Python

    Solved part 1 in a matter of minutes, spent literal days tinkering with part 2 before resorting to brute force, just to finally get a correct result, which allowed me to fix the more optimised approach. Brute force still turned out surprisingly performant, scaling linearly in time with the rotation distance and sub-linearly in memory.

    from itertools import accumulate
    from math import copysign
    from pathlib import Path
    from typing import List
    
    
    def parse_input(input: str) -> List[int]:
        return list(map(lambda s: int(f"{'-' if s[0] == 'L' else ''}{s[1:]}"), input.splitlines()))
    
    
    def part_one(input: str) -> int:
        return len(list(filter(lambda x: x == 0, accumulate(parse_input(input), lambda v, i: (i + v) % 100, initial=50))))
    
    
    def part_two(input: str) -> int:
        v, c = 50, 0
        for i in parse_input(input):
            c += i // (d := int(copysign(100, i)))  # full rotations
            count_underflow = v != 0  # was counted in previous iteration
            v += (i % d)  # remainder
            if count_underflow:
                c += abs(v // 100)  # under-/overflows
            if v == 0:
                c += 1
            v %= 100
        return c
    
    
    if __name__ == "__main__":
        input = Path("_2025/_1/input").read_text("utf-8")
        print(part_one(input))
        print(part_two(input))
    

    Brute force part 2

    def part_two(input: str) -> int:
        from itertools import chain
        return len(list(filter(
            lambda x: x == 0,
            accumulate(chain(*[[int(copysign(1, i))] * abs(i) for i in parse_input(input)]), lambda v, i: (i + v) % 100, initial = 50)
        )))
    
  • source
  • [–] 5 points 11 months ago* (2 children)

    Haskell

    They're getting interesting now!

    import Control.Monad  
    import Data.List  
    import Data.List.Split  
    import Data.Ord  
    import Data.Set qualified as Set  
    
    readPos = (\([x, y, z] :: [Int]) -> (x, y, z)) . map read . splitOn ","  
    
    pairs = init . tails >=> (\(x : xs) -> map (x,) xs)  
    
    dist (x1, y1, z1) (x2, y2, z2) =  
      (x2 - x1) ^ 2 + (y2 - y1) ^ 2 + (z2 - z1) ^ 2  
    
    connect circuits (a, b) =  
      let (joined, rest) =  
            partition (\c -> a `Set.member` c || b `Set.member` c) circuits  
       in Set.unions joined : rest  
    
    main = do  
      boxes <- map readPos . lines <$> readFile "input08"  
      let steps =  
            (zip <*> tail . scanl' connect (map Set.singleton boxes)) $  
              sortOn (uncurry dist) (pairs boxes)  
      print . product . take 3 . sortOn Down . map Set.size $  
        (snd . last . take 1000 $ steps)  
      let Just (((x1, _, _), (x2, _, _)), _) =  
            find ((== 1) . length . snd) steps  
       in print $ x1 * x2  
    
  • source
  • view more: β€Ή prev next β€Ί