This is a very easy problem to solve with numpy arrays; we can use numpy.split()
to divide the image in two parts, and numpy.fliy()
to mirror one of them. I do so for the first split half, as that's easier to then slice (from zero to the smaller width).
import typing as t
from dataclasses import dataclass
import numpy as np
from numpy.typing import NDArray
@dataclass
class ValleyPattern:
pattern: NDArray[np.bool_]
@classmethod
def from_text(cls, text: str) -> t.Self:
return cls(np.array([[c == "#" for c in line] for line in text.splitlines()]))
def _reflection(self, axis: t.Literal[0, 1], difference: int = 0) -> int:
patt, size = self.pattern, self.pattern.shape[axis]
for i in range(1, size):
rev, fwd = np.split(patt, [i], axis=axis)
idx = range(min(i, size - i))
if (
np.sum(
fwd.take(idx, axis=axis)
^ np.flip(rev, axis=axis).take(idx, axis=axis)
)
== difference
):
return i
return 0
def summary(self, difference: int = 0) -> int:
return (
self._reflection(1, difference)
or 100 * self._reflection(0, difference)
or 0
)
test_patterns = """\
#.##..##.
..#.##.#.
##......#
##......#
..#.##.#.
..##..##.
#.#.##.#.
#...##..#
#....#..#
..##..###
#####.##.
#####.##.
..##..###
#....#..#
""".split(
"\n\n"
)
test_valley_patterns = [ValleyPattern.from_text(blk) for blk in test_patterns]
assert sum(p.summary() for p in test_valley_patterns) == 405
import aocd
pattern_blocks = aocd.get_data(day=13, year=2023).split("\n\n")
valley_patterns = [ValleyPattern.from_text(blk) for blk in pattern_blocks]
print("Part 1:", sum(p.summary() for p in valley_patterns))
Part 1: 33520
Insteod of looking for a simple equality, you can find the reflection with a single smudge by looking at the difference between the two parts being equal to 1. Simple!
I refactored part 1 to sum the XOR (boolean difference) of the two parts, and for part 2 the difference needs to be 1 instead of 0.
assert sum(p.summary(1) for p in test_valley_patterns) == 400
print("Part 2:", sum(p.summary(1) for p in valley_patterns))
Part 2: 34824