this post was submitted on 21 Nov 2025
1 points (66.7% liked)

Advent Of Code

1200 readers
2 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
 

Quest 14: The Game of Light

  • 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

Link to participate: https://everybody.codes/

you are viewing a single comment's thread
view the rest of the comments
[โ€“] Pyro@programming.dev 1 points 4 weeks ago (1 children)

Python

I spent way too long optimizing this solution from ~20s to <1s

# Build grid as list of integers (bitmask for each row)
def build_grid(data: str) -> list[int]:
    grid = []
    for line in data.splitlines():
        row = 0
        for j, cell in enumerate(line):
            if cell == '#':
                row |= (1 << j)
        grid.append(row)
    return grid

# Compute next step of the grid while counting new active cells
# Highly optimized using bitwise operations
def compute_next_step(grid: list[int], m, n):
    next_grid = []
    new_active_cells = 0
    mask = (1 << n) - 1     # mask to keep only n bits

    for i in range(m):
        row_above = grid[i-1] if i > 0 else 0
        row_below = grid[i+1] if i < m - 1 else 0
        
        # Neighbors are diagonal: (i-1, j-1), (i-1, j+1), (i+1, j-1), (i+1, j+1)
        n_above = (row_above << 1) ^ (row_above >> 1)   # above neighbors are odd parity
        n_below = (row_below << 1) ^ (row_below >> 1)   # below neighbors are odd parity
        # total active neighbors are odd parity
        neighbors_xor = n_above ^ n_below
        
        # the rules say a cell becomes active if:
        # - it is currently active, and has an odd number of active neighbors
        # - it is currently inactive, and has an even number of active neighbors
        # this is equivalent to having an even number of (active neighbors + itself)
        # For single bit, equivalent to: ~(self ^ neighbors_xor) & 1
        #   self ^ neighbors_xor = 0 if both are even or both are odd (desired case)
        #   so we negate it to get 1 in desired case
        #   and we apply mask to keep only n bits
        # This is all done in parallel for the entire row using bitwise operations
        next_row = ~(grid[i] ^ neighbors_xor) & mask
        
        next_grid.append(next_row)
        new_active_cells += next_row.bit_count()

    return next_grid, new_active_cells

# Simulate for given rounds and return total active cells
def part1(data: str, rounds = 10):
    grid = build_grid(data)
    m, n = len(grid), data.index('\n')

    total_active = 0
    for _ in range(rounds):
        next_grid, new_active_cells = compute_next_step(grid, m, n)
        total_active += new_active_cells
        grid = next_grid
    return total_active

assert (t := part1(""".#.##.
##..#.
..##.#
.#.##.
.###..
###.##""")) == 200, f"Expected 200 but got {t}"

# part 2 is just part 1 with more rounds
from functools import partial
part2 = partial(part1, rounds=2025)

# Match 8x8 pattern with center of 34x34 grid
def match_pattern(next_grid, pattern_grid):
    for i in range(8):
        # skip first 13 rows and shift right by 13 to align with pattern
        if ((next_grid[i+13] >> 13) & 0xFF) != pattern_grid[i]:
            return False
    return True

def part3(pattern: str):
    pattern_grid = build_grid(pattern)
    grid_size = 34

    rounds = 1000000000
    grid = [0] * grid_size  # we start with an empty 34x34 grid

    seen = set()                    # grids we have seen
    cumu_matches_for_round = [0]    # cumulative matches for each round in the cycle
    cumu_matches = 0                # current cumulative matches

    # Simulate until we find a cycle or reach the rounds limit
    round = 1
    while round <= rounds:
        next_grid, next_actives = compute_next_step(grid, grid_size, grid_size)

        # get current state and check for cycles
        state = tuple(next_grid)
        if state in seen:
            break
        else:
            seen.add(state)
        
        # check for pattern match and update cumulative matches
        if match_pattern(next_grid, pattern_grid):
            cumu_matches += next_actives        
        # store cumulative matches from the start to this round
        cumu_matches_for_round.append(cumu_matches)

        # update variables for next round
        grid = next_grid
        round += 1
    
    # the length of the cycle is round - 1 since we break AFTER detecting a cycle (cycle + 1)
    cycle_len = round - 1
    # the number of full cycles we can fit in rounds
    full_cycles = rounds // cycle_len
    # the total matches from full cycles
    matches = full_cycles * cumu_matches
    # the remaining rounds after full cycles
    matches += cumu_matches_for_round[rounds % cycle_len]

    return matches

assert (t := part3("""#......#
..#..#..
.##..##.
...##...
...##...
.##..##.
..#..#..
#......#""")) == 278388552, f"Expected 278388552 but got {t}"
[โ€“] hades@programming.dev 1 points 4 weeks ago (1 children)

Are you assuming that the cycle will always include the initial state? It does, as it turns out, but that's not really obvious (at least to me) from the problem.

[โ€“] Pyro@programming.dev 2 points 4 weeks ago

Yes, I'm not sure how to prove it but I had a hunch that was the case. So I initially wrote some code to print the grid right after the cycle is detected and lo and behold, it was the initial state.