def read_file(file: TextIO):
"""Read lines from the `file`."""
lines = (line.rstrip() for line in file)
grid = [[int(c) for c in line] for line in lines]
assert all(len(row) == len(grid[0]) for row in grid)
return grid
def part1(file: TextIO) -> int:
"""Solve the first part of the puzzle."""
grid = read_file(file)
height, width = len(grid), len(grid[0])
def generate_larger(positions):
threshold = -1
for pos in positions:
y, x = pos
value = grid[y][x]
if value > threshold:
yield pos
threshold = value
seen = set()
for y in range(height):
seen.update(generate_larger((y, x) for x in range(width)))
seen.update(generate_larger((y, x) for x in reversed(range(width))))
for x in range(width):
seen.update(generate_larger((y, x) for y in range(height)))
seen.update(generate_larger((y, x) for y in reversed(range(height))))
return len(seen)
def scenic_score(grid, y, x):
"""Calculate the scenic score of the tree at (x, y)."""
threshold = grid[y][x]
def count_visible(positions):
count = 0
for pos in positions:
count += 1
y, x = pos
if grid[y][x] >= threshold:
break
return count
north = count_visible((y2, x) for y2 in reversed(range(y)))
south = count_visible((y2, x) for y2 in range(y + 1, len(grid)))
west = count_visible((y, x2) for x2 in reversed(range(x)))
east = count_visible((y, x2) for x2 in range(x + 1, len(grid[y])))
return north * south * west * east
def part2(file: TextIO) -> int:
"""Solve the second part of the puzzle."""
grid = read_file(file)
height, width = len(grid), len(grid[0])
return max(
scenic_score(grid, y, x)
for y in range(height)
for x in range(width))

The problem reminded me of a nice little sudoku-like puzzle called Towers.

People were speculating that the next puzzle might involve Dijkstra’s. I think I got in my head and immediately tried to solve this with Dijkstra’s without thinking it through. It didn’t make a ton of sense. Today was my worst performance so far this year.

How I tried to use Dijkstra's

mark edges visible
to_visit = visible
while to_visit:
new_node = []
for node in to_visit:
for neighbor in node.neighbors:
if neighbor is visible from edge through node:
new_node.add(neighbor)
visible.add(neighbor)
to_visit = new_node

Python solution

DIRECTIONS = [-1j ** i for i in range(4)]
def part1(self, board: InputType) -> int:
count = 0
for tree in board:
for direction in DIRECTIONS:
cur = tree + direction
while cur in board:
if board[cur] >= board[tree]:
break
cur += direction
else:
count += 1
break
return count
def part2(self, board: InputType) -> int:
scores = []
for tree in board:
visible = []
for direction in DIRECTIONS:
cur = tree + direction
num = 0
while cur in board:
num += 1
if board[cur] >= board[tree]:
break
cur += direction
visible.append(num)
scores.append(self.mult(visible))
return max(scores)

I got to reuse an existing Board from prior years for this.