this post was submitted on 07 Dec 2025
32 points (97.1% liked)

Advent Of Code

1197 readers
14 users here now

An unofficial home for the advent of code community on programming.dev! Other challenges are also welcome!

Advent of Code is an annual Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like.

Everybody Codes is another collection of programming puzzles with seasonal events.

EC 2025

AoC 2025

Solution Threads

M T W T F S S
1 2 3 4 5 6 7
8 9 10 11 12

Visualisations Megathread

Rules/Guidelines

Relevant Communities

Relevant Links

Credits

Icon base by Lorc under CC BY 3.0 with modifications to add a gradient

console.log('Hello World')

founded 2 years ago
MODERATORS
 

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

top 23 comments
sorted by: hot top controversial new old
[–] Quant@programming.dev 1 points 3 days ago* (last edited 3 days 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!∘
)

[–] skissue@programming.dev 3 points 5 days 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
[–] reboot6675@sopuli.xyz 2 points 5 days 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)
}

[–] mykl@lemmy.world 4 points 6 days ago* (last edited 6 days ago) (1 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
[–] skissue@programming.dev 3 points 6 days ago (1 children)

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!

[–] mykl@lemmy.world 2 points 5 days ago

When I compare it to some other Uiua solutions on the Discord I feel like I'm still just banging rocks together.

[–] tranzystorek_io@beehaw.org 3 points 6 days ago* (last edited 6 days 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~~

[–] Deebster@programming.dev 4 points 6 days ago* (last edited 6 days 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 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])
    }
}
[–] Gobbel2000@programming.dev 4 points 6 days ago (1 children)

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(())
}
[–] strlcpy@lemmy.sdf.org 1 points 6 days ago

Ahh I knew there would be some simple combined representation like this but couldn't be bothered. Nice!

[–] lwhjp@piefed.blahaj.zone 4 points 6 days ago* (last edited 6 days ago) (1 children)

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  
[–] lwhjp@piefed.blahaj.zone 1 points 6 days ago* (last edited 6 days 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  
[–] eco_game@discuss.tchncs.de 3 points 6 days ago (1 children)

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

[–] chunkystyles@sopuli.xyz 2 points 6 days 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
}
[–] Avicenna@programming.dev 2 points 6 days 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
[–] Pyro@programming.dev 3 points 6 days ago* (last edited 6 days 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)

[–] janAkali@lemmy.sdf.org 3 points 6 days ago* (last edited 6 days ago) (1 children)

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

[–] Deebster@programming.dev 2 points 6 days ago

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).

[–] cabhan@discuss.tchncs.de 2 points 6 days 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()));
    }
}
[–] ystael@beehaw.org 2 points 6 days 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)))))
[–] LeixB@lemmy.world 2 points 6 days 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
[–] strlcpy@lemmy.sdf.org 2 points 6 days 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());
}

[–] CameronDev@programming.dev 1 points 6 days ago

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);
    }