You feel the ground rumble again as the distress signal leads you to a large network of subterranean tunnels. You don't have time to search them all, but you don't need to: your pack contains a set of deployable sensors that you imagine were originally built to locate lost Elves.
The sensors aren't very powerful, but that's okay; your handheld device indicates that you're close enough to the source of the distress signal to use them. You pull the emergency sensor system out of your pack, hit the big button on top, and the sensors zoom off down the tunnels.
Once a sensor finds a spot it thinks will give it a good reading, it attaches itself to a hard surface and begins monitoring for the nearest signal source beacon. Sensors and beacons always exist at integer coordinates. Each sensor knows its own position and can determine the position of a beacon precisely; however, sensors can only lock on to the one beacon closest to the sensor as measured by the Manhattan distance. (There is never a tie where two beacons are the same distance to a sensor.)
It doesn't take long for the sensors to report back their positions and closest beacons (your puzzle input). For example:
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
So, consider the sensor at 2,18
; the closest beacon to it is at -2,15
. For the sensor at 9,16
, the closest beacon to it is at 10,16
.
Drawing sensors as S
and beacons as B
, the above arrangement of sensors and beacons looks like this:
1 1 2 2
0 5 0 5 0 5
0 ....S.......................
1 ......................S.....
2 ...............S............
3 ................SB..........
4 ............................
5 ............................
6 ............................
7 ..........S.......S.........
8 ............................
9 ............................
10 ....B.......................
11 ..S.........................
12 ............................
13 ............................
14 ..............S.......S.....
15 B...........................
16 ...........SB...............
17 ................S..........B
18 ....S.......................
19 ............................
20 ............S......S........
21 ............................
22 .......................B....
This isn't necessarily a comprehensive map of all beacons in the area, though. Because each sensor only identifies its closest beacon, if a sensor detects a beacon, you know there are no other beacons that close or closer to that sensor. There could still be beacons that just happen to not be the closest beacon to any sensor. Consider the sensor at 8,7
:
1 1 2 2
0 5 0 5 0 5
-2 ..........#.................
-1 .........###................
0 ....S...#####...............
1 .......#######........S.....
2 ......#########S............
3 .....###########SB..........
4 ....#############...........
5 ...###############..........
6 ..#################.........
7 .#########S#######S#........
8 ..#################.........
9 ...###############..........
10 ....B############...........
11 ..S..###########............
12 ......#########.............
13 .......#######..............
14 ........#####.S.......S.....
15 B........###................
16 ..........#SB...............
17 ................S..........B
18 ....S.......................
19 ............................
20 ............S......S........
21 ............................
22 .......................B....
This sensor's closest beacon is at 2,10
, and so you know there are no beacons that close or closer (in any positions marked #
).
None of the detected beacons seem to be producing the distress signal, so you'll need to work out where the distress beacon is by working out where it isn't. For now, keep things simple by counting the positions where a beacon cannot possibly be along just a single row.
So, suppose you have an arrangement of beacons and sensors like in the example above and, just in the row where y=10, you'd like to count the number of positions a beacon cannot possibly exist. The coverage from all sensors near that row looks like this:
1 1 2 2
0 5 0 5 0 5
9 ...#########################...
10 ..####B######################..
11 .###S#############.###########.
In this example, in the row where y=10, there are 26 positions where a beacon cannot be present.
Consult the report from the sensors you just deployed. In the row where y=2000000, how many positions cannot contain a beacon?
# Python imports
import numpy as np
import re
from collections import defaultdict
from pathlib import Path
test = Path("data/day15_test.txt") # test data
data = Path("data/day15_data.txt") # puzzle data
def parse_input(fpath: Path) -> list[tuple[tuple, tuple, tuple]]:
"""Parse puzzle input: list of sensor/beacon location pairs and distances
:param fpath: path to puzzle input
Returns (sensor_location, beacon_location, distance vector) where
the distance vector is that between the sensor and the beacon
"""
pattern = re.compile("Sensor at x=([\-0-9]*), y=([\-0-9]*): closest beacon is at x=([\-0-9]*), y=([\-0-9]*)")
output = []
with fpath.open() as ifh:
for line in [_.strip() for _ in ifh.readlines()]:
data = [int(_) for _ in pattern.match(line).groups()]
output.append(((data[0], data[1]), (data[2], data[3]),
(abs(data[2] - data[0]), abs(data[3] - data[1]))))
return output
def part1(fpath: Path, row: int) -> int:
"""Solve the puzzle
:param fpath: path to puzzle input
:param row: row number named in the puzzle - we're returning the
number of locations on that row at which a beacon
cannot exist
"""
excluded_all = set() # hold all excluded points on the query row
data = parse_input(fpath)
for sensor, beacon, dist in data: # Iterate over sensor/beacon/distance data
# Calculate locations in excluded region on target row for each
# sensor/beacon combination
# Excluded region at sensor[1] + ydist has width (xdist * 2) + 1, centred on sensor[0]
# Each row further from sensor reduces width by 2, each closer row increases width by 2
# Width at sensor is then (xdist * 2) + 1 + (2 * ydist)
# Width at row is then (xdist * 2) + 1 + (2 * ydist) - 2 * abs(row - sensor[1])
senswidth = (dist[0] * 2) + 1 + (2 * dist[1])
row_width = senswidth - 2 * abs(row - sensor[1]) # width of excluded region
# Define locations on query row where there's no beacon, as a set
excluded_x = set(range(sensor[0] - row_width//2, sensor[0] + row_width//2 + 1))
excluded_all = excluded_all.union(excluded_x)
# Remove any beacon locations present in excluded_all
for sensor, beacon, dist in data:
if beacon[1] == row:
excluded_all.discard(beacon[0])
return len(excluded_all)
part1(test, 10)
26
part1(data, 2000000)
4827924
Your handheld device indicates that the distress signal is coming from a beacon nearby. The distress beacon is not detected by any sensor, but the distress beacon must have x
and y
coordinates each no lower than 0
and no larger than 4000000
.
To isolate the distress beacon's signal, you need to determine its tuning frequency, which can be found by multiplying its x
coordinate by 4000000
and then adding its y
coordinate.
In the example above, the search space is smaller: instead, the x
and y
coordinates can each be at most 20. With this reduced search area, there is only a single position that could have a beacon: x=14
, y=11
. The tuning frequency for this distress beacon is 56000011
.
Find the only possible position for the distress beacon. What is its tuning frequency?
def part2(fpath: Path, lim: int) -> int:
"""Solve the puzzle
:param fpath: path to puzzle input
:param lim: upper bound on x/y coords (all points within (0, 0) -> (lim, lim))
This solution is rather slow. There must be a more efficient way to do this.
Could we get there with simultaneous equations?
"""
data = parse_input(fpath) # Load puzzle data
all_candidates = defaultdict(int) # holds locations in scope (within limits) that might be distress beacon
# The point we're looking for is exactly dist+1 away from any nearest sensor
# The solution is isolated, so must be bounded by at least four sensors at dist-1
# We iterate over all sensors, generating dist+1 points, and see which of those
# points is dist+1 points from at least 4 sensors
for sensor, beacon, dist in data:
dist = sum(dist)
# Scan from leftmost sensor-excluded region, populating with
# the points dist + 1 from the sensor
for xpos in range(max(0, sensor[0] - (dist + 1)), min(sensor[0] + (dist + 1), lim)):
yshift = abs(abs(xpos - sensor[0]) - abs(dist + 1)) # distance up/down to candidate
if 0 <= (sensor[1] + yshift) <= lim:
all_candidates[(xpos, sensor[1] + yshift)] += 1
if 0 <= (sensor[1] - yshift) <= lim:
all_candidates[(xpos, sensor[1] - yshift)] += 1
# Identify locations with four dist+1 sensors, that are within the limits
# These are our candidate solutions
locations = {_[0] for _ in all_candidates.items() if _[1] >= 4}
if len(locations) == 1: # We have found the only solution
x, y = tuple(list(locations)[0])
return 4000000 * x + y
# If we've more than one solution, we need to exclude disallowed options
# Exclude those within range of a sensor
for sensor, beacon, dist in data:
dist = sum(dist)
exclude = set()
for location in locations:
if abs(sensor[0] - location[0]) + abs(sensor[1] - location[1]) <= dist:
exclude.add(location)
locations = locations.difference(exclude)
# Return the solution (we trust there is only one)
x, y = tuple(list(locations)[0])
return 4000000 * x + y
part2(test, 20)
56000011
import time
t0 = time.time()
print(part2(data, 4000000))
print(f"{time.time() - t0}s")
12977110973564 41.07965898513794s