Day 11 is grid-based challenge, where I’m giving a grid floor, empty seat, and occupied seat, and asked to step through time using rules that define how a seat will be occupied at time t+1 given the state of it and it’s neighbors at time t. My code gets really ugly today, but it solves.

Challenge

The puzzle can be found here. The puzzle input is grid of characters where L represents an open seat, # represents an occupied seat, and . represents floor with no seat. The example given looks like:

L.LL.LL.LL
LLLLLLL.LL
L.L.L..L..
LLLL.LL.LL
L.LL.LL.LL
L.LLLLL.LL
..L.L.....
LLLLLLLLLL
L.LLLLLL.L
L.LLLLL.LL

In part one, the rules state that a seat will go from L –> # if none of the eight seats adjacent to it are occupied. A seat will go from # –> L if four or more of the adjacent seats are occupied. Otherwise, it will stay the same.

In part two, the rules change slightly. I no longer care about the seats directly next to the seat, but rather the first seat the occupant would see moving in each of the eight directions. The single seat in this example would have eight occupied neighbors:

.......#.
...#.....
.#.......
.........
..#L....#
....#....
.........
#........
...#.....

The other difference in part two is that now there must be five or more occupied seats before the seat goes to empty (up from four).

Solution

Part 1

I decided to handle my input as a list of lists called chairs. I’ll use R and C as short cuts for len(chairs) and len(chairs[0]), as those are used a lot, and a second grid called next_chairs which is the next state (as I don’t want to modify the original state while still calculating it). Now I can start a loop that updates the state into next_chairs, and then either breaks if no change happened, or copy next_chairs into chairs.

#!/usr/bin/env python3

import sys


with open(sys.argv[1], "r") as f:
    chairs = list(map(str.strip, f.readlines()))

R = len(chairs)
C = len(chairs[0])
neigh = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]

next_chairs = [["x" for c in r] for r in chairs]

while True:

    changed = False
    for r in range(R):
        for c in range(C):
            num_occ = sum(
                [
                    chairs[r + y][c + x] == "#"
                    for x, y in neigh
                    if 0 <= r + y < R and 0 <= c + x < C
                ]
            )

            if chairs[r][c] == "L" and num_occ == 0:
                next_chairs[r][c] = "#"
                changed = True
            elif chairs[r][c] == "#" and num_occ >= 4:
                next_chairs[r][c] = "L"
                changed = True
            else:
                next_chairs[r][c] = chairs[r][c]

    if not changed:
        break

    chairs = [[next_chairs[r][c] for c in range(C)] for r in range(R)]

part1 = sum([sum([chairs[r][c] == "#" for c in range(C)]) for r in range(R)])
print(f"Part 1: {part1}")

The only tricky part here is getting the number of occupied chairs for each chair. I used a list comprehension (I love them too much, I know). I’m looping over the eight possible neighbors, but only if that delta results in a number still in the valid range for chairs. Then I’ll check that neighbor spot to see if it’s occupied, and sum over that list (which effectively counts the True).

This runs and gives the result.

Part 2

For part two, I basically copied all of my part one code and modified it slightly. I could have made this into a function as well. This code gets a bit ugly with loops.

Two changes needed here. The easy one is to update the number of occupied neighbors that trigger the seat going empty:

            elif chairs[r][c] == "#" and num_occ >= 5:
                next_chairs[r][c] = "L"
                changed = True

The more difficult one is the sight-line look at neighbor seats. I replaced my list comprehension that counts neighbors with a proper loop over each direction:

            num_occ = 0
            for dr, dc in neigh:
                x = c + dc
                y = r + dr
                while 0 <= x < C and 0 <= y < R and chairs[y][x] == ".":
                    x += dc
                    y += dr
                if 0 <= x < C and 0 <= y < R and chairs[y][x] == "#":
                    num_occ += 1

For each direction, I’ll start by getting the positions x and y that is the first coordinate in that direction. Then, as long as it’s in-bounds and a floor place, I’ll continue moving in that direction. Once I hit non-floor or leave the grid, I’ll check if it’s in the grid and occupied, and if so, add it to the count.

Final Code

These aren’t instant, but not too slow either:

$ time python3 day11.py 11-puzzle.txt
Part 1: 2338
Part 2: 2134

real    0m5.627s
user    0m5.607s
sys     0m0.020s
#!/usr/bin/env python3

import sys


with open(sys.argv[1], "r") as f:
    chairs = list(map(str.strip, f.readlines()))

R = len(chairs)
C = len(chairs[0])
neigh = [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]

next_chairs = [["x" for c in r] for r in chairs]

while True:

    changed = False
    for r in range(R):
        for c in range(C):
            num_occ = sum(
                [
                    chairs[r + y][c + x] == "#"
                    for x, y in neigh
                    if 0 <= r + y < R and 0 <= c + x < C
                ]
            )

            if chairs[r][c] == "L" and num_occ == 0:
                next_chairs[r][c] = "#"
                changed = True
            elif chairs[r][c] == "#" and num_occ >= 4:
                next_chairs[r][c] = "L"
                changed = True
            else:
                next_chairs[r][c] = chairs[r][c]

    if not changed:
        break

    chairs = [[next_chairs[r][c] for c in range(C)] for r in range(R)]

part1 = sum([sum([chairs[r][c] == "#" for c in range(C)]) for r in range(R)])
print(f"Part 1: {part1}")


with open(sys.argv[1], "r") as f:
    chairs = list(map(str.strip, f.readlines()))

R = len(chairs)
C = len(chairs[0])

next_chairs = [["x" for c in r] for r in chairs]

while True:

    changed = False
    for r in range(R):
        for c in range(C):
            num_occ = 0
            for dr, dc in neigh:
                x = c + dc
                y = r + dr
                while 0 <= x < C and 0 <= y < R and chairs[y][x] == ".":
                    x += dc
                    y += dr
                if 0 <= x < C and 0 <= y < R and chairs[y][x] == "#":
                    num_occ += 1

            if chairs[r][c] == "L" and num_occ == 0:
                next_chairs[r][c] = "#"
                changed = True
            elif chairs[r][c] == "#" and num_occ >= 5:
                next_chairs[r][c] = "L"
                changed = True
            else:
                next_chairs[r][c] = chairs[r][c]

    if not changed:
        break

    chairs = [[next_chairs[r][c] for c in range(C)] for r in range(R)]

part2 = sum([sum([chairs[r][c] == "#" for c in range(C)]) for r in range(R)])
print(f"Part 2: {part2}")