This was an interesting problem! It took me about 45 minutes to work out. Part two was interesting to think about and work out.
I played around with a few configurations to optimize runtimes and managed to solve part two in anywhere from 110s to 1.5s.
Code: Latest – Just Solve It
Runtime Details
- Take 1, using the NUC and
dataclass
for the Sensor: 110s - Take 2, using the NUC and a
tuple
for the Sensor, reducing function calls: 53s - Take 3, same code as take 2 but on the desktop: 37s.
- Take 4, same code and hardware as take 3, switching cython for mypy3: 1.5s.
Python Solution
def part1(self, parsed_input: InputType) -> int:
"""Return the number of coordinates on a given row which cannot contain the beacon.
For each sensor, find the x-range the sensor covers on the given row.
"""
row = 10 if self.testing else 2000000
x_positions = set()
for sensor_x, sensor_y, sensor_range in parsed_input:
y_dist = abs(row - sensor_y)
if y_dist > sensor_range:
continue
covered_x_range_start = sensor_x - (sensor_range - y_dist)
covered_x_range_end = sensor_x + (sensor_range - y_dist)
x_positions.update(range(covered_x_range_start, covered_x_range_end))
return len(x_positions)
def part2(self, parsed_input: InputType) -> int:
"""Locate the distress beacon.
For each row, start a cursor at the left.
Iterate through sensors in order, and move the cursor to just past the right
edge of the sensor (if the cursor is within sensor range).
When all sensors were applied, if the cursor is within the boundaries of the board,
that is where the distress beacon is located.
"""
dims = 20 if self.testing else 4000000
sensor_tuples = parsed_input
# Sort sensors, left to right.
sensor_tuples.sort()
# Test each row until we find the beacon.
for candidate_y in range(dims + 1):
candidate_x = 0
# For each sensor, move the cursor to just past the right end, if it is in sensor range.
for sensor_x, sensor_y, sensor_range in sensor_tuples:
y_dist = abs(sensor_y - candidate_y)
point_dist = abs(sensor_x - candidate_x) + y_dist
if point_dist <= sensor_range:
candidate_x = sensor_x + sensor_range - y_dist + 1
if candidate_x <= dims:
self.debug(f"Solved!!! {candidate_x, candidate_y}")
return candidate_x * 4000000 + candidate_y
def line_parser(self, line: str) -> tuple[int, int, int]:
"""Parse a line into a tuple of sensor's coordinates and range."""
a, b, c, d = (int(i) for i in aoc.RE_INT.findall(line))
return a, b, abs(a - c) + abs(b - d)