|
1 | 1 | from __future__ import annotations
|
2 | 2 |
|
3 | 3 | import time
|
| 4 | +from collections import defaultdict |
4 | 5 | from pathlib import Path
|
| 6 | +from typing import Protocol |
5 | 7 |
|
6 | 8 | DIR = Path(__file__).parent
|
7 | 9 | FILE = DIR.parents[3] / "inputs" / f"{DIR.stem}.txt"
|
8 | 10 |
|
| 11 | +CoordType = tuple[int, int] |
| 12 | +"""Generic coordinate, 2-tuple of ints.""" |
9 | 13 |
|
10 |
| -def part1(contents: str): |
11 |
| - return "Not done yet!" |
12 | 14 |
|
| 15 | +class CalcerType(Protocol): |
| 16 | + """Protocol for a caculator func that can be passed to `num_antinodes` function.""" |
13 | 17 |
|
14 |
| -def part2(contents: str): |
15 |
| - return "Not done yet!" |
| 18 | + def __call__( |
| 19 | + self, |
| 20 | + grid: list[str], |
| 21 | + c1: CoordType, |
| 22 | + c2: CoordType, |
| 23 | + ) -> set[CoordType]: ... |
| 24 | + |
| 25 | + |
| 26 | +def _calc_anti_nodes( |
| 27 | + grid: list[str], |
| 28 | + c1: CoordType, |
| 29 | + c2: CoordType, |
| 30 | + part2: bool = False, |
| 31 | +) -> set[CoordType]: |
| 32 | + """Calculates antinodes for the given two points, in part 1.""" |
| 33 | + len_col, len_row = len(grid), len(grid[0]) |
| 34 | + permutations = [(c1, c2), (c2, c1)] |
| 35 | + new_antinodes: set[CoordType] = set() |
| 36 | + |
| 37 | + if part2: |
| 38 | + # Preset the "new" antinodes to include the current nodes |
| 39 | + new_antinodes = {c1, c2} |
| 40 | + |
| 41 | + for first, second in permutations: |
| 42 | + curr_a, curr_b = first, second |
| 43 | + while True: |
| 44 | + new_node = ( |
| 45 | + (2 * curr_b[0]) - curr_a[0], |
| 46 | + (2 * curr_b[1]) - curr_a[1], |
| 47 | + ) |
| 48 | + if -1 < new_node[0] < len_col and -1 < new_node[1] < len_row: |
| 49 | + new_antinodes.add(new_node) |
| 50 | + # TODO accurate swap? |
| 51 | + curr_a, curr_b = curr_b, new_node |
| 52 | + if not part2: |
| 53 | + break |
| 54 | + else: |
| 55 | + break |
| 56 | + |
| 57 | + return new_antinodes |
| 58 | + |
| 59 | + |
| 60 | +def num_antinodes(grid: list[str], part2: bool = False) -> int: |
| 61 | + """General method for getting the number of antinodes.""" |
| 62 | + antinodes: dict[CoordType, list[str]] = defaultdict(list) |
| 63 | + nodes: dict[str, set[CoordType]] = defaultdict(set) |
| 64 | + |
| 65 | + for x, line in enumerate(grid): |
| 66 | + for y, char in enumerate(line): |
| 67 | + if char == ".": |
| 68 | + # skip non-antenna |
| 69 | + continue |
| 70 | + this_coord = (x, y) |
| 71 | + for seen_node in nodes[char]: |
| 72 | + new_antinodes = _calc_anti_nodes( |
| 73 | + grid=grid, |
| 74 | + c1=seen_node, |
| 75 | + c2=this_coord, |
| 76 | + part2=part2, |
| 77 | + ) |
| 78 | + for new_antinode in new_antinodes: |
| 79 | + antinodes[new_antinode].append(char) |
| 80 | + nodes[char].add(this_coord) |
| 81 | + |
| 82 | + return len(antinodes) |
| 83 | + |
| 84 | + |
| 85 | +def part1(grid: list[str]) -> int: |
| 86 | + """Part 1 solver.""" |
| 87 | + return num_antinodes(grid=grid) |
| 88 | + |
| 89 | + |
| 90 | +def part2(grid: list[str]) -> int: |
| 91 | + """Part 2 solver.""" |
| 92 | + return num_antinodes(grid=grid, part2=True) |
16 | 93 |
|
17 | 94 |
|
18 | 95 | def main():
|
19 |
| - contents = FILE.read_text() |
| 96 | + """Entrypoint.""" |
| 97 | + grid = FILE.read_text().strip().split("\n") |
20 | 98 |
|
21 | 99 | _start1 = time.perf_counter()
|
22 |
| - result1 = part1(contents) |
| 100 | + result1 = part1(grid) |
23 | 101 | _delta1 = time.perf_counter() - _start1
|
24 | 102 | print(f">> Part 1: {result1} ({_delta1:.6f}s)")
|
25 | 103 |
|
26 | 104 | _start2 = time.perf_counter()
|
27 |
| - result2 = part2(contents) |
| 105 | + result2 = part2(grid) |
28 | 106 | _delta2 = time.perf_counter() - _start2
|
29 | 107 | print(f">> Part 2: {result2} ({_delta2:.6f}s)")
|
30 | 108 |
|
|
0 commit comments