Day 7: Laboratories

Megathread guidelines

  • Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
  • You can send code in code blocks by using three backticks, the code, and then three backticks or use something such as https://topaz.github.io/paste/ if you prefer sending it through a URL

FAQ

all 31 comments

sorted by: hot top controversial new old
[–] 5 points 10 months ago

QIR (Quantum Intermediate Representation)

Nah, just kidding - I used Rust. The only tricky part seemed to be finding time on a Sunday to get it coded!

Part 1 I swept down with a bool array for beams and part 2 I swept up with a score array and summed when it split (joined).

struct Teleporter(String);

impl Teleporter {
    fn new(s: String) -> Self {
        Self(s)
    }

    fn start_pos(line: &str) -> Result<usize> {
        line.find('S').ok_or_eyre("Start not found")
    }

    fn splits(&self) -> Result<usize> {
        let mut input = self.0.lines();
        let start_line = input.next().ok_or_eyre("No start line")?;

        let mut beams = vec![false; start_line.len()];
        beams[Self::start_pos(start_line)?] = true;

        let mut splits = 0;
        for line in input {
            for (i, ch) in line.bytes().enumerate() {
                if beams[i] && ch == b'^' {
                    splits += 1;
                    beams[i] = false;
                    beams[i - 1] = true;
                    beams[i + 1] = true;
                }
            }
        }
        Ok(splits)
    }

    fn timelines(&self) -> Result<usize> {
        let mut input = self.0.lines();
        let start_line = input.next().ok_or_eyre("No start line")?;
        let start = Self::start_pos(start_line)?;

        let mut num_paths = vec![1; start_line.len()];
        for line in input.rev() {
            for (i, c) in line.bytes().enumerate() {
                if c == b'^' || c == b'S' {
                    num_paths[i] = num_paths[i - 1] + num_paths[i + 1];
                }
            }
        }
        Ok(num_paths[start])
    }
}
  • source
  • [–] 5 points 10 months ago (1 child)

    Rust

    Dynamic programming? Nah, just keep track of the number of overlapping beams and part 2 becomes no different to part 1.

    View on github

    use std::collections::VecDeque;
    
    fn parse_input(input: &str) -> (Vec<Vec<bool>>, (usize, usize)) {
        let splits = input
            .lines()
            .map(|l| l.chars().map(|c| c == '^').collect())
            .collect();
        // Assume start is on first row
        let start = (input.chars().position(|c| c == 'S').unwrap(), 0);
        (splits, start)
    }
    
    fn solve(input: String) {
        let (splits, start) = parse_input(&input);
        let mut nsplits = 0u32;
        let mut timelines = 1u64;
        let mut frontier = VecDeque::from([(start, 1)]);
        while let Some((pos, multiplicity)) = frontier.pop_front() {
            let (x, y) = (pos.0, pos.1 + 1);
            if y == splits.len() {
                // Falls out of bottom
                continue;
            }
            if splits[y][x] {
                nsplits += 1;
                timelines += multiplicity;
                if let Some((b, m2)) = frontier.back_mut()
                    && *b == (x - 1, y)
                {
                    *m2 += multiplicity;
                } else {
                    frontier.push_back(((x - 1, y), multiplicity));
                }
                frontier.push_back(((x + 1, y), multiplicity));
            } else if let Some((b, m2)) = frontier.back_mut()
                && *b == (x, y)
            {
                *m2 += multiplicity;
            } else {
                frontier.push_back(((x, y), multiplicity));
            }
        }
        println!("Part 1: {nsplits}");
        println!("Part 2: {timelines}");
    }
    
    fn main() -> std::io::Result<()> {
        let (input, _) = util::get_input("day7.txt")?;
        solve(input);
        Ok(())
    }
    
  • source
  • hideshow 1 child comment
  • [–] 4 points 10 months ago* (last edited 10 months ago) (1 child)

    Haskell

    That was a fun little problem.

    import Data.Map qualified as Map  
    import Data.Set qualified as Set  
    import Data.Tuple (swap)  
    
    readInput s =  
      Map.fromDistinctAscList  
        [((i, j), c) | (i, l) <- zip [0 ..] $ lines s, (j, c) <- zip [0 ..] l]  
    
    beamPaths input = scanl step (Map.singleton startX 1) [startY .. endY]  
      where  
        Just (startY, startX) = lookup 'S' $ map swap $ Map.assocs input  
        Just ((endY, _), _) = Map.lookupMax input  
        step beams y =  
          Map.unionsWith (+) $  
            [ if input Map.!? (y + 1, j) == Just '^'  
                then Map.fromList [(j - 1, n), (j + 1, n)]  
                else Map.singleton j n  
              | (j, n) <- Map.assocs beams  
            ]  
    
    part1 = sum . map Set.size . (zipWith (Set.\\) <*> tail) . map Map.keysSet . beamPaths  
    
    part2 = sum . last . beamPaths  
    
    main = do  
      input <- readInput <$> readFile "input07"  
      print $ part1 input  
      print $ part2 input  
    
  • source
  • hideshow 1 child comment
  • [–] 1 point 10 months ago*

    And here's a super-simple version, because why not.

    import Data.List (elemIndex, elemIndices)  
    import Data.Map qualified as Map  
    import Data.Maybe (fromJust)  
    import Data.Set qualified as Set  
    
    main = do  
      (start : rows) <- lines <$> readFile "input07"  
      let splitsByRow =  
            zipWith  
              ( \row beams ->  
                  Set.intersection (Map.keysSet beams)  
                    . Set.fromDistinctAscList  
                    $ elemIndices '^' row  
              )  
              rows  
              beamsByRow  
          beamsByRow =  
            scanl  
              ( \beams splits ->  
                  let unsplit = beams `Map.withoutKeys` splits  
                      split = beams `Map.restrictKeys` splits  
                      splitLeft = Map.mapKeysMonotonic pred split  
                      splitRight = Map.mapKeysMonotonic succ split  
                   in Map.unionsWith (+) [unsplit, splitLeft, splitRight]  
              )  
              (Map.singleton (fromJust $ elemIndex 'S' start) 1)  
              splitsByRow  
      print . sum $ map Set.size splitsByRow  
      print . sum $ last beamsByRow  
    
  • source
  • parent
  • [–] 3 points 10 months ago* (last edited 10 months ago) (1 child)

    Nim

    Another simple one.

    Part 1: count each time a beam crosses a splitter.
    Part 2: keep count of how many particles are in each column in all universes
    (e.g. with a simple 1d array), then sum.

    Runtime: 116 ΞΌs 95 Β΅s 86 Β΅s

    old version

    type
      AOCSolution[T,U] = tuple[part1: T, part2: U]
    
    proc solve(input: string): AOCSolution[int, int] =
      var beams = newSeq[int](input.find '\n')
      beams[input.find 'S'] = 1
    
      for line in input.splitLines():
        var newBeams = newSeq[int](beams.len)
        for pos, cnt in beams:
          if cnt == 0: continue
          if line[pos] == '^':
            newBeams[pos-1] += cnt
            newBeams[pos+1] += cnt
            inc result.part1
          else:
            newbeams[pos] += cnt
        beams = newBeams
      result.part2 = beams.sum()
    

    Update: found even smaller and faster version that only needs a single array.
    Update #2: small optimization

    type
      AOCSolution[T,U] = tuple[part1: T, part2: U]
    
    proc solve(input: string): AOCSolution[int, int] =
      var beams = newSeq[int](input.find '\n')
      beams[input.find 'S'] = 1
    
      for line in input.splitLines():
        for pos, c in line:
          if c == '^' and beams[pos] > 0:
            inc result.part1
            beams[pos-1] += beams[pos]
            beams[pos+1] += beams[pos]
            beams[pos] = 0
      result.part2 = beams.sum()
    

    Full solution at Codeberg: solution.nim

  • source
  • hideshow 1 child comment
  • [–] 3 points 10 months ago

    Uiua

    Heavily copied ahem, inspired by @mykl@lemmy.world's solution :)

    Parse     ← Β°βŠ‚ β–½βŠΈβ‰‘/β†₯β‰ @.βŠœβˆ˜βŠΈβ‰ @\n
    Flow      ← +βŠƒ(/+≑↻1_Β―1€×|Γ—βŠ™Β¬)
    Propagate ← ˜∧(⊸˜Flow)
    Part₁     ← /+♭↧ ⊸Propagate
    Partβ‚‚     ← /+⊣Propagate βŠ™(ΛœβŠ‚0)
    
    $ .......S.......
    $ ...............
    $ .......^.......
    $ ...............
    $ ......^.^......
    $ ...............
    $ .....^.^.^.....
    $ ...............
    $ ....^.^...^....
    $ ...............
    $ ...^.^...^.^...
    $ ...............
    $ ..^...^.....^..
    $ ...............
    $ .^.^.^.^.^...^.
    $ ...............
    &fras "7.txt"
    
    βŠƒ(Part₁|Partβ‚‚) Parse
    
  • source
  • [–] 3 points 10 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
  • [–] 3 points 10 months ago*

    Python

    Not too difficult, especially since there are no edge cases (particles leaving the grid, adjacent splitters).

    My initial solution mutated the input for the simulation which I cleaned up after by creating an array that would record the number of particle paths at every column location + some other optimizations. I chose to implement both parts in the same function because they share the majority of the logic.

    click to view code

    def solve(data: str):
        grid = data.splitlines()
        m, n = len(grid), len(grid[0])
    
        # find the first particle
        particle_paths = [0] * n  # count of particle paths that will reach this column
        for j in range(n):
            if grid[0][j] == 'S':
                particle_paths[j] = 1
                break
        
        # count the number of splits for part 1
        splits = 0
    
        # simulate the particle moving down the grid
        #   optimization 1: we can start from the 3rd row (index 2) because that's where the first splitter is
        #   optimization 2: we can skip alternating rows because every other row is empty
        for i in range(2, m, 2):
            # particle paths per column after this row is processed
            next_particle_paths = [0] * n
    
            for j in range(n):
                if particle_paths[j] == 0:
                    # skip if there are no particle paths coming from above in this column
                    continue
                
                if grid[i][j] == '.':
                    # no splitter here, the number of paths in this column remains the same
                    # make sure to use += to account for neighboring splitters dumping additional paths into this column
                    next_particle_paths[j] += particle_paths[j]
                else:
                    # splitter activated here, any particle arriving here can end up in the left or right column
                    # this can be simulated by adding the number of paths to the columns on either side
                    splits += 1
                    next_particle_paths[j-1] += particle_paths[j]
                    next_particle_paths[j+1] += particle_paths[j]
            
            # update vars for next iteration
            particle_paths = next_particle_paths
    
        # return both 
        #   the number of splits AND 
        #   the count of timelines a particle would create
        return splits, sum(particle_paths)
    
    sample = """.......S.......
    ...............
    .......^.......
    ...............
    ......^.^......
    ...............
    .....^.^.^.....
    ...............
    ....^.^...^....
    ...............
    ...^.^...^.^...
    ...............
    ..^...^.....^..
    ...............
    .^.^.^.^.^...^.
    ..............."""
    assert solve(sample) == (21, 40)
    

  • source
  • [–] 3 points 10 months ago* (last edited 10 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
  • hideshow 2 child comments
  • [–] 2 points 10 months ago (1 child)

    Man, your solution is so much cleaner than the hell I was trying (and failing) to coerce into doing what I wanted. Curse my inability to think bottom-up and array-oriented at the same time!

  • source
  • parent
  • hideshow 1 child comment
  • [–] 2 points 10 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 10 months ago (2 children)

    Go

    Oh my scheduling God! Part 1 was easy. Then for part 2 I started tracing each particle one by one using goroutines, but spawning billions of goroutines seemed to make my poor laptop sad. So I implemented a whole thread pool with process management and stuff, but nothing worked. So at the end I started doing the unthinkable: using my brain.

    It seems we can just reuse the same idea as part 1 one but with a clever counting scheme. Thing works and is as fast as part 1. I'm both happy and deeply sad not to have been able to leverage Go's supposed killer features - which are actually rarely useful when programming things other than servers tbh.

    Here we goooooo:

    day07.go

    package main
    
    import (
    	"aoc/utils"
    )
    
    func parseStartLine(line string) []bool {
    	runes := []rune(line)
    	beams := make([]bool, len(runes))
    
    	for idx, char := range runes {
    		if char == 'S' {
    			beams[idx] = true
    		}
    	}
    
    	return beams
    }
    
    func stepOne(input chan string) (int, error) {
    	beams := parseStartLine(<-input)
    	splitCount := 0
    
    	for line := range input {
    		runes := []rune(line)
    		for idx, char := range runes {
    			if char == '^' && beams[idx] {
    				splitCount++
    				if idx > 0 {
    					beams[idx-1] = true
    				}
    				if idx < len(runes)-1 {
    					beams[idx+1] = true
    				}
    				beams[idx] = false
    			}
    		}
    	}
    
    	return splitCount, nil
    }
    
    func valueBeams(beams []bool) []int {
    	valBeams := make([]int, len(beams))
    
    	for idx, b := range beams {
    		val := 0
    		if b {
    			val = 1
    		}
    		valBeams[idx] = val
    	}
    
    	return valBeams
    }
    
    func stepTwo(input chan string) (int, error) {
    	beams := valueBeams(parseStartLine(<-input))
    
    	for line := range input {
    		runes := []rune(line)
    		for idx, char := range runes {
    			bc := beams[idx]
    			if char == '^' && bc > 0 {
    				beams[idx] = 0
    				if idx > 0 {
    					beams[idx-1] += bc
    				}
    				if idx < len(runes)-1 {
    					beams[idx+1] += bc
    				}
    			}
    		}
    	}
    
    	sum := 0
    	for _, bc := range beams {
    		sum += bc
    	}
    
    	return sum, nil
    }
    
    func main() {
    	inputFile := utils.FilePath("day07.txt")
    	utils.RunStep(utils.ONE, inputFile, stepOne)
    	utils.RunStep(utils.TWO, inputFile, stepTwo)
    }
    
    

  • source
  • hideshow 2 child comments
  • [–] 2 points 10 months ago (1 child)

    Kotlin

    Tried recursive for part1, didn't work. LUCKILY for once I was smart and committed anyway, as it was the right solution for part2! I was losing my mind for a bit though as I had originally written my method with Integers...

    Solution

    class Day07 : Puzzle {
    
        val grid = mutableListOf<MutableList<Char>>()
        val partTwoCache = mutableMapOf<Pair<Int, Int>, Long>()
    
        override fun readFile() {
            val input = readInputFromFile(2025, 7, false)
            for (line in input.lines().filter { it.isNotBlank() }) {
                grid.add(line.toCharArray().toMutableList())
            }
        }
    
        override fun solvePartOne(): String {
            grid[1][grid[0].indexOf('S')] = '|'
    
            var splits = 0
            for (r in 1..<grid.size - 1) {
                for (c in 0..<grid[r].size) {
                    if (grid[r][c] == '|') {
                        if (grid[r+1][c] == '.') {
                            grid[r+1][c] = '|'
                        } else if (grid[r+1][c] == '^') {
                            grid[r+1][c-1] = '|'
                            grid[r+1][c+1] = '|'
                            splits++
                        }
                    }
                }
            }
            return splits.toString()
        }
    
        override fun solvePartTwo(): String {
            val start = grid[0].indexOf('S')
            return (1 + processBeamPartTwo(1, start)).toString() // don't forget to count the original timeline
        }
    
        private fun processBeamPartTwo(row: Int, column: Int): Long {
            if (partTwoCache.contains(Pair(row, column))) {
                return partTwoCache[Pair(row, column)]!!
            }
    
            if (row == grid.size) return 0L
            if (column == grid[row].size || column < 0) return 0L
    
            val out = if (grid[row][column] == '^') { // splitter
                1L + processBeamPartTwo(row, column - 1) + processBeamPartTwo(row, column + 1)
            } else {
                processBeamPartTwo(row + 1, column)
            }
            partTwoCache[Pair(row, column)] = out
            return out
        }
    }
    

    full code on Codeberg

  • source
  • hideshow 1 child comment
  • [–] 2 points 10 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
  • [–] 2 points 10 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 10 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 10 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
  • [–] 2 points 10 months ago

    Go

    Now I'm behind by 1 day, will try to catch up.

    For part 2 I spent a good while thinking about it, then when I convinced myself my plan could work, struggled a bit with the implementation. But it worked in the end. Basically grid[i][j] is how many different ways you can reach a cell. Start at 1 on the S cell, then propagate the values down and keep adding up the nums when you reach cells through different paths. The answer is the sum of the nums in the last row.

    spoiler

    func part2() {
    	// file, _ := os.Open("sample.txt")
    	file, _ := os.Open("input.txt")
    	defer file.Close()
    	scanner := bufio.NewScanner(file)
    
    	input := [][]rune{}
    
    	for scanner.Scan() {
    		line := []rune(scanner.Text())
    		input = append(input, line)
    	}
    
    	m := len(input)
    	n := len(input[0])
    	grid := make([][]int, m)
    	for i := range m {
    		grid[i] = make([]int, n)
    	}
    
    	for i := range m {
    		for j := range n {
    			c := input[i][j]
    			if i == 0 {
    				if c == 'S' {
    					grid[i][j] = 1
    				}
    				continue
    			}
    			if c == '^' {
    				grid[i][j-1] += grid[i-1][j]
    				grid[i][j+1] += grid[i-1][j]
    			} else {
    				grid[i][j] = grid[i][j] + grid[i-1][j]
    			}
    		}
    	}
    
    	paths := 0
    	for j := range n {
    		paths += grid[m-1][j]
    	}
    
    	fmt.Println(paths)
    }
    

  • source
  • [–] 2 points 9 months ago (4 children)

    (Browser-based) Javascript

    I got lazy for part 1 and stuffed the beam head's coordinates into a string instead of reusing my implementation that puts them into an int, gambling on part 1 not needing that extra bit of speed.

    I did a similar mistake for part 2 as I did on day 6; I wrote a(n overkill) solution for keeping track of the entire path each beam/split follows - which involved cloning each array-storing-a-beam's-path at each split. Whereas on day 6 I got an "out of memory" error, this time I locked up my computer πŸ’ͺ πŸ”₯ (Control+C doesn't exist in the browser console, and Control+W to close the tab wasn't getting through). Was very disappointed to realize we only need to keep track of the number of beams present at each position/index/abscissa without caring about which path they took to reach said index.

    Code

    function part1(inputText) {
      let beams = new Set([inputText.indexOf('S')]);
      const beamSplits = new Set();
      const lines = inputText.trimEnd().split('\n');
      for (let y = 1; y < lines.length; y++) {
        const nextBeams = new Set();
        for (const beamX of beams) {
          if (lines[y][beamX] === '.') {
        nextBeams.add(beamX);
          } else if (lines[y][beamX]) {
        beamSplits.add([beamX, y].toString());
        nextBeams.add(beamX + 1);
        nextBeams.add(beamX - 1);
          }
        }
        beams = nextBeams;
      }
      return beamSplits.size;
    }
    
    {
      const start = performance.now();
      const result = part1(document.body.textContent);
      const end = performance.now();
      console.info({ day: 7, part: 1, time: end - start, result });
    }
    
    
    function part2(inputText) {
      let beams = new Map([
        [inputText.indexOf('S'), 1]
      ]);
      const lines = inputText.trimEnd().split('\n');
      for (let y = 1; y < lines.length; y++) {
    //     console.debug({ y, beams })
        const nextBeams = new Map();
        for (const [beamHeadX, beamCount] of beams) {
          if (lines[y][beamHeadX] === '.') {
            nextBeams.set(beamHeadX, (nextBeams.get(beamHeadX) ?? 0) + beamCount);
          } else if (lines[y][beamHeadX] === '^') {
            nextBeams.set(beamHeadX + 1, (nextBeams.get(beamHeadX + 1) ?? 0) + beamCount);
            nextBeams.set(beamHeadX - 1, (nextBeams.get(beamHeadX - 1) ?? 0) + beamCount);
          }
        }
        beams = nextBeams;
      }
      return beams.entries().reduce((accu, [_, count]) => accu + count, 0);
    }
    
    {
      const start = performance.now();
      const result = part2(document.body.textContent);
      const end = performance.now();
      console.info({
        day: 7,
        part: 2,
        time: end - start,
        result
      })
    }
    

  • source
  • hideshow 4 child comments
  • [–] [S] 2 points 9 months ago (3 children)

    Surprised you didn't trigger the OOM killer. Windows?

  • source
  • parent
  • hideshow 3 child comments
  • [–] 2 points 9 months ago (2 children)

    Naw, linux. I had a Youtube video running in the background that might have made the problem worse. I could switch desktops and move the mouse (albeit with a lot of lag and jitter) but not close the tab or even hard-close the browser.

    Running the same code on my macbook triggered the "a script on this page is slowing down firefox, do you want to wait some more or pause and debug it?" prompt.

    Either the OOM killer is not properly set up on my desktop, or it was actually a hot CPU loop that caused the lock-up and not memory pressure. Or some sort of hybrid cpu+mem problem? The code was cloning ever-longer arrays in a for-loop, and from my understanding the event loop for a browser page/tab is not only single-threaded but contains both UI events and script execution (unless you explicitly/specifically shell out to a web worker or the like).

  • source
  • parent
  • hideshow 2 child comments
  • [–] [S] 2 points 9 months ago (1 child)

    Possibly using up swap space first?

  • source
  • parent
  • hideshow 1 child comment
  • [–] 1 point 9 months ago

    This might be the actual culprit; I don't think I have any swap space allocated (htop always displays 0/0 for it). I have 48 gigs of RAM, and the "out of memory" error I got on a previous day's problem didn't provoke a single slowdown, freeze, or program crash...

  • source
  • parent
  • [–] 1 point 10 months ago* (last edited 10 months ago)

    Uiua

    Part 1 was fun, though I had quite some frustration with getting the loop work the way I wanted it to.
    For part 2 I didn't actually need to modify that much, just pass another function to deal with the list of tachyons: part 1, remove duplicates, part 2, don't remove and count at the end. Easy. Or so I thought.
    I realized something was wrong when it just wouldn't stop running and the fan got louder.

    Today I started over from scratch and decided to solve both parts at once since I basically got the counts for each row anyways, so I just had to change the way the splitting was handled.
    There was one occasion where I got an error that the resulting array would be too big (~4GB) but at least I got a warning instead of seeing RAM usage spike suddenly :P

    Run with example input

    Code

    $ .......S.......
    $ ...............
    $ .......^.......
    $ ...............
    $ ......^.^......
    $ ...............
    $ .....^.^.^.....
    $ ...............
    $ ....^.^...^....
    $ ...............
    $ ...^.^...^.^...
    $ ...............
    $ ..^...^.....^..
    $ ...............
    $ .^.^.^.^.^...^.
    $ ...............
    # &fras "input-7.txt" β—Œ
    βŠœβˆ˜βŠΈβ‰ @\n
    βŠƒ(=@SβŠ‘β‚€)(⊏⊚>0βŠΈβ‰‘/+=@^β†˜β‚‚)
    ShootSplit ← (
      ‚(=2+>0)
      ⧻⊸⊚
      βŠ™(βŸœβ‚‚(
          βŸœβŠβŠšβŠ™βŸœ[β§»]
          ⍚(βŠ‚βŠƒ-₁+₁)
          /+β‰‘βŒŸ(⬚0Λœβ†―Γ—β—‡Β°βŠš)
        )
        +⍜⊏(Λœβ†―0β§»)⊚
      )
    )
    
    Solve ← (
      0
      β₯(βŠ™(+|ShootSplit|βŠƒβŠ‘β‚€β†˜β‚))β—‘β‹…β‹…β§»
      βŠŸβŠ™βŠ™β—ŒβŠ™/+
    )
    
    Solve
    ≑(&p $"Part _: _") 1_2
    

    And for completeness sake:

    Previous attemptEdit: Tried to scrape the old code together but seems like it doesn't actually work like this. Probably just some small thing I missed but I don't have the mental energy to take a look at this mess again :P

    Prep ← βŠƒ(⊚=@S)βŠœβˆ˜βŠΈβ‰ @\n
    
    Splits! ← (
      ˙⍀>0β—‘β‹…β§»
      ‚(=β§»β€™Λœβ¨‚)
      βˆ©βŒŸβ–½βŠΈΒ¬
      ⊸⧻
      βŠ™(♭≑[βŠƒ-₁+₁]
        ^βŠ‚)
    )
    
    Shoot! ← (
      0
      ⍒(βŠ™(+
        | ⍣Splits!^βŠΈβ‹…0
        | ⊚=@^Β°βŠ‚
        )
      | >0β‹…β‹…β§»)
    )
    
    PartOne ← (
      &fras "input-7.txt"
      Prep
      βŠ™β‹…β—ŒShoot!β—΄
    )
    
    PartTwo ← (
      &fras "input-7.txt"
      Prep
      β§»β‹…βŠ™β—ŒShoot!∘
    )
    

  • source
  • [–] 1 point 9 months ago

    Kotlin

    Part 1 is easily solved by simulating the beams and modifying the grid in-place.

    This does not work for part 2, however. Here I opted for a BFS algorithm, moving down row-by-row.
    Similar to other solutions, I store the amount of incoming paths to a splitter, so that I can reference it later. Practically, the two beams from each splitter are representatives of all incoming beams. This reduces the search complexity by a lot!
    The final count of timelines is the sum of the array that collects the incoming beam counts for each bottom-row of the diagram.

    Code on GitHub

    Code

    class Day07 : AOCSolution {
        override val year = 2025
        override val day = 7
    
        override fun part1(inputFile: String): String {
            val diagram = readResourceLines(inputFile)
                .mapArray { line -> line.mapArray { char -> Cell.byChar(char) } }
                .toGrid()
    
            var count = 0
            for (y in 1 until diagram.height) {
                for (x in 0 until diagram.width) {
                    // Search for beam sources in the preceding row
                    if (diagram[x, y - 1] in Cell.beamSourceTypes) {
                        // Simulate the beam moving down
                        when (diagram[x, y]) {
                            Cell.Empty -> diagram[x, y] = Cell.Beam
                            Cell.Splitter -> {
                                // Split the beam and count this splitter
                                diagram[x - 1, y] = Cell.Beam
                                diagram[x + 1, y] = Cell.Beam
                                count++
                            }
    
                            else -> continue
                        }
                    }
                }
            }
    
            return count.toString()
        }
    
        override fun part2(inputFile: String): String {
            val diagram = readResourceLines(inputFile)
                .mapArray { line -> line.mapArray { char -> Cell.byChar(char) } }
                .toGrid()
            val height = diagram.height.toLong()
    
            val startPosition = diagram.positionOfFirst { it == Cell.Start }
    
            // Working stack of beam origin and split origin positions
            val stack = ArrayDeque<Pair<Position, Position>>()
            stack.add(startPosition to startPosition)
    
            // Splitter positions mapped to the count of timelines to them
            // Start with the start position and 1 timeline.
            val splitters = mutableMapOf<Position, Long>(startPosition to 1)
    
            // Keep track of all splitters for which new beams have been spawned already
            // Could be used to solve part 1, as well
            val spawnedSplitters = mutableSetOf<Position>()
    
            // Count the timelines per diagram exit, which is the bottom-most row
            val diagramExits = LongArray(diagram.width)
    
            while (stack.isNotEmpty()) {
                // Breadth first search for memorizing the amount of paths to a splitter
                val (beamOrigin, splitOrigin) = stack.poll()
                val originPathCount = splitters.getValue(splitOrigin)
    
                val nextPosition = beamOrigin + Direction.DOWN
    
                if (nextPosition.y < height) {
                    if (diagram[nextPosition] == Cell.Splitter) {
                        if (nextPosition !in spawnedSplitters) {
                            // Only spawn new beams, if they weren't spawned already
                            stack.add((nextPosition + Direction.LEFT) to nextPosition)
                            stack.add((nextPosition + Direction.RIGHT) to nextPosition)
                            spawnedSplitters.add(nextPosition)
                            // Initialize the count
                            splitters[nextPosition] = originPathCount
                        } else {
                            splitters.computeIfPresent(nextPosition) { _, v -> v + originPathCount }
                        }
                    } else {
                        // Just move down
                        stack.add(nextPosition to splitOrigin)
                    }
                } else {
                    diagramExits[nextPosition.x.toInt()] += originPathCount
                }
            }
    
            // Sum the count of timelines leading to the bottom row, i.e. leaving the diagram for each position
            return diagramExits.sum().toString()
        }
    
        private companion object {
            enum class Cell(val char: Char) {
                Start('S'),
                Empty('.'),
                Splitter('^'),
                Beam('|');
    
                override fun toString(): String {
                    return char.toString()
                }
    
                companion object {
                    fun byChar(char: Char) = entries.first { it.char == char }
    
                    val beamSourceTypes = arrayOf(Start, Beam)
                }
            }
        }
    }
    

  • source
  • [–] 1 point 10 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