diff --git a/README.md b/README.md index c25b697..bb7132e 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ I use python 3.11 without any libraries beyond the standard. | Day | Time | Rank | Score | Time | Rank | Score | | --- | --------- | ----- | ----- | -------- | ----- | ----- | +| 15 | 00:58:10 | 3963 | 0 | 02:26:22 | 4011 | 0 | | 14 | 00:58:39 | 4431 | 0 | 01:18:15 | 4620 | 0 | | 13 | 01:23:44 | 5522 | 0 | 01:45:59 | 5610 | 0 | | 12 | 01:43:14 | 6571 | 0 | 01:51:15 | 6246 | 0 | diff --git a/advent/days/day15/__init__.py b/advent/days/day15/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/advent/days/day15/data/input.txt b/advent/days/day15/data/input.txt new file mode 100644 index 0000000..3ee5618 --- /dev/null +++ b/advent/days/day15/data/input.txt @@ -0,0 +1,32 @@ +2000000/4000000 +Sensor at x=3859432, y=2304903: closest beacon is at x=3677247, y=3140958 +Sensor at x=2488890, y=2695345: closest beacon is at x=1934788, y=2667279 +Sensor at x=3901948, y=701878: closest beacon is at x=4095477, y=368031 +Sensor at x=2422190, y=1775708: closest beacon is at x=1765036, y=2000000 +Sensor at x=2703846, y=3282799: closest beacon is at x=2121069, y=3230302 +Sensor at x=172003, y=2579074: closest beacon is at x=-77667, y=3197309 +Sensor at x=1813149, y=1311283: closest beacon is at x=1765036, y=2000000 +Sensor at x=1704453, y=2468117: closest beacon is at x=1934788, y=2667279 +Sensor at x=1927725, y=2976002: closest beacon is at x=1934788, y=2667279 +Sensor at x=3176646, y=1254463: closest beacon is at x=2946873, y=2167634 +Sensor at x=2149510, y=3722117: closest beacon is at x=2121069, y=3230302 +Sensor at x=3804434, y=251015: closest beacon is at x=4095477, y=368031 +Sensor at x=2613561, y=3932220: closest beacon is at x=2121069, y=3230302 +Sensor at x=3997794, y=3291220: closest beacon is at x=3677247, y=3140958 +Sensor at x=98328, y=3675176: closest beacon is at x=-77667, y=3197309 +Sensor at x=2006541, y=2259601: closest beacon is at x=1934788, y=2667279 +Sensor at x=663904, y=122919: closest beacon is at x=1618552, y=-433244 +Sensor at x=1116472, y=3349728: closest beacon is at x=2121069, y=3230302 +Sensor at x=2810797, y=2300748: closest beacon is at x=2946873, y=2167634 +Sensor at x=1760767, y=2024355: closest beacon is at x=1765036, y=2000000 +Sensor at x=3098487, y=2529092: closest beacon is at x=2946873, y=2167634 +Sensor at x=1716839, y=634872: closest beacon is at x=1618552, y=-433244 +Sensor at x=9323, y=979154: closest beacon is at x=-245599, y=778791 +Sensor at x=1737623, y=2032367: closest beacon is at x=1765036, y=2000000 +Sensor at x=26695, y=3049071: closest beacon is at x=-77667, y=3197309 +Sensor at x=3691492, y=3766350: closest beacon is at x=3677247, y=3140958 +Sensor at x=730556, y=1657010: closest beacon is at x=1765036, y=2000000 +Sensor at x=506169, y=3958647: closest beacon is at x=-77667, y=3197309 +Sensor at x=2728744, y=23398: closest beacon is at x=1618552, y=-433244 +Sensor at x=3215227, y=3077078: closest beacon is at x=3677247, y=3140958 +Sensor at x=2209379, y=3030851: closest beacon is at x=2121069, y=3230302 diff --git a/advent/days/day15/data/test01.txt b/advent/days/day15/data/test01.txt new file mode 100644 index 0000000..19d5711 --- /dev/null +++ b/advent/days/day15/data/test01.txt @@ -0,0 +1,15 @@ +10/20 +Sensor at x=2, y=18: closest beacon is at x=-2, y=15 +Sensor at x=9, y=16: closest beacon is at x=10, y=16 +Sensor at x=13, y=2: closest beacon is at x=15, y=3 +Sensor at x=12, y=14: closest beacon is at x=10, y=16 +Sensor at x=10, y=20: closest beacon is at x=10, y=16 +Sensor at x=14, y=17: closest beacon is at x=10, y=16 +Sensor at x=8, y=7: closest beacon is at x=2, y=10 +Sensor at x=2, y=0: closest beacon is at x=2, y=10 +Sensor at x=0, y=11: closest beacon is at x=2, y=10 +Sensor at x=20, y=14: closest beacon is at x=25, y=17 +Sensor at x=17, y=20: closest beacon is at x=21, y=22 +Sensor at x=16, y=7: closest beacon is at x=15, y=3 +Sensor at x=14, y=3: closest beacon is at x=15, y=3 +Sensor at x=20, y=1: closest beacon is at x=15, y=3 \ No newline at end of file diff --git a/advent/days/day15/solution.py b/advent/days/day15/solution.py new file mode 100644 index 0000000..3e2fb8d --- /dev/null +++ b/advent/days/day15/solution.py @@ -0,0 +1,117 @@ +from __future__ import annotations +from dataclasses import dataclass + +from typing import Iterator, Self + +day_num = 15 + + +def part1(lines: Iterator[str]) -> int: + row, _ = next(lines).split('/') + sensor_map = SensorMap.parse(lines) + return sensor_map.count_impossible(int(row)) + + +def part2(lines: Iterator[str]) -> int: + _, max_range = next(lines).split('/') + sensor_map = SensorMap.parse(lines) + return sensor_map.get_possible_frequency(int(max_range)) + + +Position = tuple[int, int] +ColRange = tuple[int, int] + + +@dataclass(slots=True, frozen=True) +class Sensor: + sensor: Position + distance: int + + @classmethod + def parse(cls, line: str) -> tuple[Self, Position]: + parts = line.split('=') + sensor = int(parts[1].split(',')[0].strip()), int(parts[2].split(':')[0].strip()) + beacon = int(parts[3].split(',')[0].strip()), int(parts[4].strip()) + return cls(sensor, Sensor.manhatten(sensor, beacon)), beacon + + @classmethod + def manhatten(cls, first: Position, other: Position) -> int: + return abs(first[0] - other[0]) + abs(first[1] - other[1]) + + def col_range_at_row(self, row: int) -> ColRange | None: + col_distance = self.distance - abs(self.sensor[1] - row) + if col_distance < 0: + return None + + from_x = self.sensor[0] - col_distance + to_x = self.sensor[0] + col_distance + + return from_x, to_x + + +@dataclass(slots=True, frozen=True) +class SensorMap: + sensors: list[Sensor] + beacons: set[Position] + + @classmethod + def parse(cls, lines: Iterator[str]) -> SensorMap: + sensors: list[Sensor] = [] + beacons: set[Position] = set() + for line in lines: + sensor, beacon = Sensor.parse(line) + sensors.append(sensor) + beacons.add(beacon) + return cls(sensors, beacons) + + def get_impossible(self, row: int) -> list[ColRange]: + col_ranges: list[ColRange] = [] + + for sensor in self.sensors: + x_range = sensor.col_range_at_row(row) + if x_range is None: + continue + from_x, to_x = x_range + col_ranges.append((from_x, to_x)) + + return col_ranges + + @classmethod + def merged_col_ranges(cls, col_ranges: list[ColRange]) -> Iterator[ColRange]: + col_ranges = sorted(col_ranges) + current = col_ranges[0] + for col_range in col_ranges: + if current[1] < col_range[1]: + if current[1] < col_range[0]: + yield current + current = col_range + else: + current = current[0], col_range[1] + yield current + + def count_impossible(self, row: int) -> int: + col_ranges = self.get_impossible(row) + + seen = sum(rng[1] - rng[0] + 1 for rng in SensorMap.merged_col_ranges(col_ranges)) + beacons = len({beacon[0] for beacon in self.beacons if beacon[1] == row}) + + return seen - beacons + + def get_possible(self, max_range: int) -> Position: + for row in range(max_range): + col_ranges = sorted(self.get_impossible(row)) + + curr1 = col_ranges[0][1] + for one0, one1 in col_ranges: + if curr1 < one1: + if curr1 < one0: + return curr1 + 1, row + if one1 > max_range: + break + curr1 = one1 + + raise Exception("No best spot found") + + def get_possible_frequency(self, max_range: int) -> int: + freq_x, freq_y = self.get_possible(max_range) + return freq_x * 4_000_000 + freq_y diff --git a/advent/days/day15/test_solution.py b/advent/days/day15/test_solution.py new file mode 100644 index 0000000..48ef866 --- /dev/null +++ b/advent/days/day15/test_solution.py @@ -0,0 +1,49 @@ +from advent.common import input + +from .solution import Sensor, SensorMap, day_num, part1, part2 + + +def test_part1(): + lines = input.read_lines(day_num, 'test01.txt') + expected = 26 + result = part1(lines) + assert result == expected + + +def test_part2(): + lines = input.read_lines(day_num, 'test01.txt') + expected = 56000011 + result = part2(lines) + assert result == expected + + +def test_parse(): + input = "Sensor at x=2, y=18: closest beacon is at x=-2, y=15" + expected = Sensor((2, 18), 7), (-2, 15) + result = Sensor.parse(input) + assert result == expected + + +def test_x_range(): + input = "Sensor at x=8, y=7: closest beacon is at x=2, y=10" + sensor, _ = Sensor.parse(input) + assert sensor.col_range_at_row(10) == (2, 14) + assert sensor.col_range_at_row(11) == (3, 13) + + +def test_impossible(): + lines = input.read_lines(day_num, 'test01.txt') + next(lines) + sensor_map = SensorMap.parse(lines) + expected = 26 + result = sensor_map.count_impossible(10) + assert result == expected + + +def test_possible(): + lines = input.read_lines(day_num, 'test01.txt') + next(lines) + sensor_map = SensorMap.parse(lines) + expected = 56000011 + result = sensor_map.get_possible_frequency(20) + assert result == expected