[–] 5 points 11 months ago* (last edited 11 months ago) (2 children)

Python

Both of my solutions make use of a Heap and a Disjoint-Set-Union / Union-Find data structure

Part 1: Use a heap to keep 1000 shortest connections, then union them all in DSU and get the 3 largest connected components.
Part 2: Use a heap to keep all connections, then union the shortest ones until all components are connected.

click to view code

import heapq as hq
from itertools import combinations
from collections import Counter

# Disjoint Set Union (or Union-Find) data structure
#   with path compression and union by rank
class DSU:
    def __init__(self, size: int) -> None:
        self.parent = list(range(size)) # ith value is the parent node to node i
        self.rank = [1] * size          # max depth of subtree rooted here (used for union by rank)
        
    def find(self, x: int) -> int:
        # if the node is not its own parent, we need to recurse on parent
        if x != self.parent[x]:
            # path compression
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]
    
    def isConnected(self, x: int, y: int) -> bool:
        return self.find(x) == self.find(y)
    
    # returns a boolean whether or not union was needed
    def union(self, x: int, y: int) -> bool:
        rootX = self.find(x)
        rootY = self.find(y)        
        
        if rootX == rootY:
            # no union needed
            return False
        
        if self.rank[rootX] > self.rank[rootY]:
            # rootX has deeper subtree, therefore set it as parent to rootY (and its subtree)
            self.parent[rootY] = rootX
        elif self.rank[rootX] < self.rank[rootY]:
            # rootY has deeper subtree, therefore set it as parent to rootX (and its subtree)
            self.parent[rootX] = rootY
        else:
            # both subtrees are of equal depth, therefore choose either one of them as the parent of the other
            # here we chose rootX as the parent of rootY, therefore rootX depth increases by 1
            self.parent[rootY] = rootX
            self.rank[rootX] += 1
        
        # union complete
        return True

# parse puzzle input into junction coordinates (x, y, z)
def parse_junctions(data: str):
    return [tuple(map(int, line.split(','))) for line in data.splitlines()]

# get distance between two points in 3D space
# skipping the sqrt because we only care about relative distances
def get_dist(p1, p2):
    x1, y1, z1 = p1
    x2, y2, z2 = p2
    return (x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2

def part1(data: str, max_connections: int = 1000):
    junctions = parse_junctions(data)
    n = len(junctions)

    # <max_connections> shortest connections
    closest_connections = []
    # iterate over all pairs of junctions
    # combinations is just some syntactic sugar to avoid a nested for loop (still O(n^2))
    for i, j in combinations(range(n), 2):
        dist = get_dist(junctions[i], junctions[j])
        # keep <max_connections> closest connections
        #   have to use -dist to simulate max heap
        if len(closest_connections) < max_connections:
            hq.heappush(closest_connections, (-dist, i, j))
        else:
            hq.heappushpop(closest_connections, (-dist, i, j))

    # union all the closest connections
    dsu = DSU(n)
    for _, i, j in closest_connections:
        dsu.union(i, j)

    # count all circuit sizes
    circuit_sizes = Counter()
    for i in range(n):
        circuit_sizes[dsu.find(i)] += 1

    # multiply the sizes of the 3 largest circuits
    ans = 1
    for _, f in circuit_sizes.most_common(3):
        ans *= f
    return ans

def part2(data: str):
    junctions = parse_junctions(data)
    n = len(junctions)

    # all n^2 junction connections
    all_connections = []
    # combinations is just some syntactic sugar to avoid a nested for loop (still O(n^2))
    for i, j in combinations(range(n), 2):
        dist = get_dist(junctions[i], junctions[j])
        all_connections.append((dist, i, j))
    
    # create min heap of all connections
    # Heapify later to benefit from the heapify algorithm (O(n) instead of O(n log n))
    hq.heapify(all_connections)
    
    # connect junctions until all are connected
    dsu = DSU(n)
    unions = 0
    while all_connections:
        _, i, j = hq.heappop(all_connections)
        unions += dsu.union(i, j)
        # if we have n-1 unions, that means all n junctions are connected
        if unions == n-1:
            return junctions[i][0] * junctions[j][0]
    
    assert False, "It's not possible that all junctions never connect"

sample = """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"""
assert part1(sample, max_connections=10) == 40
assert part2(sample) == 25272

  • source
  • [–] 3 points 11 months ago

    I normally use the "most appropriate" size, but this year I'm just using usize everywhere and it's a lot more fun. So far I haven't found any problem where it ran too slow and the problem was in my implementation and not my algorithm choice.

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

    In 16-bit real mode assembly, as a hybrid DOS .COM program and BIOS-bootable disk image. With bonus palette animations! I had the hybrid thing going on before (see the repo), but this is the first time getting something animated.

    Repo | day07.com (12 KB) | full video

    video

  • source
  • [–] 3 points 11 months ago*

    Janet

    I hate dynamic programming I hate dynamic programming I hate dynamic programming I hate dynamic programming I hate dynamic programming I hate dynamic programming I hate dynamic programming I hate dynamic programming I hate dynamic programming I hate dynamic programming

  • source
  • [–] 2 points 11 months ago

    I went for recursion (and a simple graph node class) but my favourite solution is transfer matrices one that I have seen some people do

    
    from pathlib import Path
    import numpy as np
    import itertools as it
    
    cwd = Path(__file__).parent
    
    
    def parse_input(file_path):
      with file_path.open("r") as fp:
        data = list(map(str.strip, fp.readlines()))
    
      return np.array([list(row) for row in data], dtype=object)
    
    
    class Node:
    
      def __init__(self, symbol, row, col):
        self.symbol = symbol
        self.loc = tuple([row, col])
        self.parents = []
        self.npaths_to_start = -1
    
      @property
      def is_connected(self):
        return self.symbol=='S' or len(self.parents)>0
    
      def connect(self, node_dict, s0, s1):
    
        if self.loc[0]==0:
          return
    
        top = node_dict[self.loc[0]-1, self.loc[1]]
    
        if top.symbol in ['.','S'] and top.is_connected:
          self.parents.append(top)
    
        if self.symbol=='^' and top.is_connected:
          left =  node_dict[self.loc[0], self.loc[1]-1]
          right =  node_dict[self.loc[0], self.loc[1]+1]
    
          left.parents.append(self)
          right.parents.append(self)
    
    
    def count_paths_to_start(node0, node1):
      '''
      node0 should always be the start node else
      result is meaningless
      '''
    
      if node0 in node1.parents:
        return 1
      else:
        npaths = 0
        for p in node1.parents:
          if p.npaths_to_start != -1:
            npaths += p.npaths_to_start
          else:
            p.npaths_to_start = count_paths_to_start(node0, p)
            npaths += p.npaths_to_start
    
        return npaths
    
    
    def solve_problem(file_name, quantum=False):
    
      grid = parse_input(Path(cwd, file_name))
      s0,s1 = grid.shape
    
      node_dict = {(i,j):Node(grid[i,j], i, j) for i,j in
                   it.product(range(s0), range(s1))}
      [node.connect(node_dict, s0, s1) for node in node_dict.values()]
    
      if not quantum:
        return len([x for x in node_dict.values() if x.symbol == '^' and
                    x.is_connected>0])
      else:
        start_ind = [(0, j) for j in range(s1) if node_dict[0,j].symbol=='S'][0]
    
        return\
          sum([count_paths_to_start(node_dict[start_ind], node_dict[s0-1, j]) for
               j in range(s1) if node_dict[s0-1,j].is_connected])
    
    
    
    if __name__ == "__main__":
    
      assert solve_problem("test_input") == 21
      assert solve_problem("input") == 1633
      assert solve_problem("test_input", True) == 40
      assert solve_problem("input", True) == 34339203133559
    
  • source
  • [–] 2 points 11 months ago

    I didn't do recursion on part 1, so my part 1 and 2 were fairly different.

    const val start = 'S'
    const val empty = '.'
    const val splitter = '^'
    const val beam = '|'
    
    var width: IntRange = IntRange(0, 0)
    var height: IntRange = IntRange(0, 0)
    val cache: MutableMap<Pair<Int, Int>, Long> = mutableMapOf()
    var map: List<List<Char>> = listOf()
    
    fun main() {
        val input = getInput(7)
        map = parseInput1(input)
        height = map.indices
        width = map[0].indices
        val startLocation = map[0].indexOf(start) to 0
        val splits = moveBeam(startLocation) + 1
        println(splits)
    }
    
    fun parseInput1(input: String): List<List<Char>> = input.lines()
        .filter { it.isNotBlank() }
        .map { it.toCharArray().toList() }
    
    fun moveBeam(beamLocation: Pair<Int, Int>): Long {
        if (cache.containsKey(beamLocation)) {
            return cache[beamLocation]!!
        }
        val belowLocation = beamLocation.first to beamLocation.second + 1
        if (belowLocation.second !in height) {
            return 0L
        }
        if (cache.containsKey(belowLocation)) {
            return cache[belowLocation]!!
        }
        val below = map[belowLocation.second][belowLocation.first]
        var splits = 0L
        if (below == empty) {
            splits = moveBeam(belowLocation)
        } else if (below == splitter) {
            splits++
            val leftLocation = belowLocation.first - 1 to belowLocation.second
            val left = if (leftLocation.first in width) map[leftLocation.second][leftLocation.first] else '!'
            if (left == empty || left == splitter) {
                splits += moveBeam(leftLocation)
            }
            val rightLocation = belowLocation.first + 1 to belowLocation.second
            val right = if (rightLocation.first in width) map[rightLocation.second][rightLocation.first] else '!'
            if (right == empty || right == splitter) {
                splits += moveBeam(rightLocation)
            }
        }
        cache[beamLocation] = splits
        return splits
    }
    
  • source
  • parent
  • context
  • [–] 2 points 11 months ago

    Rust

    Part 1 was quite straightforward: just keep track of a frontier of every beam, and keep following it, adding splitter locations to a set. Easy.

    I had to give some thought to Part 2 and ended up doing dynamic programming: for every position, keep track of how many timelines go through it, solving recursively where necesary.

    use std::collections::{HashMap, HashSet, VecDeque};
    
    use crate::{grid::{Coordinate, Direction, Grid}, solver::DaySolver};
    
    pub struct Day07Solver;
    
    impl DaySolver for Day07Solver {
        fn part1(&self, input: String) -> String {
            let grid = Grid::from_grid_string(
                &input,
                |c| match c {
                    'S' => Location::Start,
                    '^' => Location::Splitter,
                    '.' => Location::Empty,
                    _ => panic!("Invalid location type"),
                }
            );
    
            let mut reached_splitters: HashSet<Coordinate> = HashSet::new();
    
            let mut beam_coordinates: VecDeque<Coordinate> = grid.iter()
                .filter_map(|(c, l)| match l {
                    Location::Start => Some(c),
                    _ => None,
                })
                .collect();
    
            while let Some(next) = beam_coordinates.pop_front() {
                if let Some(next_coord) = grid.neighbor_in_direction(next, Direction::Down) {
                    match grid[next_coord] {
                        Location::Start => { panic!("Encountered a second start!"); },
                        Location::Splitter => {
                            if !reached_splitters.contains(&next_coord) {
                                reached_splitters.insert(next_coord);
                                [
                                    grid.neighbor_in_direction(next_coord, Direction::Left),
                                    grid.neighbor_in_direction(next_coord, Direction::Right),
                                ]
                                    .into_iter()
                                    .flatten()
                                    .for_each(|c| beam_coordinates.push_back(c));
                            }
                        },
                        Location::Empty => { beam_coordinates.push_back(next_coord); }
                    }
                }
            }
    
            reached_splitters
                .len()
                .to_string()
        }
    
        fn part2(&self, input: String) -> String {
            let grid = Grid::from_grid_string(
                &input,
                |c| match c {
                    'S' => Location::Start,
                    '^' => Location::Splitter,
                    '.' => Location::Empty,
                    _ => panic!("Invalid location type"),
                }
            );
    
            let start_coord = grid.iter().find(|(_, l)| **l == Location::Start).unwrap().0;
    
            let mut num_timelines: HashMap<Coordinate, usize> = HashMap::new();
            count_timelines(&mut num_timelines, &grid, 1, start_coord);
    
            num_timelines[&start_coord].to_string()
        }
    }
    
    fn count_timelines(
        num_timelines: &mut HashMap<Coordinate, usize>,
        grid: &Grid<Location>,
        timelines_so_far: usize,
        coordinate: Coordinate,
    ) {
        if num_timelines.contains_key(&coordinate) {
            return
        }
    
        match grid[coordinate] {
            Location::Splitter => {
                let neighbors: Vec<Coordinate> = [
                    grid.neighbor_in_direction(coordinate, Direction::Left),
                    grid.neighbor_in_direction(coordinate, Direction::Right),
                ]
                    .into_iter()
                    .flatten()
                    .collect();
    
                neighbors.iter()
                    .for_each(|next| {
                        count_timelines(num_timelines, grid, timelines_so_far, *next);
                    });
    
                let timelines_from_here = neighbors.iter().map(|c| num_timelines[c]).sum();
                num_timelines.insert(coordinate, timelines_from_here);
            },
            _ => {
                let timelines_from_here = if let Some(next) = grid.neighbor_in_direction(coordinate, Direction::Down) {
                    count_timelines(num_timelines, grid, timelines_so_far, next);
                    num_timelines[&next]
                } else {
                    timelines_so_far
                };
    
                num_timelines.insert(coordinate, timelines_from_here);
            },
        };
    }
    
    #[derive(PartialEq, Eq, Copy, Clone)]
    enum Location {
        Start,
        Splitter,
        Empty,
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn part1() {
            let input = include_str!("../../inputs/test/07");
    
            let solver = Day07Solver {};
            assert_eq!("21", solver.part1(input.to_string()));
        }
    
    
        #[test]
        fn part2() {
            let input = include_str!("../../inputs/test/07");
    
            let solver = Day07Solver {};
            assert_eq!("40", solver.part2(input.to_string()));
        }
    }
    
  • source
  • [–] 4 points 11 months ago* (last edited 11 months ago) (2 children)

    Uiua

    A bit late getting to this, but happy with this solution, despite it being nothing more than just building/summing all paths. As usual, you can drop your own file onto that solution.

    D ← ".......S.......\n...............\n.......^.......\n...............\n......^.^......\n...............\n.....^.^.^.....\n...............\n....^.^...^....\n...............\n...^.^...^.^...\n...............\n..^...^.....^..\n...............\n.^.^.^.^.^...^.\n..............."
    # D ← &fras"AOC2025day07.txt" # <- Uncomment and drop file here.
    
    Parse ← βŠƒβ†˜β†™1β‰ @.βŠœβˆ˜βŠΈβ‰ @\n
    Flow  ← βŠƒ(+βŠƒβ†»β‚β†»β‚‹β‚Γ—|Γ—Β¬)βŠ™βŠΈβŠ£
    P₁    ← /+>0β™­β¬š0Γ—βŸœβˆ§(ΛœβŠ‚β†₯Flow)
    Pβ‚‚    ← /+⊣∧(ΛœβŠ‚+Flow)
    βŠƒP₁ Pβ‚‚ Parse D
    
  • source
  • [–] 2 points 11 months ago

    My choice of representation for part 1 ended up being fortunate, as all I needed to change for part 2 was three setf in propagate that became incf. This was also another good use case for multiple return values, as it's the beam vector you want to reduce on, but for part 1 the split count needs to come along for the ride.

    (defun parse-line (line)
      (let ((result (make-array (length line) :initial-element 0)))
        (loop for i from 0 to (1- (length line))
              if (member (char line i) '(#\^ #\S))
                do (setf (aref result i) 1))
        result))
    
    (defun read-inputs (filename)
      (let ((input-lines (uiop:read-file-lines filename)))
        (mapcar #'parse-line input-lines)))
    
    (defun propagate (in-beams splitters)
      (let ((out-beams (make-array (length in-beams) :initial-element 0))
            (splits 0))
        (loop for i from 0 to (1- (length in-beams))
              if (not (zerop (aref in-beams i)))
                do (if (not (zerop (aref splitters i)))
                       (progn (incf (aref out-beams (1- i)) (aref in-beams i))
                              (incf (aref out-beams (1+ i)) (aref in-beams i))
                              (incf splits))
                       (incf (aref out-beams i) (aref in-beams i))))
        (values out-beams splits)))
    
    (defun propagate-all (initial-beam-vector splitter-map)
      (let* ((total-splits 0)
             (final-beam-vector
               (reduce #'(lambda (in-beams splitters)
                           (multiple-value-bind (out-beams this-splits)
                               (propagate in-beams splitters)
                             (incf total-splits this-splits)
                             out-beams))
                       splitter-map :initial-value initial-beam-vector)))
        (values final-beam-vector total-splits)))
    
    (defun main-1 (filename)
      (let ((grid (read-inputs filename)))
        (multiple-value-bind (final-beam-vector total-splits)
            (propagate-all (car grid) (cdr grid))
          total-splits)))
    
    (defun main-2 (filename)
      (let ((grid (read-inputs filename)))
        (multiple-value-bind (final-beam-vector total-splits)
            (propagate-all (car grid) (cdr grid))
          (reduce #'+ (coerce final-beam-vector 'list)))))
    
  • source
  • [–] 2 points 11 months ago

    Haskell

    import Control.Arrow
    import Control.Monad
    import Control.Monad.Writer.Strict
    import Data.Array
    import Data.Functor
    import Data.List
    import Data.Maybe
    import Data.Monoid
    
    parse content = (elemIndices 'S' x, filter (not . null) $ elemIndices '^' <$> xs)
      where
        (x : xs) = lines content
    
    split :: [Int] -> Int -> Writer (Sum Int) [Int]
    split splitters beam
        | beam `elem` splitters = tell 1 $> [pred beam, succ beam]
        | otherwise = pure [beam]
    
    part1 = getSum . execWriter . uncurry process
      where
        process start =
            foldl'
                (\beams splitters -> nub . concat <$> (beams >>= mapM (split splitters)))
                (pure start)
    
    part2 :: ([Int], [[Int]]) -> Int
    part2 (start, splitterList) = go (head start, 0)
      where
        go (i, j)
            | j >= depth = 1
            | hasSplitter i j = r ! (pred i, succ j) + r ! (succ i, succ j)
            | otherwise = r ! (i, succ j)
    
        r = listArray bounds [go (i, j) | (i, j) <- range bounds]
        bounds = ((0, 0), (width, depth))
    
        hasSplitter i j = j < length splitterList && i `elem` splitterList !! j
    
        depth = length splitterList
        width = succ . maximum $ concat splitterList
    
    main = getContents >>= print . (part1 &&& part2) . parse
    
  • source
  • [–] 2 points 11 months ago

    C

    Accidentally solved part 2 first but had the foresight to save the code. Otherwise my solution looks similar to what other people are doing, just with more code πŸ˜…

    Code

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <inttypes.h>
    #include <assert.h>
    
    #define GW 148
    #define GH 74
    
    static char g[GH][GW];
    static uint64_t dp[2][GW];
    static int w,h;
    
    /* recursively traces beam for part 1, marking visited splitters with 'X' */
    static uint64_t
    solve_p1(int x, int y)
    {
    	if (x<0 || x>=w || y<0 || y>=h || g[y][x] == 'X')
    		return 0;
    	else if (g[y][x] != '^')
    		return solve_p1(x, y+1);
    	else {
    		g[y][x] = 'X';
    		return 1 + solve_p1(x-1, y) + solve_p1(x+1, y);
    	}
    }
    
    /* DP for part 2 */
    static uint64_t
    solve_p2(void)
    {
    	int x,y, odd;
    
    	for (y=h-1; y>=0; y--)
    	for (x=1; x<w-1; x++) {
    		/* only two rows relevant at a time, so don't store any more */
    		odd = y&1;
    
    		if (g[y][x] == 'S') {
    			printf("\n");
    			return dp[!odd][x] + 1;
    		}
    
    		dp[odd][x] = g[y][x] == '^' || g[y][x] == 'X'
    		    ? dp[!odd][x-1] + dp[!odd][x+1] + 1
    		    : dp[!odd][x];
    	}
    
    	return 0;
    }
    
    int
    main()
    {
    	int x,y, sx,sy;
    
    	for (h=0; ; ) {
    		/* one bottom row of padding */
    		assert(h < GH-1);
    		/* input already side padded, plus we have \n\0 */
    		if (!fgets(g[h], GW, stdin))
    			break;
    		/* skip empty rows */
    		for (x=0; g[h][x]; x++)
    			if (g[h][x] == 'S' || g[h][x] == '^')
    				{ h++; break; }
    	}
    
    	w = strlen(g[0])-1; /* strip \n */
    
    	for (y=0; y<h; y++)
    	for (x=0; x<w; x++)
    		if (g[y][x] == 'S')
    			{ sx=x; sy=y; break; }
    
    	printf("07: %"PRIu64" %"PRIu64"\n", solve_p1(sx,sy), solve_p2());
    }
    

  • source
  • view more: β€Ή prev next β€Ί