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
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
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
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) } } } }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 start = Self::start_pos(start_line)?; let mut beams = vec![false; start_line.len()]; beams[start] = 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]) } }Rust
Dynamic programming? Nah, just keep track of the number of overlapping beams and part 2 becomes no different to part 1.
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(()) }Ahh I knew there would be some simple combined representation like this but couldn’t be bothered. Nice!
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 DMan, 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!
When I compare it to some other Uiua solutions on the Discord I feel like I’m still just banging rocks together.
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 inputAnd 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
Uiua
Heavily
copiedahem, inspired by @[email protected]’s solution :)Parse ← °⊂ ▽⊸≡/↥≠@.⊜∘⊸≠@\n Flow ← +⊃(/+≡↻1_¯1¤×|×⊙¬) Propagate ← ˜∧(⊸˜Flow) Part₁ ← /+♭↧ ⊸Propagate Part₂ ← /+⊣Propagate ⊙(˜⊂0) $ .......S....... $ ............... $ .......^....... $ ............... $ ......^.^...... $ ............... $ .....^.^.^..... $ ............... $ ....^.^...^.... $ ............... $ ...^.^...^.^... $ ............... $ ..^...^.....^.. $ ............... $ .^.^.^.^.^...^. $ ............... &fras "7.txt" ⊃(Part₁|Part₂) ParseKotlin
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
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 }
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)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 μs95 µs86 µsold 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 optimizationtype 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
Ah, it took me looking at your updated Codeberg version to understand this - you looked at part two in the opposite way than I did but it comes out the same in the end (and yours is much more efficient).
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())); } }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) }~32B coroutines does sound like it might make any CPU a bit sad :D
Yeah, the process was terminated by Linux in about 2-3 mins x)
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()); }My choice of representation for part 1 ended up being fortunate, as all I needed to change for part 2 was three
setfinpropagatethat becameincf. This was also another good use case for multiple return values, as it’s the beam vector you want toreduceon, 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)))))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








