#!/usr/bin/env python import itertools import sys from dataclasses import dataclass from typing import List, Optional, Set, Tuple @dataclass(eq=True, frozen=True) # Hash-able class Vector: x: int y: int z: int def __add__(self, other: "Vector") -> "Vector": return Vector(self.x + other.x, self.y + other.y, self.z + other.z) def __sub__(self, other: "Vector") -> "Vector": return Vector(self.x - other.x, self.y - other.y, self.z - other.z) @dataclass(eq=True, frozen=True) # Hash-able class Matrix: v1: Vector v2: Vector v3: Vector def __matmul__(self, other: Vector) -> Vector: return Vector( self.v1.x * other.x + self.v1.y * other.y + self.v1.z * other.z, self.v2.x * other.x + self.v2.y * other.y + self.v2.z * other.z, self.v3.x * other.x + self.v3.y * other.y + self.v3.z * other.z, ) def rotations() -> List[Matrix]: def cos(angle: int) -> int: if angle == 0: return 1 if angle == 180: return -1 assert angle in (90, 270) # Sanity check return 0 def sin(angle: int) -> int: if angle == 90: return 1 if angle == 270: return -1 assert angle in (0, 180) # Sanity check return 0 def rotate(x: int, y: int, z: int) -> Matrix: v1 = Vector( cos(z) * cos(y), cos(z) * sin(y) * sin(x) - sin(z) * cos(x), cos(z) * sin(y) * cos(x) + sin(z) * sin(x), ) v2 = Vector( sin(z) * cos(y), sin(z) * sin(y) * sin(x) + cos(z) * cos(x), sin(z) * sin(y) * cos(x) - cos(z) * sin(x), ) v3 = Vector(-sin(y), cos(y) * sin(x), cos(y) * cos(x)) return Matrix(v1, v2, v3) return [ rotate(0, 0, 0), rotate(90, 0, 0), rotate(180, 0, 0), rotate(270, 0, 0), rotate(0, 90, 0), rotate(90, 90, 0), rotate(180, 90, 0), rotate(270, 90, 0), rotate(0, 180, 0), rotate(90, 180, 0), rotate(180, 180, 0), rotate(270, 180, 0), rotate(0, 270, 0), rotate(90, 270, 0), rotate(180, 270, 0), rotate(270, 270, 0), rotate(0, 0, 90), rotate(90, 0, 90), rotate(180, 0, 90), rotate(270, 0, 90), rotate(0, 0, 270), rotate(90, 0, 270), rotate(180, 0, 270), rotate(270, 0, 270), ] ROTATIONS = rotations() BeaconList = Set[Vector] def solve(input: List[str]) -> int: def parse() -> List[BeaconList]: res: List[BeaconList] = [] for line in input: if "scanner" in line: res.append(set()) continue if line == "": continue x, y, z = map(int, line.split(",")) res[-1].add(Vector(x, y, z)) return res def find_overlap( known: BeaconList, other: BeaconList ) -> Optional[Tuple[Matrix, Vector]]: def find_delta(known: BeaconList, other: BeaconList) -> Optional[Vector]: for dest, source in itertools.product(known, rotated): delta = dest - source if sum((v + delta) in known for v in rotated) >= 12: return delta return None for r in ROTATIONS: rotated = set(r @ v for v in other) if (delta := find_delta(known, rotated)) is not None: return r, delta return None def apply(known: BeaconList, other: BeaconList) -> Tuple[bool, BeaconList]: res = find_overlap(known, other) if res is None: return False, known rot, delta = res new = {(rot @ v) + delta for v in other} # Return whether there are new points in the set return not (new <= known), (known | new) def match_all(scans: List[BeaconList]) -> BeaconList: # First scan is our basis known = scans[0] # No need to inspect the first scan in the future to_match = scans[1:] while to_match: s = to_match.pop(0) applied, known = apply(known, s) if not applied: to_match.append(s) return known beacons = parse() return len(match_all(beacons)) def main() -> None: input = [line.strip() for line in sys.stdin.readlines()] print(solve(input)) if __name__ == "__main__": main()