Colorised video: https://youtube.com/shorts/H_Fmf1rupGg
comments
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
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).
There's an alternative approach to merging ranges. Put starting and ending points into a heap then scan it, keeping track of the number of open ranges present. Something like this:
mergeRanges rs =
let
-- We put in this order so we count openings before closings
starts = map (\(a, b) -> (a, -1)) rs
ends = map (\(a, b) -> (b, 1)) rs
heap = H.fromList (starts ++ ends) :: H.MinHeap (Int, Int)
in
mergeNo heap
where
-- not in a range currently
mergeNo :: H.MinHeap (Int, Int) -> [R]
mergeNo h =
case H.view h of
Nothing -> []
Just ((a, -1), h') -> mergeYes a 1 h'
-- in a range
mergeYes :: Int -> Int -> H.MinHeap (Int, Int) -> [R]
mergeYes start height h =
let Just ((v, delta), h') = H.view h
newHeight = height - delta
in if newHeight == 0
then (start, v) : mergeNo h'
else mergeYes start newHeight h'
Day 7 - Colourised, single image.

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(())
}
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
Yeah, the process was terminated by Linux in about 2-3 mins x)
Rust
Not too difficult, but my pt1 approach didnt work for pt2. Small amount of tweaking and it was back in action. This should be a really good one for visualisations, so I am definitely going to try do something. Maybe colorise the nodes based on how many paths go through it? Definitely an animation of the trickle down. Looking forward to seeing everyone elses visualisations as well!
spoiler
#[test]
fn test_y2025_day7_part2() {
let input = include_str!("../../input/2025/day_7.txt");
let mut rows = input
.lines()
.map(|l| {
l.chars()
.map(|c| match c {
'S' | '|' => 1isize,
'^' => -1isize,
_ => 0isize,
})
.collect::<Vec<isize>>()
})
.collect::<Vec<Vec<isize>>>();
let width = rows[0].len();
for i in 1..rows.len() {
for j in 0..width {
if rows[i - 1][j] >= 1 {
if rows[i][j] == -1 {
rows[i][j - 1] += rows[i - 1][j];
rows[i][j + 1] += rows[i - 1][j];
} else {
rows[i][j] += rows[i - 1][j];
}
}
}
}
print_tree(&rows);
let total: isize = rows.pop().unwrap().iter().sum();
println!("Total: {}", total);
}
~32B coroutines does sound like it might make any CPU a bit sad :D
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)
}
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 ΞΌ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
Thanks! I try to write code to be readable by humans above all else.
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

That's a great addition :D
Running my own input I also noticed that your solution is a lot faster than mine (processing each roll individually). I'll keep that 2D-rotation in mind for the future.

Uiua
Quite simple this one. Part 2 still takes a few seconds because I'm essentially checking off each roll individually.
Code
$ ..@@.@@@@.
$ @@@.@.@.@@
$ @@@@@.@.@@
$ @.@@@@..@.
$ @@.@@@@.@@
$ .@@@@@@@.@
$ .@.@.@.@@@
$ @.@@@.@@@@
$ .@@@@@@@@.
$ @.@.@@@.@.
βββΈβ @\n
=@@
Rolls β β£(β§»βΛβ€)ββΈβ‘1_1
Removable β (
β¬0β§Rolls[3_3 1_1 1_1]
ββ€4
)
Remove β ββ‘(Λβ―0β§»)
PartOne β β§»Removable
PartTwo β (
β0
β₯(
β(β+ββ§»)βRemovable
ΛRemove
)β
β
)
&pf "Part One: "
&p βΈPartOne
&pf "Part Two: "
&p PartTwo
Old Part 2
Before seeing mykl's solution this was my solution for part 2
PartTwoOld β (
0β0
β’(βββ
ββΈRemovable
+ββΈβ§»
ββΈRemove
| Β¬ββ)
ββ
β
)
It's basically the same, just that I used a while-do-loop, making the check for the ending condition myself (which took me a bit to get right because I still find loops in Uiua a bit confusing).
Using the repeat-loop as above also gets rid of the dip's (β). I could've removed them here as well but I was already deep in the trouble of getting the loop to work correctly and I liked the little face at the beginning 0β0
Looks good. I like how you managed to use the same structure for Parts 1 and 2; that's more than I did. And yours is faster than mine too.
Uiua
I'm new to Uiua, so probably not the best way to express the solution!
ParseRows β (
βββΈβ @\n
ββΒ°β # Get the row of operations.
)
ParseOperations β ββ’βΈβ @\s
SumCalculations β (
β‘(
β£(β/ΓΒ°@*
| β/+Β°@+
)
)
/+
)
Partβ β (
ParseRows
β(ParseOperations
| β β‘(βββΈβ @\s) # Parse columns.
)
SumCalculations
)
Partβ β (
ParseRows
β(ParseOperations
| β(β‘β‘(ββ½βΈβ @\s)) β‘/β₯βΈβ @\sβ # Parse numbers.
)
SumCalculations
)
&fras "6.txt"
βPartβ Partβ
Rust
Finally having some fun with iters. I think part 2 came out nicely once I figured I can rotate the whole input to get numbers that look like ours.
Q: Anyone have any tips how to reason about the intermediate types in a long iter chain? I'm currently placing dummy maps, and on occasion, even printing the values from them when I get too stuck.
the code
use std::fs::File;
use std::io::{BufReader, Lines};
#[allow(dead_code)]
pub fn part1(input: Lines<BufReader<File>>) {
let mut input = input
.map_while(Result::ok)
.map(|line| {
line.split_ascii_whitespace()
.map(|s| s.to_string())
.collect::<Vec<String>>()
})
.collect::<Vec<Vec<String>>>();
let ops = input.pop().unwrap();
let values = input
.iter()
.map(|v| {
v.iter()
.map(|v| v.parse::<i64>().unwrap())
.collect::<Vec<i64>>()
})
.collect::<Vec<Vec<i64>>>();
let transposed: Vec<Vec<i64>> = (0..values[0].len())
.map(|i| values.iter().map(|row| row[i]).collect())
.collect();
let mut sum = 0;
for i in 0..ops.len() {
let op: &str = &ops[i];
match op {
"+" => {
sum += transposed[i].iter().sum::<i64>();
}
"*" => {
sum += transposed[i].iter().product::<i64>();
}
_ => panic!("Invalid operation"),
}
}
println!("sum = {}", sum)
}
#[allow(dead_code)]
pub fn part2(input: Lines<BufReader<File>>) {
let mut input = input
.map_while(Result::ok)
.map(|line| line.chars().collect::<Vec<char>>())
.collect::<Vec<Vec<char>>>();
let ops = input
.pop()
.unwrap()
.iter()
.map(|c| c.to_string())
.filter(|s| s != " ")
.collect::<Vec<String>>();
let transposed: Vec<i64> = (0..input[0].len())
.map(|i| input.iter().map(|row| row[i]).collect())
.map(|vec: String| vec.trim().to_string())
.map(|s| {
if s.len() == 0 {
0
} else {
s.parse::<i64>().unwrap()
}
})
.collect();
let groups = transposed
.into_iter()
.fold(Vec::new(), |mut acc: Vec<Vec<i64>>, num| {
if num == 0 {
if let Some(last) = acc.last_mut() {
if !last.is_empty() {
acc.push(Vec::new());
}
} else {
acc.push(Vec::new());
}
} else {
if acc.is_empty() {
acc.push(Vec::new());
}
acc.last_mut().unwrap().push(num);
}
acc
});
let mut sum = 0;
for i in 0..ops.len() {
let op: &str = &ops[i];
match op {
"+" => {
sum += groups[i].iter().sum::<i64>();
}
"*" => {
sum += groups[i].iter().product::<i64>();
}
_ => panic!("Invalid operation"),
}
}
println!("sum = {}", sum)
}
Ruby
I decided to rotate the entire input character-by-character, then parse the numbers (see the full source here)
grid = input.lines.map(&:chomp).map {|l| l.each_char.map.to_a }.to_a
transposed = Array.new(grid[0].length) { Array.new(grid.length) }
grid.each_with_index do |row, y|
row.each_with_index do |col, x|
transposed[x][y] = col
end
end
vals = []
ops = []
temp_vals = []
transposed.each do |row|
l = row.join("").strip
temp_vals << l.scan(/\d+/).map(&:to_i).to_a[0]
/[+*]/.match(l) { |m| ops << m.to_s.to_sym }
if l == ""
vals << temp_vals.compact
temp_vals = []
end
end
vals << temp_vals.compact unless temp_vals.empty?
vals.each_with_index.sum do |v, i|
v.inject(ops[i])
end
Haskell
import Control.Arrow
import Data.Char
import Data.List
import Text.ParserCombinators.ReadP
op "*" = product
op "+" = sum
part1 s = sum $ zipWith ($) (op <$> a) (transpose $ fmap read <$> as)
where
(a : as) = reverse . fmap words . lines $ s
parseGroups = fst . last . readP_to_S (sepBy (endBy int eol) eol) . filter (/= ' ')
where
eol = char '\n'
int = read <$> munch1 isDigit :: ReadP Int
part2 s = sum $ zipWith ($) (op <$> words a) (parseGroups . unlines $ reverse <$> transpose as)
where
(a : as) = reverse $ lines s
main = getContents >>= print . (part1 &&& part2)