Point of Incidence
2025-10-23 Original Prompt Part 1
While each block of terrain looks like a grid, we don’t have to treat it like one; in fact, leaving the blocks as lists of strings simplifies some of our work.
First, let’s create a function that will find the reflection point in a list of rows. We’ll use a neat trick to apply it to the columns later.
If we have an index (i) into a list of rows (rows), then rows[:i] gives us
the items above that index, and rows[i:] gives us the items below (and
including) that index. If there is a mirror at that index, each row below going
down should be the same as the corresponding row above going up.
Once we have the rows above and the rows below, we can loop through each
pair of corresponding rows using zip(reversed(above), below). (Note that the
rows above are reversed, because the last row above will be the one closest to
the mirror, and we’re moving outwards from there.) The location of the mirror
will be the one where all such pairs of rows are equal.
from collections.abc import Sequence
def find_mirror_row(rows: Sequence[Sequence[str]]) -> int: # NOTE 0 is not included, because the rows above would be empty, and # we'd detect this as a valid reflection. That's not what we want. for i in range(1, len(rows)): above, below = rows[:i], rows[i:] if all(a == b for a, b in zip(reversed(above), below)): return i return 0zip will stop looping at the end of the shortest sequence it receives, so we
won’t be comparing rows outside of the grid. And as a failsafe, we return 0 if
we don’t find a mirror between any rows.
Next, let’s create a function to score a single block of terrain. Row-mirrors are worth 100 times their location, and column-mirrors are worth their location. (I decided to return 0 if no mirror was found anywhere, but because AoC inputs are well-formed, this should never happen.)
def score_block(block: str) -> int: rows = block.splitlines() if row := find_mirror_row(rows): return 100 * row if col := find_mirror_row(list(zip(*rows))): return col return 0And here’s where the neat trick comes into play. If we have a list of rows
called rows, then zip(*rows) will iterate through the columns; this is
because it’s looping through each item of every row in parallel. (This is a very
useful trick to transpose a series of rows.)
>>> rows = ["line", "area", "vows", "ante"]>>> list(zip(*rows))[('l', 'a', 'v', 'a'), ('i', 'r', 'o', 'n'), ('n', 'e', 'w', 't'), ('e', 'a', 's', 'e')]Finally, we can sum up the scores of each block to solve Part 1.
...
class Solution(StrSplitSolution): separator = "\n\n"
def part_1(self) -> int: return sum(score_block(block) for block in self.input)Part 2
Turns out the mirrors have exactly one “smudge”, and the reflections are
identical except for this smudge. So our find_mirror_row function should take
that into account.
First, though, we should create a function that will count the number of smudges
in two rows (i.e. number of differences). This looks like a job for zip1
and sum.
def num_smudges(a: Sequence[str], b: Sequence[str]) -> int: return sum(char_a != char_b for char_a, char_b in zip(a, b))Now we can use this function to count the number of smudges in a reflection.
def find_mirror_row(rows: Sequence[Sequence[str]], smudges: int) -> int: # NOTE Row 0 is not included, because the rows above would be empty, # and we'd detect this as a valid reflection. We don't want that. for i in range(1, len(rows)): above, below = rows[:i], rows[i:] if ( sum(num_smudges(a, b) for a, b in zip(reversed(above), below)) == smudges ): return i return 0
def score_block(block: str, smudges: int = 0) -> int: rows = block.splitlines() if row := find_mirror_row(rows, smudges): return 100 * row if col := find_mirror_row(list(zip(*rows)), smudges): return col return 0And lastly, we can pass in the number of smudges we need for Part 2 (one smudge).
...
class Solution(StrSplitSolution): ...
def part_2(self) -> int: return sum(score_block(block, smudges=1) for block in self.input)This was the second row-list day we’ve had this year (the first one being
Day 3). I find these kinds of days rather nice,
especially when I get to use that zip(*rows) trick.
Footnotes
-
Wow, lots of
zips today, aren’t there? ↩