8 votes

Day 7: Laboratories

Today's problem description: https://adventofcode.com/2025/day/7

Please post your solutions in your own top-level comment. Here's a template you can copy-paste into your comment to format it nicely, with the code collapsed by default inside an expandable section with syntax highlighting (you can replace python with any of the "short names" listed in this page of supported languages):

<details>
<summary>Part 1</summary>

```python
Your code here.
```

</details>

13 comments

  1. mawelborn
    Link
    Wow I burned about 2 hours way overengineering a solution that made Part 02 impossible to solve. Only to realize while writing a debug visualization that both are actually incredibly simple. And...

    Wow I burned about 2 hours way overengineering a solution that made Part 02 impossible to solve. Only to realize while writing a debug visualization that both are actually incredibly simple. And then did both parts in 5 minutes with a dozen lines each.

    Don't assume a problem's hard folks! You might waste a lot of time writing a hard solution for something that's actually very simple. (Still have to relearn that lesson the hard way sometimes, even after a decade+ experience.)

    Part 1
    def solve(input: str) -> int:
        start, *rows = input.split()
        beams = [(cell == "S") for cell in start]
        splits = 0
    
        for row in rows:
            for index, cell in enumerate(row):
                if beams[index] and cell == "^":
                    beams[index - 1] = True
                    beams[index + 1] = True
                    beams[index] = False
                    splits += 1
    
        return splits
    
    Part 1
    def solve(input: str) -> int:
        start, *rows = input.split()
        timelines = [int(cell == "S") for cell in start]
    
        for row in rows:
            for index, cell in enumerate(row):
                if cell == "^":
                    timelines[index - 1] += timelines[index]
                    timelines[index + 1] += timelines[index]
                    timelines[index] = 0
    
        return sum(timelines)
    
    5 votes
  2. [3]
    lily
    Link
    Surprisingly easy problem - I'd thought we would be getting to the more involved ones by now. I liked the part 2 twist, though. The trick here is one that's been used in prior puzzles, so I...

    Surprisingly easy problem - I'd thought we would be getting to the more involved ones by now. I liked the part 2 twist, though. The trick here is one that's been used in prior puzzles, so I figured it out pretty quickly, but I did have to scrap all my original part 1 code.

    Solution (Lily)
    var lines = File.read_to_string("inputs/day_07.txt").split("\n")
    
    var beam_counts = List.repeat(lines[0].size(), 0)
    beam_counts[lines[0].find("S").unwrap()] = 1
    
    var splits = 0
    
    for line in lines: {
        for i, char in line: {
            if char != '^': {
                continue
            }
    
            var count = beam_counts[i]
            if count > 0: {
                beam_counts[i - 1] += count
                beam_counts[i + 1] += count
                beam_counts[i] = 0
    
                splits += 1
            }
        }
    }
    
    print("Part 1: {}\nPart 2: {}".format(
        splits, beam_counts.fold(0, (|sum, count| sum + count))
    ))
    
    2 votes
    1. [2]
      whispersilk
      Link Parent
      Looking at this actually made me curious — why was an unconditional beam_counts[i (+/-) 1] working? Did your input just not have a splitter on the far edges? And then I looked at my input and saw...

      Looking at this actually made me curious — why was an unconditional beam_counts[i (+/-) 1] working? Did your input just not have a splitter on the far edges? And then I looked at my input and saw mine doesn't, either! It seems likely that's something that was forbidden in the input, so every input always leaves a blank space on either side at the bottom, but it isn't something I'd considered when I was writing my solution.

      1 vote
      1. lily
        Link Parent
        I didn't think about that case, actually! A splitter on the left edge wouldn't crash my program, since Lily supports Python-style negative indexing on lists, but the answer would be wrong. Another...

        I didn't think about that case, actually! A splitter on the left edge wouldn't crash my program, since Lily supports Python-style negative indexing on lists, but the answer would be wrong.

        Another convenience of the input I noticed is that the starting location is always in the exact center of the first row. So, I could simplify that line by just dividing the size by 2 instead of manually searching for the first S character.

  3. Berdes
    Link
    When I saw the problem, I could smell what part 2 would involve from a mile ahead. I feel like the problem could have been made a bit more interesting if they also had beams going at an angle,...

    When I saw the problem, I could smell what part 2 would involve from a mile ahead. I feel like the problem could have been made a bit more interesting if they also had beams going at an angle, like with a splitter that splits incoming vertical rays into two rays going at 45° and straighten incoming 45° beams.

    I experimented with a bunch of small optimizations like swapping my two vectors instead of discarding one every time or keeping track of the range of values that might contain a ray for the inner loop, but neither lead to significant enough improvement to really justify the increased complexity.

    Solution (Rust)
    use aoc_2025::timed_run;
    use std::io;
    use std::vec::Vec;
    
    struct Input {
      width: usize,
      lines: Vec<String>,
    }
    
    impl Input {
      fn read_from_stdin() -> Input {
        let lines: Vec<String> = io::stdin().lines().map(|l| l.unwrap()).collect();
        assert!(lines.len() > 0);
        let width = lines[0].len();
        for l in &lines {
          assert_eq!(width, l.len());
        }
        Input { width, lines }
      }
    }
    
    fn part1(input: &Input) -> u64 {
      let mut ret = 0;
      let mut beams = vec![false; input.width];
      for l in &input.lines {
        let mut new_beams = vec![false; input.width];
        for i in 0..input.width {
          match l.as_bytes()[i] {
            b'.' => new_beams[i] = new_beams[i] || beams[i],
            b'S' => new_beams[i] = true,
            b'^' => {
              if beams[i] {
                new_beams[i - 1] = true;
                new_beams[i + 1] = true;
                ret += 1;
              }
            }
            _ => panic!(""),
          }
        }
        beams = new_beams;
      }
      ret
    }
    
    fn part2(input: &Input) -> u64 {
      let mut beams = vec![0; input.width];
      for l in &input.lines {
        let mut new_beams = vec![0; input.width];
        for i in 0..input.width {
          match l.as_bytes()[i] {
            b'.' => new_beams[i] += beams[i],
            b'S' => new_beams[i] = 1,
            b'^' => {
              new_beams[i - 1] += beams[i];
              new_beams[i + 1] += beams[i];
            }
            _ => panic!(""),
          }
        }
        beams = new_beams;
      }
      let mut ret = 0;
      for n in beams {
        ret += n;
      }
      ret
    }
    
    fn main() {
      let input = Input::read_from_stdin();
      timed_run("Part 1", || part1(&input));
      timed_run("Part 2", || part2(&input));
    }
    
    2 votes
  4. scarecrw
    (edited )
    Link
    Pretty easy day for the problem itself, so I used the opportunity to try a couple new things: DCGs and "Association Lists" (why does every language have to have their own name for this?). I'm not...

    Pretty easy day for the problem itself, so I used the opportunity to try a couple new things: DCGs and "Association Lists" (why does every language have to have their own name for this?).

    I'm not convinced using a DCG for this style of parsing made it any easier (it certainly didn't make it more succinct), but I can certainly see how it could be useful for more advanced parsing.

    I have to say, while Prolog is growing on me, I'm still feeling silly writing ~9 lines to update a couple values in a data structure. It's not that it's that hard, I'm just hoping there's a more idiomatic approach I don't know about.

    Prolog Solution
    :- initialization(main).
    
    main:-
        phrase_from_file(input(StartingColumn, Rows), 'inputs/day07.txt'),
        list_to_assoc([StartingColumn-1], Beams),
        foldl(process_line, Rows, (Beams, 0), (TotalBeams, Result1)),
        assoc_to_values(TotalBeams, BeamCounts),
        sum_list(BeamCounts, Result2),
        format('Part 1: ~w~n', [Result1]),
        format('Part 2: ~w~n', [Result2]),
        halt.
    
    process_line([], State, State).
    process_line([Splitter|Rest], (Beams, Splits), (ResBeams, Splits1)):-
        get_assoc(Splitter, Beams, _),
        process_line(Rest, (Beams, Splits), (RecBeams, RecSplits)),
        split_beams(Splitter, RecBeams, ResBeams),
        succ(RecSplits, Splits1).
    process_line([_|Rest], State, ResultingState):-
        process_line(Rest, State, ResultingState).
    
    split_beams(Splitter, Beams, ResBeams):-
        get_assoc(Splitter, Beams, Count),
        succ(L, Splitter),
        succ(Splitter, R),
        (get_assoc(L, Beams, LCount) ; LCount = 0),
        (get_assoc(R, Beams, RCount) ; RCount = 0),
        del_assoc(Splitter, Beams, _, Beams1),
        LCountNew is LCount + Count,
        RCountNew is RCount + Count,
        put_assoc(L, Beams1, LCountNew, Beams2),
        put_assoc(R, Beams2, RCountNew, ResBeams).
    
    % Parsing
    input(StartingColumn, Rows) -->
        starting_column(StartingColumn, 0),
        "\n",
        rows(Rows).
    
    starting_column(StartingColumn, Idx) -->
        ".",
        { succ(Idx, Idx1) },
        starting_column(StartingColumn, Idx1).
    starting_column(StartingColumn, StartingColumn) -->
        "S",
        rest_of_line.
    
    rest_of_line --> ".", rest_of_line ; [].
    
    rows([Row]) --> row(Row, 0).
    rows([Row|Rest]) -->
        row(Row, 0),
        "\n",
        rows(Rest).
    
    row(Row, Idx) -->
        ".",
        { succ(Idx, Idx1) },
        row(Row, Idx1).
    row(Row, Idx) -->
        "^",
        { succ(Idx, Idx1) },
        row(RowRest, Idx1),
        {Row = [Idx|RowRest]}.
    row([], _) --> [].
    
    1 vote
  5. [2]
    whispersilk
    Link
    I agree with the rest of the commenters that this was a surprisingly easy day. I enjoy when it's feasible to do both parts in a single pass over the input! I was honestly a bit confused at first...

    I agree with the rest of the commenters that this was a surprisingly easy day. I enjoy when it's feasible to do both parts in a single pass over the input!

    I was honestly a bit confused at first on what the second part actually meant, because I couldn't make the numbers add up in my head. Why would the number of timelines be less than the number of splits? And then I realized I was being dumb and comparing the number of splits from my input to the number of timelines from the test input and yes, it was in fact exactly what I thought it would be. From there tacking part 2's logic onto my part 1 solution was simple and the whole thing comes in under 30 lines of code.

    Time in release mode is ~20 µs, which is insane. One thing I always enjoy about Advent of Code is that it reminds me just how fast modern computers can be.

    Rust Solution
    fn day_7(input: String) -> String {
        let lines = input.lines().filter(|l| !l.is_empty()).collect::<Vec<_>>();
        let mut beams = vec![0; lines[0].len()];
        let beams = beams.as_mut_slice();
        beams[lines[0].as_bytes().iter().position(|b| *b == b'S').unwrap()] = 1;
        let num_splits = lines.iter().fold(0, |mut count, l| {
            for (idx, _) in l.as_bytes().iter().enumerate().filter(|(_, b)| **b == b'^') {
                if beams[idx] > 0 {
                    let x = beams[idx];
                    beams[idx] = 0;
                    idx.checked_sub(1).map(|idx| beams[idx] += x);
                    Some(idx + 1)
                        .filter(|i| *i < beams.len())
                        .map(|idx| beams[idx] += x);
                    count += 1;
                }
            }
            count
        });
        let num_timelines = beams.iter().sum::<u64>();
        format!("Part 1: {num_splits}\nPart 2: {num_timelines}")
    }
    
    1 vote
    1. mawelborn
      Link Parent
      Same! I really shot myself in the foot by thinking it couldn't be done in a single pass when I started, only to realize while writing a debug visualization, that the visualization was a single...

      I enjoy when it's feasible to do both parts in a single pass over the input!

      Same! I really shot myself in the foot by thinking it couldn't be done in a single pass when I started, only to realize while writing a debug visualization, that the visualization was a single pass solution.

      1 vote
  6. imperialismus
    Link
    I didn't come up with a clever single-pass solution, but I can confirm it's feasible to solve with recursion with memoization. Still runs in about 70ms on my machine, which is good enough for me....

    I didn't come up with a clever single-pass solution, but I can confirm it's feasible to solve with recursion with memoization. Still runs in about 70ms on my machine, which is good enough for me.

    Solution part 2 (Crystal)
    class Solver
        @line_len : Int32
        @num_rows : Int32
        @s : Array(String)
    
        def initialize(s : String)
            @s = s.split("\n")
            @line_len = @s[0].size
            @num_rows = @s.size
            @cache = {} of {Int32, Int32} => Int64
        end
    
        def solve
            start = @s[0].index("S").as(Int32)
            search(1, start)
        end
    
        def search_with_cache(row : Int32, col : Int32) : Int64
            if res = @cache[{row,col}]?
                res
            else
                @cache[{row,col}] = search(row, col)
            end
        end
    
        def search(row : Int32, col : Int32) : Int64
            unless col >= 0 && col < @line_len
                puts "out of bounds"
                return 1i64
            end
            count : Int64 = 0
            while row < @num_rows
                char = @s[row][col]
                if char == '^'
                    count += search_with_cache(row+1, col+1)
                    count += search_with_cache(row+1, col-1)
                    return count
                else
                    row += 1
                end
            end
            count+1
        end
    end
    
    input = File.read("./input.txt")[0..-2]
    solver = Solver.new(input)
    puts solver.solve
    
    1 vote
  7. hpr
    Link
    Elixir I played this one with the big handycap of refusing to treat the obvious grid as a grid. This has made it much more lengthy and complex than it should've been, especially now that I look...

    Elixir

    I played this one with the big handycap of refusing to treat the obvious grid as a grid. This has made it much more lengthy and complex than it should've been, especially now that I look y'all's solutions.
    My solution instead scans the input once, looking at two lines at a time. (which obviously would've been just as feasible with a grid too!)

    Then, the chars above each other are compared to then generate a potential output.
    So (top: |, bottom: ^) would generate (left: |, middle: ^, right: |).
    These chunks are then recombined to create the new output line. (By priority: if any potential chunk has a ^, take that, then check for |)

    For the second part, I do the same but also create all permutations of the new line value.
    To make the program go fast enough, I then cull all timelines where the last line is the same and keep a running total of how many source-timelines exist for that state so the count is still accurate. This was clearly necessary to not end up in exponential-hell, memoization would've been an alternative solution.

    I'm not too happy with the runtime, but it's fine after that optimization.

    I'm getting tons of practice thinking in lists, recursion and such, but I don't really enjoy having to track little numeric counters and such through tuple types instead of mutating a sum somewhere.

    Overengineered Code
      ### PARSING ###
    
      def parse() do
        parse(AocElixir.read_lines(7))
      end
    
      def parse(lines) do
        Enum.map(lines, &String.to_charlist/1)
      end
    
      ### PART ONE ###
    
      def sliding_window_chunks(list, num_window_size)
          when is_list(list) and num_window_size > length(list) do
        []
      end
    
      def sliding_window_chunks(list, num_window_size)
          when is_list(list) and num_window_size <= length(list) do
        {initial, rest} = Enum.split(list, num_window_size)
    
        Enum.chunk_while(
          rest,
          initial,
          fn elem, acc ->
            new_acc = tl(acc) ++ [elem]
            emitted_chunk = acc
            {:cont, emitted_chunk, new_acc}
          end,
          # emit the remaining accumulator
          fn acc -> {:cont, acc, []} end
        )
      end
    
      defp process_chunks(chunks, join_fun) do
        {heads, tails} = chunks |> Enum.map(&Enum.split(&1, 1)) |> Enum.unzip()
        heads = heads |> Enum.reject(&Enum.empty?/1)
        tails = tails |> Enum.reject(&Enum.empty?/1)
    
        head = heads |> Enum.map(&hd/1) |> join_fun.()
    
        {head, tails}
      end
    
      defp join_chunks(remaining_chunks, current_chunks, join_fun, results) do
        case {remaining_chunks, current_chunks} do
          {[], []} ->
            results
    
          {[], current_chunks} ->
            {head, tails} = process_chunks(current_chunks, join_fun)
            join_chunks([], tails, join_fun, [head | results])
    
          {[new_chunk | next_remaining_chunks], current_chunks} ->
            current_chunks = [new_chunk | current_chunks]
            {head, tails} = process_chunks(current_chunks, join_fun)
            join_chunks(next_remaining_chunks, tails, join_fun, [head | results])
        end
      end
    
      def join_chunks(chunks, join_fun), do: join_chunks(chunks, [], join_fun, [])
    
      # vals: {upper_char, lower_char}
      def advance_beam(vals) do
        case vals do
          {?S, ?.} -> [?., ?|, ?.]
          {?|, ?^} -> [?|, ?^, ?|]
          {?|, ?.} -> [?., ?|, ?.]
          {?., ?^} -> [?., ?^, ?.]
          {_, ?.} -> [?., ?., ?.]
        end
      end
    
      def choose_char_from_options(list_of_chars) do
        cond do
          Enum.member?(list_of_chars, ?^) -> ?^
          Enum.member?(list_of_chars, ?|) -> ?|
          Enum.member?(list_of_chars, ?.) -> ?.
        end
      end
    
      def simulate_beam(input_lines) do
        input_lines
        |> Enum.reduce([], fn line, result ->
          if Enum.empty?(result) do
            [line | result]
          else
            prev_line = hd(result)
    
            new_line =
              Enum.zip(prev_line, line)
              |> Enum.map(&advance_beam/1)
              |> join_chunks(&choose_char_from_options/1)
              # because we're generating 3-tuples for each char, we need to remove the first and last generated, which are not collapsed by join_chunks
              |> Enum.drop(1)
              # we're building in reverse because of linked lists, so reverse
              |> Enum.reverse()
    
            [tl(new_line) | result]
          end
        end)
        # we're building in reverse because of linked lists, so reverse
        |> Enum.reverse()
      end
    
      def count_splits(input_lines) do
        input_lines
        # columnwise
        |> Enum.zip()
        |> Enum.map(fn col ->
          sliding_window_chunks(Tuple.to_list(col), 2)
          |> Enum.count(fn [upper, lower] -> upper === ?| and lower === ?^ end)
        end)
        |> Enum.sum()
      end
    
      def part1(input_lines) do
        input_lines
        |> simulate_beam()
        |> count_splits()
      end
    
      ### PART 2 ###
    
      # vals: {upper_char, lower_char}
      def advance_quantum_beam(vals) do
        case vals do
          {?S, ?.} -> [[?., ?|, ?.]]
          {?|, ?^} -> [[?|, ?^, ?.], [?., ?^, ?|]]
          {?|, ?.} -> [[?., ?|, ?.]]
          {?., ?^} -> [[?., ?^, ?.]]
          {_, ?.} -> [[?., ?., ?.]]
        end
      end
    
      def permutations(list_of_lists) do
        Enum.reduce(Enum.reverse(list_of_lists), [[]], fn options, permutations ->
          Enum.flat_map(options, fn option ->
            Enum.map(permutations, fn permutation ->
              [option | permutation]
            end)
          end)
        end)
      end
    
      def simulate_quantum_beam(input_lines) do
        input_lines
        |> Enum.reduce([], fn line, results ->
          if Enum.empty?(results) do
            [{[line], 1}]
          else
            new_results =
              Enum.flat_map(results, fn {result_lines, sources} ->
                prev_line = hd(result_lines)
    
                new_quantum_lines =
                  Enum.zip(prev_line, line)
                  |> Enum.map(&advance_quantum_beam/1)
                  |> permutations()
                  |> Enum.map(fn permutation ->
                    permutation
                    |> join_chunks(&choose_char_from_options/1)
                    # because we're generating 3-tuples for each char, we need to remove the first and last generated, which are not collapsed by join_chunks
                    |> Enum.drop(1)
                    # we're building in reverse because of linked lists, so reverse
                    |> Enum.reverse()
                    |> Enum.drop(1)
                  end)
    
                Enum.map(new_quantum_lines, fn new_line -> {[new_line | result_lines], sources} end)
              end)
    
            new_results
            |> Enum.uniq_by(fn {result_lines, _} -> hd(result_lines) end)
            |> Enum.map(fn {result_lines, _} ->
              {result_lines,
               Enum.filter(new_results, fn res -> hd(elem(res, 0)) === hd(result_lines) end)
               |> Enum.sum_by(&elem(&1, 1))}
            end)
          end
        end)
        # we're building in reverse because of linked lists, so reverse
        |> Enum.map(fn {result_lines, sources} -> {Enum.reverse(result_lines), sources} end)
      end
    
      def part2(input) do
        input
        |> simulate_quantum_beam()
        |> Enum.sum_by(fn {_, sources} -> sources end)
      end
    
    Benchmarks
    Name               ips        average  deviation         median         99th %
    parse         14168.17      0.0706 ms    ±33.69%      0.0844 ms       0.116 ms
    part one        174.39        5.73 ms     ±7.58%        5.58 ms        7.01 ms
    part two          3.09      324.02 ms     ±1.01%      323.01 ms      332.19 ms
    
    1 vote
  8. balooga
    Link
    TypeScript Yesterday was busy for me so I'm playing catch-up now. This was the first puzzle that tripped me up! I breezed through Part 1 with a nice single-loop solution but walked right into the...

    TypeScript

    Yesterday was busy for me so I'm playing catch-up now. This was the first puzzle that tripped me up! I breezed through Part 1 with a nice single-loop solution but walked right into the trap of doing a naive recursive brute-force approach for Part 2 and hit my first OOM crash of the year.

    I guess I've "done" dynamic programming in the past but never called it that. Usually I'm thinking in terms of managing React state and minimizing re-renders by memoizing expensive function outputs, etc. I'm not used to considering this kind of problem in the same way.

    I don't think it's cheating to lean on ChatGPT a bit when you get stuck in AoC, as long as you're earnestly tackling the problem, and working through different approaches — not just generating a full solution in one go. So in full disclosure, that's the route I had to take for this Part 2. Seems like most of you figured out the right approach straightaway, but it was not intuitive to me all! Just as I predicted, lol.

    Parts 1 and 2
    import { RunFunction } from '../../../types';
    
    type InputData = string[][];
    enum Direction {
      L = -1,
      R = 1,
    }
    
    function formatInput(input: string): InputData {
      return input.split('\n').map(row => row.split(''));
    }
    
    export const run: RunFunction<InputData> = () => {
      const validatePosition = (grid: InputData, rowIndex: number, colIndex: number): boolean => {
        return rowIndex >= 0 && rowIndex < grid.length && colIndex >= 0 && colIndex < grid[0].length;
      };
    
      const countBeamSplits = (grid: InputData): number => {
        let total = 0;
        for (let rowIndex = 0; rowIndex < grid.length; rowIndex++) {
          for (let colIndex = 0; colIndex < grid[rowIndex].length; colIndex++) {
            if (grid[rowIndex][colIndex] === '|' || grid[rowIndex][colIndex] === 'S') {
              if (validatePosition(grid, rowIndex + 1, colIndex)) {
                if (grid[rowIndex + 1][colIndex] === '^') {
                  if (validatePosition(grid, rowIndex + 1, colIndex + Direction.L)) {
                    grid[rowIndex + 1][colIndex + Direction.L] = '|';
                  }
                  if (validatePosition(grid, rowIndex + 1, colIndex + Direction.R)) {
                    grid[rowIndex + 1][colIndex + Direction.R] = '|';
                  }
                  total++;
                  continue;
                }
                grid[rowIndex + 1][colIndex] = '|';
              }
            }
          }
        }
        return total;
      };
    
      const countQuantumTimelines = (grid: InputData) => {
        let total = 0n;
        const startCol = grid[0].indexOf('S');
        let curr = Array<bigint>(grid[0].length).fill(0n);
        curr[startCol] = 1n;
    
        for (let rowIndex = 0; rowIndex < grid.length; rowIndex++) {
          const next = Array<bigint>(grid[rowIndex].length).fill(0n);
    
          for (let colIndex = 0; colIndex < grid[rowIndex].length; colIndex++) {
            const count = curr[colIndex];
            if (count === 0n) {
              continue;
            }
    
            const nextRowIndex = rowIndex + 1;
            if (nextRowIndex >= grid.length) {
              total += count;
              continue;
            }
    
            if (grid[nextRowIndex][colIndex] === '^') {
              if (colIndex - 1 >= 0) {
                next[colIndex - 1] += count;
              } else {
                total += count;
              }
    
              if (colIndex + 1 < grid[rowIndex].length) {
                next[colIndex + 1] += count;
              } else {
                total += count;
              }
            } else {
              next[colIndex] += count;
            }
          }
          curr = next;
        }
    
        return total;
      };
    
      return [formatInput, countBeamSplits, countQuantumTimelines];
    };
    

    Benchmarks: The median input formatting time over 50 iterations is 22µs on my machine. Part 1 is 74µs and Part 2 is 160µs.

    1 vote
  9. jzimbel
    Link
    Elixir Pretty straightforward, just count the unique splitters encountered for part 1 and count the total number of unique paths for part 2. This was another good candidate for my new StringGrid...

    Elixir

    Pretty straightforward, just count the unique splitters encountered for part 1 and count the total number of unique paths for part 2. This was another good candidate for my new StringGrid struct.

    Both parts

    I went back and smushed everything into fewer lines for no obvious reason. (Certainly not readability...)

    defmodule AdventOfCode.Solution.Year2025.Day07 do
      alias AdventOfCode.StringGrid, as: SG
    
      use AdventOfCode.Solution.SharedParse
    
      @impl true
      def parse(input), do: {SG.new(input), input |> :binary.match("S") |> elem(0)}
    
      def part1({grid, start_x}),
        do: grid |> reduce_grid({[start_x], 0}, &sum_splits/3) |> then(fn {_xs, sum} -> sum end)
    
      def part2({grid, start_x}),
        do: grid |> reduce_grid(%{start_x => 1}, &count_paths/3) |> Map.values() |> Enum.sum()
    
      defp reduce_grid(grid, init_acc, reducer),
        do: for(y <- 0..(grid.height - 3)//1, reduce: init_acc, do: (acc -> reducer.(y, acc, grid)))
    
      defp sum_splits(y, {xs, sum}, grid) do
        xs
        |> Stream.map(&{&1, grid[{&1, y + 1}]})
        |> Enum.flat_map_reduce(0, fn
          {x, ?.}, n -> {[x], n}
          {x, ?^}, n -> {[x - 1, x + 1], n + 1}
        end)
        |> then(fn {xs, num_splits} -> {Enum.uniq(xs), sum + num_splits} end)
      end
    
      defp count_paths(y, counts_by_x, grid) do
        counts_by_x
        |> Stream.map(fn {x, n} -> {x, n, grid[{x, y + 1}]} end)
        |> Enum.reduce(%{}, fn
          {x, n, ?.}, acc -> Map.update(acc, x, n, &(&1 + n))
          {x, n, ?^}, acc -> acc |> Map.update(x - 1, n, &(&1 + n)) |> Map.update(x + 1, n, &(&1 + n))
        end)
      end
    end
    
    Benchmarks
    Name             ips        average  deviation         median         99th %
    Parse      4866.31 K        0.21 μs  ±9313.72%       0.167 μs        0.25 μs
    Part 1        1.65 K      605.77 μs     ±4.12%      600.58 μs      715.32 μs
    Part 2        1.49 K      673.21 μs     ±5.37%      660.25 μs      797.15 μs
    
    1 vote
  10. DeaconBlue
    Link
    Coming back to the days I hadn't completed because of Real Life Obligations. This one seemed alarmingly easy. All of the edge cases were conveniently removed from the input, so no cases of side by...

    Coming back to the days I hadn't completed because of Real Life Obligations.

    This one seemed alarmingly easy. All of the edge cases were conveniently removed from the input, so no cases of side by side splitters or splitters on the grid edge that would make this single pass a problem. I think it would have been interesting if they did some kind of diamond shape with wrap-around requirements. If there were items on the grid edge, I would have just done the classic trick of grid problems where you just bump the grid size by one and ignore it later.

    Parts 1 and 2
    use std::{
        env,
        fs::File,
        io::{self, BufRead},
        path::Path,
        time::Instant,
    };
    fn main() {
        let args: Vec<String> = env::args().collect();
        if let Ok(lines) = read_lines(&args[1]) {
            let now = Instant::now();
            let mut split_count = 0;
            let mut context: Vec<i64> = Vec::new();
            for line in lines {
                for (i, char) in line.expect("").chars().enumerate() {
                    if let Some(state) = context.get_mut(i) {
                        if *state > 0 && char == '^' {
                            context[i - 1] += context[i];
                            context[i + 1] += context[i];
                            context[i] = 0;
                            split_count += 1;
                        }
                    } else {
                        if char == 'S' {
                            context.push(1);
                        } else {
                            context.push(0);
                        }
                    }
                }
            }
            let elapsed = now.elapsed();
            println!("Part 1: {split_count}");
            println!("Part 2: {:?}", context.iter().sum::<i64>());
            println!("Elapsed: {:.2?}", elapsed);
        }
    }
    
    fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
    where
        P: AsRef<Path>,
    {
        let file = File::open(filename)?;
        Ok(io::BufReader::new(file).lines())
    }