day15 much faster

This commit is contained in:
Ruediger Ludwig 2023-01-16 20:24:34 +01:00
parent b83bb6b37a
commit 3d00f265ca
3 changed files with 145 additions and 38 deletions

View file

@ -5,66 +5,97 @@ from typing import Iterator, Literal
@dataclass(slots=True, frozen=True, order=True) @dataclass(slots=True, frozen=True, order=True)
class Position: class Position:
""" This class represents a position in 2D integer space """
x: int x: int
y: int y: int
@classmethod
def splat(cls, value: int) -> Position:
""" Creates a Position with two equal values """
return Position(value, value)
def __str__(self) -> str:
return f"({self.x}, {self.y})"
def __getitem__(self, index: Literal[0, 1]) -> int: def __getitem__(self, index: Literal[0, 1]) -> int:
""" Gets the first or second component of this position """
match index: match index:
case 0: return self.x case 0: return self.x
case 1: return self.y case 1: return self.y
case _: raise IndexError() case _: raise IndexError()
def __iter__(self) -> Iterator[int]: def __iter__(self) -> Iterator[int]:
""" Iterates over all components of this psition """
yield self.x yield self.x
yield self.y yield self.y
def set_x(self, x: int) -> Position: def set_x(self, x: int) -> Position:
""" Creates a copy of this position with x set to the new value """
return Position(x, self.y) return Position(x, self.y)
def set_y(self, y: int) -> Position: def set_y(self, y: int) -> Position:
""" Creates a copy of this position with y set to the new value """
return Position(self.x, y) return Position(self.x, y)
def __neg__(self) -> Position: def __neg__(self) -> Position:
""" Creates a copy of this position with all values negated """
return Position(-self.x, -self.y) return Position(-self.x, -self.y)
def __add__(self, other: Position) -> Position: def __add__(self, other: Position) -> Position:
""" Adds the components of these two positions """
return Position(self.x + other.x, self.y + other.y) return Position(self.x + other.x, self.y + other.y)
def __sub__(self, other: Position) -> Position: def __sub__(self, other: Position) -> Position:
""" Subtracts the components of these two positions """
return Position(self.x - other.x, self.y - other.y) return Position(self.x - other.x, self.y - other.y)
def __mul__(self, factor: int) -> Position: def __mul__(self, factor: int) -> Position:
""" Multiplies all components of this position by the given factor """
return Position(self.x * factor, self.y * factor) return Position(self.x * factor, self.y * factor)
def taxicab_distance(self, other: Position | None = None) -> int:
if other is None:
return abs(self.x) + abs(self.y)
else:
return abs(self.x - other.x) + abs(self.y - other.y)
def right(self) -> Position: def right(self) -> Position:
return Position(self.x + 1, self.y) """ Returns the neighboring position to the right """
return self + UNIT_X
def up(self) -> Position: def up(self) -> Position:
return Position(self.x, self.y - 1) """ Returns the neighboring position above """
return self + UNIT_NEG_Y
def left(self) -> Position: def left(self) -> Position:
return Position(self.x - 1, self.y) """ Returns the neighboring position to the left """
return self + UNIT_NEG_X
def down(self) -> Position: def down(self) -> Position:
return Position(self.x, self.y + 1) """ Returns the neighboring position below """
return self + UNIT_Y
def unit_neighbors(self) -> Iterator[Position]: def unit_neighbors(self) -> Iterator[Position]:
""" Returns an iterator of all direct neighbors """
yield self.right() yield self.right()
yield self.up() yield self.up()
yield self.left() yield self.left()
yield self.down() yield self.down()
def is_within(self, top_left: Position, bottom_right: Position) -> bool: def is_within(self, top_left: Position, bottom_right: Position) -> bool:
"""
Checks if this point is within the rectangle spanned by the given positions.
bottom_right is considered to be the first point outside the spanned rectangle.
Behavior is undefined if top_left and bottom_right are not in the correct order.
"""
return top_left.x <= self.x < bottom_right.x and top_left.y <= self.y < bottom_right.y return top_left.x <= self.x < bottom_right.x and top_left.y <= self.y < bottom_right.y
def taxicab_distance(self, other: Position | None = None) -> int:
"""
If other is given returns the taxicab distance from this point to other.
Otherwise returns the taxicab distance of this position to the Origin.
"""
if other is None:
return abs(self.x) + abs(self.y)
else:
return abs(self.x - other.x) + abs(self.y - other.y)
ORIGIN = Position(0, 0)
ORIGIN = Position.splat(0)
UNIT_X = Position(1, 0) UNIT_X = Position(1, 0)
UNIT_Y = Position(0, 1) UNIT_Y = Position(0, 1)
UNIT_NEG_X = Position(-1, 0) UNIT_NEG_X = Position(-1, 0)

View file

@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from itertools import combinations
from typing import Iterator, Self from typing import Iterator, Self
from advent.common.position import Position from advent.common.position import ORIGIN, Position
day_num = 15 day_num = 15
@ -25,26 +26,65 @@ ColRange = tuple[int, int]
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
class Sensor: class Sensor:
sensor: Position number: int
position: Position
distance: int distance: int
@classmethod @classmethod
def parse(cls, line: str) -> tuple[Self, Position]: def parse(cls, line: str, number: int) -> tuple[Self, Position]:
parts = line.split('=') parts = line.split('=')
sensor = Position(int(parts[1].split(',')[0].strip()), int(parts[2].split(':')[0].strip())) sensor = Position(int(parts[1].split(',')[0].strip()), int(parts[2].split(':')[0].strip()))
beacon = Position(int(parts[3].split(',')[0].strip()), int(parts[4].strip())) beacon = Position(int(parts[3].split(',')[0].strip()), int(parts[4].strip()))
return cls(sensor, sensor.taxicab_distance(beacon)), beacon return cls(number, sensor, sensor.taxicab_distance(beacon)), beacon
def col_range_at_row(self, row: int) -> ColRange | None: def col_range_at_row(self, row: int) -> ColRange | None:
col_distance = self.distance - abs(self.sensor.y - row) col_distance = self.distance - abs(self.position.y - row)
if col_distance < 0: if col_distance < 0:
return None return None
from_x = self.sensor.x - col_distance from_x = self.position.x - col_distance
to_x = self.sensor.x + col_distance to_x = self.position.x + col_distance
return from_x, to_x return from_x, to_x
def is_within(self, position: Position) -> bool:
return self.position.taxicab_distance(position) <= self.distance
@dataclass(slots=True, frozen=True)
class ManhattenLine:
start: Position
direction_up: bool
steps: int
@classmethod
def create(cls, start: Position, end: Position) -> ManhattenLine | None:
if start.x > end.x:
start, end = end, start
steps_x = end.x - start.x
steps_y = end.y - start.y
if steps_x != abs(steps_y):
return None
return ManhattenLine(start, steps_y < 0, steps_x)
def crosspoint(self, other: ManhattenLine) -> Position | None:
if self.direction_up == other.direction_up:
return None
elif self.direction_up:
bottom_up, top_down = self, other
else:
bottom_up, top_down = other, self
r2 = bottom_up.start.x + bottom_up.start.y - (top_down.start.x + top_down.start.y)
if r2 % 2 != 0:
return None
r = r2 // 2
if r < 0 or r > top_down.steps:
return None
return top_down.start + Position.splat(r)
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
class SensorMap: class SensorMap:
@ -55,8 +95,8 @@ class SensorMap:
def parse(cls, lines: Iterator[str]) -> SensorMap: def parse(cls, lines: Iterator[str]) -> SensorMap:
sensors: list[Sensor] = [] sensors: list[Sensor] = []
beacons: set[Position] = set() beacons: set[Position] = set()
for line in lines: for number, line in enumerate(lines):
sensor, beacon = Sensor.parse(line) sensor, beacon = Sensor.parse(line, number)
sensors.append(sensor) sensors.append(sensor)
beacons.add(beacon) beacons.add(beacon)
return cls(sensors, beacons) return cls(sensors, beacons)
@ -83,7 +123,7 @@ class SensorMap:
yield current yield current
current = col_range current = col_range
else: else:
current = current[0], col_range[1] current = ColRange((current[0], col_range[1]))
yield current yield current
def count_impossible(self, row: int) -> int: def count_impossible(self, row: int) -> int:
@ -94,21 +134,57 @@ class SensorMap:
return seen - beacons return seen - beacons
def get_possible(self, max_range: int) -> Position: @classmethod
for row in range(max_range): def tuning_frequency(cls, position: Position) -> int:
col_ranges = sorted(self.get_impossible(row)) return position.x * 4_000_000 + position.y
curr1 = col_ranges[0][1] @classmethod
for one0, one1 in col_ranges: def get_midline(cls, sensor1: Sensor, sensor2: Sensor) -> ManhattenLine | None:
if curr1 < one1: distance = sensor1.position.taxicab_distance(sensor2.position)
if curr1 < one0: if distance != sensor1.distance + sensor2.distance + 2:
return Position(curr1 + 1, row) return None
if one1 > max_range:
break
curr1 = one1
raise Exception("No best spot found") if sensor1.position.x == sensor2.position.x or sensor1.position.y == sensor2.position.y:
return None # Don't know how to handle that now, maybe include later
if sensor1.position.x > sensor2.position.x:
sensor1, sensor2 = sensor2, sensor1
if sensor1.position.y < sensor2.position.y:
if sensor1.distance < sensor2.distance:
return ManhattenLine.create(Position(sensor1.position.x + sensor1.distance + 1,
sensor1.position.y),
Position(sensor1.position.x,
sensor1.position.y + sensor1.distance + 1))
else:
return ManhattenLine.create(Position(sensor2.position.x - sensor2.distance - 1,
sensor2.position.y),
Position(sensor2.position.x,
sensor2.position.y - sensor2.distance - 1))
else:
if sensor1.distance < sensor2.distance:
return ManhattenLine.create(Position(sensor1.position.x,
sensor1.position.y - sensor1.distance - 1),
Position(sensor1.position.x + sensor1.distance + 1,
sensor1.position.y))
else:
return ManhattenLine.create(Position(sensor2.position.x,
sensor2.position.y + sensor2.distance + 1),
Position(sensor2.position.x - sensor2.distance - 1,
sensor2.position.y))
def get_possible_frequency(self, max_range: int) -> int: def get_possible_frequency(self, max_range: int) -> int:
freq_x, freq_y = self.get_possible(max_range) max_point = Position.splat(max_range)
return freq_x * 4_000_000 + freq_y midlines: list[ManhattenLine] = []
for sensor1, sensor2 in combinations(self.sensors, 2):
midline = SensorMap.get_midline(sensor1, sensor2)
if midline is not None:
midlines.append(midline)
for line1, line2 in combinations(midlines, 2):
point = line1.crosspoint(line2)
if point is not None:
if (point.is_within(ORIGIN, max_point)
and all(not sensor.is_within(point) for sensor in self.sensors)):
return SensorMap.tuning_frequency(point)
raise Exception("No point found")

View file

@ -20,14 +20,14 @@ def test_part2():
def test_parse(): def test_parse():
input = "Sensor at x=2, y=18: closest beacon is at x=-2, y=15" input = "Sensor at x=2, y=18: closest beacon is at x=-2, y=15"
expected = Sensor(Position(2, 18), 7), Position(-2, 15) expected = Sensor(1, Position(2, 18), 7), Position(-2, 15)
result = Sensor.parse(input) result = Sensor.parse(input, 1)
assert result == expected assert result == expected
def test_x_range(): def test_x_range():
input = "Sensor at x=8, y=7: closest beacon is at x=2, y=10" input = "Sensor at x=8, y=7: closest beacon is at x=2, y=10"
sensor, _ = Sensor.parse(input) sensor, _ = Sensor.parse(input, 1)
assert sensor.col_range_at_row(10) == (2, 14) assert sensor.col_range_at_row(10) == (2, 14)
assert sensor.col_range_at_row(11) == (3, 13) assert sensor.col_range_at_row(11) == (3, 13)