aoc-2023/d03/main.py

111 lines
2.8 KiB
Python

from collections.abc import Iterable
from functools import reduce
from itertools import product
from pathlib import Path
from typing import NamedTuple
class Point(NamedTuple):
x: int
y: int
def iter_adjacent(self, wrap=False) -> Iterable["Point"]:
for offset in product((1, 0, -1), (1, 0, -1)):
adj = Point(self.x + offset[0], self.y + offset[1])
if adj == self:
continue
if not wrap and (adj.x < 0 or adj.y < 0):
continue
yield adj
def __str__(self) -> str:
return f"Point({self.x}, {self.y})"
def is_symbol(v: str) -> bool:
return not v.isnumeric() and v != "."
def read_part(grid: list[str], point: Point) -> tuple[Point, int]:
points = [point]
num = grid[point.y][point.x]
if not num.isnumeric():
raise ValueError(f"Can't read a part because {num}@{point} is not a number")
row = grid[point.y]
x = point.x - 1
while x >= 0 and row[x].isnumeric():
num = row[x] + num
points.append(Point(x, point.y))
x -= 1
left_point = Point(x, point.y)
x = point.x + 1
while x < len(row) and row[x].isnumeric():
num = num + row[x]
points.append(Point(x, point.y))
x += 1
print(f"Part {num}@{left_point} for {point}")
return left_point, int(num)
def find_parts(grid: list[str], point: Point) -> dict[Point, int]:
parts: dict[Point, int] = {}
for adjacent in point.iter_adjacent():
if adjacent.y >= len(grid) or adjacent.x >= len(grid[adjacent.y]):
continue
if grid[adjacent.y][adjacent.x].isnumeric():
point, part_num = read_part(grid, adjacent)
parts[point] = part_num
return parts
def part1(input: Path) -> int:
with input.open() as f:
grid = [line.strip() for line in f]
all_parts: dict[Point, int] = {}
for y, row in enumerate(grid):
for x, cell in enumerate(row):
p = Point(x, y)
if is_symbol(cell):
parts = find_parts(grid, p)
all_parts.update(parts)
return sum(all_parts.values())
def part2(input: Path) -> int:
with input.open() as f:
grid = [line.strip() for line in f]
total = 0
for y, row in enumerate(grid):
for x, cell in enumerate(row):
p = Point(x, y)
if cell != "*":
continue
parts = find_parts(grid, p)
if len(parts) != 2:
continue
gear_ratio = reduce(lambda x, y: x*y, parts.values(), 1)
total += gear_ratio
return total
if __name__ == "__main__":
result1 = part1(Path("./input.txt"))
print("part 1", result1)
result2 = part2(Path("./input.txt"))
print("part 2", result2)