···11+22+import sys
33+import fileinput
44+55+from utils import parse_nums, Point, NE, SE, SW, NW
66+77+# How far to the closest beacon?
88+manhattan = {}
99+sensors = set()
1010+beacons = set()
1111+1212+min_x = 1e9
1313+max_x = -1e9
1414+1515+# Parse input
1616+for line in fileinput.input():
1717+ sx, sy, bx, by = parse_nums(line)
1818+ min_x = min(min_x, sx)
1919+ max_x = max(max_x, sx)
2020+ s = Point(sx, sy)
2121+ b = Point(bx, by)
2222+ sensors.add(s)
2323+ beacons.add(b)
2424+ manhattan[s] = s.dist_manhattan(b)
2525+2626+2727+# Part 1
2828+TARGET_Y = 2000000
2929+part_1 = 0
3030+3131+# Only consider the sensors whose area could possibly overlap with the given row.
3232+close_sensors = set(s for s in sensors if s.y - manhattan[s] <= TARGET_Y <= s.y + manhattan[s])
3333+3434+# Compute the intervals created by the sensor areas along this row.
3535+intervals = []
3636+for s in close_sensors:
3737+ vertical_delta = abs(s.y - TARGET_Y)
3838+ half_chord = manhattan[s] - vertical_delta
3939+ intervals.append((s.x - half_chord, s.x + half_chord))
4040+4141+# Merge the intervals to de-duplicate overlaps.
4242+intervals.sort()
4343+merged_intervals = []
4444+merged = set()
4545+for i, (a, b) in enumerate(intervals):
4646+ if i in merged:
4747+ continue
4848+4949+ end = b
5050+ for j, (c, d) in enumerate(intervals):
5151+ if i == j:
5252+ continue
5353+ if a <= c <= end:
5454+ end = max(end, d)
5555+ merged.add(j)
5656+5757+ merged_intervals.append((a, end))
5858+5959+# Answer is the sum of merged intervals minus any beacons in that row.
6060+part_1 = sum(b - a + 1 for a, b in merged_intervals)
6161+part_1 -= sum(1 for b in beacons if b.y == TARGET_Y)
6262+print("Part 1:", part_1)
6363+6464+6565+# Part 2
6666+MIN_BEACON = 0
6767+MAX_BEACON = 4000000
6868+6969+def gen_outskirts():
7070+ """Yields the set of points outside-neighbouring the perimeter of all sensors."""
7171+ for s in sensors:
7272+ d = manhattan[s] + 1
7373+ p = Point(s.x - d, s.y)
7474+ for move in [NE, SE, SW, NW]:
7575+ for _ in range(d):
7676+ p += move
7777+ if MIN_BEACON <= p.x <= MAX_BEACON and MIN_BEACON <= p.y <= MAX_BEACON:
7878+ yield p
7979+8080+# Check all outskirt positions for whether it could be the undetected beacon.
8181+for p in gen_outskirts():
8282+ for s in sensors:
8383+ if p.dist_manhattan(s) <= manhattan[s]:
8484+ break
8585+ else:
8686+ tuning_freq = p.x * 4000000 + p.y
8787+ print("Part 2:", tuning_freq)
8888+ break
8989+