-
Notifications
You must be signed in to change notification settings - Fork 0
/
day-19.py
299 lines (236 loc) · 9.44 KB
/
day-19.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import re
import sys
from collections import defaultdict
from itertools import combinations
from math import comb, sqrt
import numpy as np
# Get the day from the filename e.g. day-1.py
DAY = re.match(r"day\-(\d+)\.py", sys.argv[0]).groups()[0]
def read_data():
with open(f"{path}/day-{DAY}.txt", "r") as f:
# Split on the empty line
values = [i.split("\n") for i in f.read().split("\n\n")]
# Create a dictionary of the scanners to the co-ordinates
values = {
re.match(r"\-\-\- scanner (\d+) \-\-\-", i[0]).groups()[0]: [
tuple(int(j) for j in x.split(",")) for x in i[1:]
]
for i in values
}
return values
def distance(point1, point2):
"""Get the distance between 2 3D points"""
return sqrt(
pow(point2[0] - point1[0], 2)
+ pow(point2[1] - point1[1], 2)
+ pow(point2[2] - point1[2], 2)
)
def manhattan_distance(point1, point2):
"""Get the manhattan distance between two points"""
return sum(abs(point1[i] - point2[i]) for i in range(3))
def get_all_distances(values):
"""Get all pairs of distances"""
scanner_distances = {}
# Get the distances between all points for each scanner, for each beacon
for scanner, beacons in values.items():
# Create empty list of the distances
distances = defaultdict(list)
# Calculate distance between all pairs of points
for b1, b2 in combinations(beacons, 2):
dist = distance(b1, b2)
distances[b1].append(dist)
distances[b2].append(dist)
# Record all possible distances between points
scanner_distances[scanner] = dict(distances)
return scanner_distances
def get_common_locations(scanner_distances, number_scanners):
"""Create a dictionary showing the scanners with common points"""
# Find how many distances there are in common between beacons in each
# scanner to work out the beacons in common
common = {}
# Add the first 'known' scanner
locations = {
"0": {
"start": (0, 0, 0),
"axis_order": ["x", "y", "z"],
"flip": [1, 1, 1],
}
}
seen_scanners = set(["0"])
s1 = "0"
# Keep going until all scanners have been found
while len(seen_scanners) < number_scanners:
for s2 in scanner_distances.keys():
# Don't end up in a loop
if s2 in seen_scanners:
continue
# Create empty list of the distances
common_beacons = []
# Flag to stop it checking the pairs once it's found the 12
found = False
for b1, d1 in scanner_distances[s1].items():
for b2, d2 in scanner_distances[s2].items():
# If there are at least 11 distances in common
if len(set(d1).intersection(set(d2))) >= 11:
# Then the beacon is common between the two scanners
common_beacons.append((b1, b2))
# If there are at least 12 common beacons, then we say
# the scanners overlap, and we stop checking this pair
if len(common_beacons) >= 12:
common[(s1, s2)] = common_beacons
found = True
break
# Stop checking this pair
if found:
break
# If they don't have common beacons, look at the next sensor
if not found:
continue
# Now find out the relative position of s2 from s1
# Firstly, need to work out which co-ordinates are the 'same'
# common_beacons is of the form:
# [
# ((404, -588, -901), (-336, 658, 858)),
# ((528, -643, 409), (-460, 603, -452)),
# ((390, -675, -793), (-322, 571, 750)),
# ]
# Want to find the pattern and work out which are 'flipped' and
# translated
# Look at the differences in the pattern to identify which
# co-ordinate is which relative to the start sensor
b1 = [i[0] for i in common_beacons]
b2 = [i[1] for i in common_beacons]
# Get the differences
d1 = [np.diff([i[j] for i in b1]) for j in range(3)]
d2 = [np.diff([i[j] for i in b2]) for j in range(3)]
# Identify which is which
axis_order = [None, None, None]
flip = [1, 1, 1]
known_start = locations[s1]["start"]
known_axis_order = locations[s1]["axis_order"]
known_flip = locations[s1]["flip"]
# Loop through each 'known' option
for option, axis in enumerate(d1):
# Work out the 'arrangement' for the 'known' scanner
this_axis = known_axis_order[option]
this_flip = known_flip[option]
# Look at each possible option to determine the layout of the
# 'unknown' scanner
for i in range(3):
# If this axis is identical, then it's clearly the same
if list(axis) == list(d2[i]):
axis_order[i] = this_axis
flip[i] = this_flip
break
# If it's the same value, but negative, it's flipped
elif list(axis) == [-x for x in d2[i]]:
axis_order[i] = this_axis
flip[i] = this_flip * -1
break
# Now we know the order and 'flip' can compare the values to get the
# start position
point1 = [i * b for i, b in zip(known_flip, b1[0])]
# Rearrange the axes
point1 = (
point1[known_axis_order.index("x")],
point1[known_axis_order.index("y")],
point1[known_axis_order.index("z")],
)
# Flip as required
point2 = [i * b for i, b in zip(flip, b2[0])]
# Rearrange the axes
point2 = (
point2[axis_order.index("x")],
point2[axis_order.index("y")],
point2[axis_order.index("z")],
)
# Use rules of vectors
start = tuple(
known_start[i] + point1[i] - point2[i] for i in range(3)
)
# Add to the known list
locations[s2] = {
"start": start,
"axis_order": axis_order,
"flip": flip,
}
# Look at the pair for this one
s1 = s2
seen_scanners.add(s2)
# Couldn't find a pair, so try and find a different pair that we already know
s1 = str((int(s1) + 1) % number_scanners)
while s1 not in seen_scanners:
s1 = str((int(s1) + 1) % number_scanners)
return locations
def part_1(path):
"""Part 1/Star 1"""
# Open the file
values = read_data()
# Get the number of scanners
number_scanners = len(values.keys())
# Get the distances between the beacons
scanner_distances = get_all_distances(values)
# Get the details about each scanner
locations = get_common_locations(scanner_distances, number_scanners)
# Now need to get a full list of the beacons
all_beacons = set()
for scanner, beacons in values.items():
known_start = locations[scanner]["start"]
known_axis_order = locations[scanner]["axis_order"]
known_flip = locations[scanner]["flip"]
for beacon in beacons:
# Now we know the order, 'flip' and start, can get the actual
# position
# Flip as required
point = [i * b for i, b in zip(known_flip, beacon)]
# Rearrange the axes
point = [
point[known_axis_order.index("x")],
point[known_axis_order.index("y")],
point[known_axis_order.index("z")],
]
# Add to the start
all_beacons.add(tuple(point[i] + known_start[i] for i in range(3)))
# Print out the response
print(f"Task 1 Answer: {len(all_beacons)}")
def part_2(path):
"""Part 2/Star 2"""
# Open the file
values = read_data()
# Get the number of scanners
number_scanners = len(values.keys())
# Get the distances between the beacons
scanner_distances = get_all_distances(values)
# Get the details about each scanner
locations = get_common_locations(scanner_distances, number_scanners)
# Get all the start points
start_points = [locations[i]["start"] for i in locations.keys()]
answer = 0
for p1, p2 in combinations(start_points, 2):
answer = max([manhattan_distance(p1, p2), answer])
# Print out the response
print(f"Task 2 Answer: {answer}")
if __name__ == "__main__":
"""
Run using e.g.
`python day-1.py -test`
`python day-1.py`
`python day-1.py -test -2`
`python day-1.py -2`
`python day-1.py -test -both`
`python day-1.py -both`
"""
# Identify the folder that the input is in
test = "-test" in sys.argv
if test:
path = "input-tests"
else:
path = "inputs"
# Identify which one to run - 1 is default
if "-2" in sys.argv:
part_2(path)
elif "-both" in sys.argv:
part_1(path)
part_2(path)
else:
part_1(path)