day15 much faster
This commit is contained in:
parent
b83bb6b37a
commit
3d00f265ca
3 changed files with 145 additions and 38 deletions
|
|
@ -5,66 +5,97 @@ from typing import Iterator, Literal
|
|||
|
||||
@dataclass(slots=True, frozen=True, order=True)
|
||||
class Position:
|
||||
""" This class represents a position in 2D integer space """
|
||||
x: 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:
|
||||
""" Gets the first or second component of this position """
|
||||
match index:
|
||||
case 0: return self.x
|
||||
case 1: return self.y
|
||||
case _: raise IndexError()
|
||||
|
||||
def __iter__(self) -> Iterator[int]:
|
||||
""" Iterates over all components of this psition """
|
||||
yield self.x
|
||||
yield self.y
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def __neg__(self) -> Position:
|
||||
""" Creates a copy of this position with all values negated """
|
||||
return Position(-self.x, -self.y)
|
||||
|
||||
def __add__(self, other: Position) -> Position:
|
||||
""" Adds the components of these two positions """
|
||||
return Position(self.x + other.x, self.y + other.y)
|
||||
|
||||
def __sub__(self, other: Position) -> Position:
|
||||
""" Subtracts the components of these two positions """
|
||||
return Position(self.x - other.x, self.y - other.y)
|
||||
|
||||
def __mul__(self, factor: int) -> Position:
|
||||
""" Multiplies all components of this position by the given 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:
|
||||
return Position(self.x + 1, self.y)
|
||||
""" Returns the neighboring position to the right """
|
||||
return self + UNIT_X
|
||||
|
||||
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:
|
||||
return Position(self.x - 1, self.y)
|
||||
""" Returns the neighboring position to the left """
|
||||
return self + UNIT_NEG_X
|
||||
|
||||
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]:
|
||||
""" Returns an iterator of all direct neighbors """
|
||||
yield self.right()
|
||||
yield self.up()
|
||||
yield self.left()
|
||||
yield self.down()
|
||||
|
||||
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
|
||||
|
||||
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_Y = Position(0, 1)
|
||||
UNIT_NEG_X = Position(-1, 0)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
from itertools import combinations
|
||||
|
||||
from typing import Iterator, Self
|
||||
|
||||
from advent.common.position import Position
|
||||
from advent.common.position import ORIGIN, Position
|
||||
|
||||
day_num = 15
|
||||
|
||||
|
|
@ -25,26 +26,65 @@ ColRange = tuple[int, int]
|
|||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class Sensor:
|
||||
sensor: Position
|
||||
number: int
|
||||
position: Position
|
||||
distance: int
|
||||
|
||||
@classmethod
|
||||
def parse(cls, line: str) -> tuple[Self, Position]:
|
||||
def parse(cls, line: str, number: int) -> tuple[Self, Position]:
|
||||
parts = line.split('=')
|
||||
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()))
|
||||
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:
|
||||
col_distance = self.distance - abs(self.sensor.y - row)
|
||||
col_distance = self.distance - abs(self.position.y - row)
|
||||
if col_distance < 0:
|
||||
return None
|
||||
|
||||
from_x = self.sensor.x - col_distance
|
||||
to_x = self.sensor.x + col_distance
|
||||
from_x = self.position.x - col_distance
|
||||
to_x = self.position.x + col_distance
|
||||
|
||||
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)
|
||||
class SensorMap:
|
||||
|
|
@ -55,8 +95,8 @@ class SensorMap:
|
|||
def parse(cls, lines: Iterator[str]) -> SensorMap:
|
||||
sensors: list[Sensor] = []
|
||||
beacons: set[Position] = set()
|
||||
for line in lines:
|
||||
sensor, beacon = Sensor.parse(line)
|
||||
for number, line in enumerate(lines):
|
||||
sensor, beacon = Sensor.parse(line, number)
|
||||
sensors.append(sensor)
|
||||
beacons.add(beacon)
|
||||
return cls(sensors, beacons)
|
||||
|
|
@ -83,7 +123,7 @@ class SensorMap:
|
|||
yield current
|
||||
current = col_range
|
||||
else:
|
||||
current = current[0], col_range[1]
|
||||
current = ColRange((current[0], col_range[1]))
|
||||
yield current
|
||||
|
||||
def count_impossible(self, row: int) -> int:
|
||||
|
|
@ -94,21 +134,57 @@ class SensorMap:
|
|||
|
||||
return seen - beacons
|
||||
|
||||
def get_possible(self, max_range: int) -> Position:
|
||||
for row in range(max_range):
|
||||
col_ranges = sorted(self.get_impossible(row))
|
||||
@classmethod
|
||||
def tuning_frequency(cls, position: Position) -> int:
|
||||
return position.x * 4_000_000 + position.y
|
||||
|
||||
curr1 = col_ranges[0][1]
|
||||
for one0, one1 in col_ranges:
|
||||
if curr1 < one1:
|
||||
if curr1 < one0:
|
||||
return Position(curr1 + 1, row)
|
||||
if one1 > max_range:
|
||||
break
|
||||
curr1 = one1
|
||||
@classmethod
|
||||
def get_midline(cls, sensor1: Sensor, sensor2: Sensor) -> ManhattenLine | None:
|
||||
distance = sensor1.position.taxicab_distance(sensor2.position)
|
||||
if distance != sensor1.distance + sensor2.distance + 2:
|
||||
return None
|
||||
|
||||
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:
|
||||
freq_x, freq_y = self.get_possible(max_range)
|
||||
return freq_x * 4_000_000 + freq_y
|
||||
max_point = Position.splat(max_range)
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@ def test_part2():
|
|||
|
||||
def test_parse():
|
||||
input = "Sensor at x=2, y=18: closest beacon is at x=-2, y=15"
|
||||
expected = Sensor(Position(2, 18), 7), Position(-2, 15)
|
||||
result = Sensor.parse(input)
|
||||
expected = Sensor(1, Position(2, 18), 7), Position(-2, 15)
|
||||
result = Sensor.parse(input, 1)
|
||||
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)
|
||||
sensor, _ = Sensor.parse(input, 1)
|
||||
assert sensor.col_range_at_row(10) == (2, 14)
|
||||
assert sensor.col_range_at_row(11) == (3, 13)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue