advent-2022-python/advent/days/day18/solution.py
2023-01-08 14:02:48 +01:00

129 lines
4.2 KiB
Python

from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Iterable, Iterator, Self
day_num = 18
def part1(lines: Iterator[str]) -> int:
shower = Shower.create(Position3D.parse_all(lines))
return shower.faces
def part2(lines: Iterator[str]) -> int:
shower = Shower.create(Position3D.parse_all(lines))
return shower.faces - shower.count_trapped_droplets()
@dataclass(slots=True, frozen=True)
class Position3D:
x: int
y: int
z: int
@classmethod
def parse(cls, line: str) -> Self:
x, y, z = line.split(",")
return Position3D(int(x), int(y), int(z))
@classmethod
def parse_all(cls, lines: Iterable[str]) -> Iterator[Self]:
return (cls.parse(line) for line in lines)
def neighbors(self) -> Iterator[Position3D]:
yield Position3D(self.x + 1, self.y, self.z)
yield Position3D(self.x - 1, self.y, self.z)
yield Position3D(self.x, self.y + 1, self.z)
yield Position3D(self.x, self.y - 1, self.z)
yield Position3D(self.x, self.y, self.z + 1)
yield Position3D(self.x, self.y, self.z - 1)
def min_max(self, mm: tuple[Position3D, Position3D] | None) -> tuple[Position3D, Position3D]:
if mm is None:
return self, self
return (Position3D(min(mm[0].x, self.x),
min(mm[0].y, self.y),
min(mm[0].z, self.z)),
Position3D(max(mm[1].x, self.x),
max(mm[1].y, self.y),
max(mm[1].z, self.z)))
def is_between(self, min: Position3D, max: Position3D) -> bool:
return (min.x <= self.x <= max.x
and min.y <= self.y <= max.y
and min.z <= self.z <= max.z)
class DropletType(Enum):
DROP = 1
UNKNOWN = 2
OUTER = 3
@dataclass(slots=True, frozen=True)
class Shower:
droplets: dict[Position3D, tuple[DropletType, int]]
faces: int
@classmethod
def create(cls, positions: Iterable[Position3D]) -> Self:
droplets: dict[Position3D, tuple[DropletType, int]] = {}
faces = 0
for position in positions:
candidate = droplets.get(position)
if candidate is not None:
_, touching_faces = candidate
else:
touching_faces = 0
faces += 6
for neighbor in position.neighbors():
candidate = droplets.get(neighbor)
if candidate is not None:
droplet_type, neighbor_touching_faces = candidate
if droplet_type == DropletType.DROP:
touching_faces += 1
faces -= 2
droplets[neighbor] = droplet_type, neighbor_touching_faces + 1
else:
droplets[neighbor] = DropletType.UNKNOWN, 1
droplets[position] = DropletType.DROP, touching_faces
return Shower(droplets, faces)
def count_trapped_droplets(self) -> int:
droplets = self.droplets.copy()
minmax: tuple[Position3D, Position3D] | None = None
for position in droplets.keys():
minmax = position.min_max(minmax)
if minmax is None:
raise Exception("I got no data to work with")
min_values, max_values = minmax
todo: list[Position3D] = [min_values]
while todo:
current = todo[0]
todo = todo[1:]
candidate = droplets.get(current)
skip_further = True
if candidate is None:
droplets[current] = DropletType.OUTER, 0
skip_further = False
else:
droplet_type, faces = candidate
if droplet_type == DropletType.UNKNOWN:
droplets[current] = DropletType.OUTER, faces
skip_further = False
if not skip_further:
for neighbor in current.neighbors():
if neighbor.is_between(min_values, max_values):
todo.append(neighbor)
return sum(touching_faces
for droplet_type, touching_faces in droplets.values()
if droplet_type == DropletType.UNKNOWN)