Skip to content

Commit 1f91b46

Browse files
committed
Day 19
1 parent b7c47ff commit 1f91b46

File tree

5 files changed

+437
-18
lines changed

5 files changed

+437
-18
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
| 16 | [source](src/advent_2021_clojure/day16.clj) | [blog](docs/day16.md) |
2121
| 17 | [source](src/advent_2021_clojure/day17.clj) | [blog](docs/day17.md) |
2222
| 18 | [source](src/advent_2021_clojure/day18.clj) | [blog](docs/day18.md) |
23+
| 19 | [source](src/advent_2021_clojure/day19.clj) | [blog](docs/day19.md) |
2324
| 20 | [source](src/advent_2021_clojure/day20.clj) | [blog](docs/day20.md) |
2425
| 21 | [source](src/advent_2021_clojure/day21.clj) | [blog](docs/day21.md) |
2526
| 22 | [source](src/advent_2021_clojure/day22.clj) | [blog](docs/day22.md) |

docs/day19.md

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
# Day Ninteen: Beacon Scanner
2+
3+
* [Problem statement](https://adventofcode.com/2021/day/19)
4+
* [Solution code](https://github.com/abyala/advent-2021-clojure/blob/master/src/advent_2021_clojure/day19.clj)
5+
6+
---
7+
8+
## Preamble
9+
10+
I'll just come out and say that this problem was too hard for a single day. I could see this having been split into 2
11+
or even 3 different problems, given the complexities of both the background and the puzzle itself. I have a working
12+
solution that's pretty slow, but I got there.
13+
14+
One of my biggest struggles with this problem was understanding the nature of the beacons and the scanners. I
15+
improperly spent a large amount of time having created the space of beacons around the scanner, and then attempted to
16+
rotate the entire space. This is not how the problem is intended to be interpretted. Instead, one needs to hold the
17+
beacons steady, and then change the way that the scanner is pointing. So the trick was to think of the beacons as
18+
normal points within a cube, and to imagine the scanner as a six-sided die with only a single point having dots.
19+
20+
Once I got that, I was at least closer to a working solution. But teasing it apart piece by piece, the puzzle is
21+
doable.
22+
23+
---
24+
25+
## Part 1
26+
27+
First, let's parse our input. Splitting the data into groups of lines, I'll transform each block of text lines into a
28+
scanner of form `{:id n, :beacons #{[x y z]}, :scanners #{[0 0 0]}}`. The ID comes from the header line, the beacons
29+
from the rest of the data, and the scanners will be a set of a three-dimensional origin, since the scanners begin not
30+
having any context to each other. Finally, the parser will map each ID to its scanner, thus ending with a total
31+
structure of `{n {:id n, :beacons #{[x y z]}, :scanners #{[0 0 0]}}`.
32+
33+
```clojure
34+
(defn parse-scanner [input]
35+
(let [[header & beacons] (str/split-lines input)]
36+
{:id (->> header (re-seq #"\d+") first parse-int)
37+
:beacons (->> beacons
38+
(map (fn [line] (mapv parse-int (str/split line #","))))
39+
set)
40+
:scanners #{[0 0 0]}}))
41+
42+
(defn parse-input [input]
43+
(reduce #(assoc %1 (:id %2) %2)
44+
{}
45+
(map parse-scanner (utils/split-blank-line input))))
46+
```
47+
48+
Next, we'll make a vector of 24 transformational functions, which represent the different ways that the _scanner_ can
49+
face. Again, this has nothing to do with the beacons; it's all about the scanner. To that end, there are six groups of
50+
four functions, signifying the six directions the scanner can face, and the four rotations it can make within each
51+
direction. Each of these functions takes in an `[x y z]` point, and returns how the scanner would transform that point
52+
based on its orientation. I was only able to come up with this vector by placing and rotating objects within my house,
53+
so I could see how all 3 dimensions work together.
54+
55+
```clojure
56+
(def orientation-fns [; Face straight
57+
identity
58+
(fn [[x y z]] [(- z) y x])
59+
(fn [[x y z]] [(- x) y (- z)])
60+
(fn [[x y z]] [z y (- x)])
61+
62+
; Face right
63+
(fn [[x y z]] [(- y) x z])
64+
(fn [[x y z]] [(- z) x (- y)])
65+
(fn [[x y z]] [y x (- z)])
66+
(fn [[x y z]] [z x y])
67+
68+
; Face behind
69+
(fn [[x y z]] [(- x) (- y) z])
70+
(fn [[x y z]] [(- z) (- y) (- x)])
71+
(fn [[x y z]] [x (- y) (- z)])
72+
(fn [[x y z]] [z (- y) x])
73+
74+
; Face left
75+
(fn [[x y z]] [y (- x) z])
76+
(fn [[x y z]] [(- z) (- x) y])
77+
(fn [[x y z]] [(- y) (- x) (- z)])
78+
(fn [[x y z]] [z (- x) (- y)])
79+
80+
; Face up
81+
(fn [[x y z]] [x z (- y)])
82+
(fn [[x y z]] [y z x])
83+
(fn [[x y z]] [(- x) z y])
84+
(fn [[x y z]] [(- y) z (- x)])
85+
86+
; Face down
87+
(fn [[x y z]] [x (- z) y])
88+
(fn [[x y z]] [(- y) (- z) x])
89+
(fn [[x y z]] [(- x) (- z) (- y)])
90+
(fn [[x y z]] [y (- z) (- x)])])
91+
```
92+
93+
Next, I'll make two helper functions for relating two points to each other. `path-to` takes in a `from` point and a
94+
`to` point, and returns the different between the two, representing the values to add to `from` to end up at `to`.
95+
Then `follow-path` takes in a point and a path from `path-to`, and moves the point along that path.
96+
97+
```clojure
98+
(defn path-to [from to] (mapv - to from))
99+
(defn follow-path [point path] (mapv + point path))
100+
```
101+
102+
Next, I have three similar functions - `combine-beacons`, `combine-scanners`, and `combine-all-scanners`. Let's handle
103+
them one at a time.
104+
105+
`combine-beacons` takes in two sets of beacons, from two separate scanners. Assuming the first set of beacons is the
106+
fixed set, it attempts to return the first path that would move at least 12 points from the second set onto the first.
107+
For this, we'll use `(for [b0 beacons0, b1 beacons1] (path-to b1 b0))` to return _almost_ all possible vectors for the
108+
paths from the second set of beacons onto the first. We know that at least points need to overlap, but we don't know
109+
which ones. Regardless, instead of testing all of `beacons1` against all of `beacons0`, we can test against all but any
110+
11 arbitrary values from `beacons0` instead; if we know we need at least 12 points to match, then one working path
111+
should imply at least 12 ways to get to it. Finally, to see if the path is acceptable, we call `follow-path` on all
112+
beacons in the second set, and test if the intersection against the first set if at leats 12; if not, then that path
113+
does not lead to the necessary overlap. The function then returns either the first workable path, or else `nil`.
114+
115+
```clojure
116+
(defn combine-beacons [beacons0 beacons1]
117+
(->> (for [b0 (drop 11 beacons0), b1 beacons1] (path-to b1 b0))
118+
(filter (fn [path] (>= (->> (map #(follow-path % path) beacons1)
119+
set
120+
(set/intersection beacons0)
121+
count)
122+
12)))
123+
first))
124+
```
125+
126+
Peeling up a layer, we get to `combine-scanners`. This looks at two scanners and their beacons, and checks to see if
127+
there is any orientation of `scanner1` such that its beacons overlap with those from `scanner0`. If so, then return
128+
what `scanner0` would look like if we merged in all of the data from `scanner1`. This means applying the orientation
129+
function to all of the points, finding the working path, moving all reoriented points along that path, and adding them
130+
to `scanner0`'s beacons. Similarly, since both scanners think themselves the origin, we'll reorient the `:scanners` in
131+
`scanner1` and send them along the path, adding the resulting values ot the set of `:scanners` in `scanner0`. If the
132+
two scanners can't overlap, again return `nil`.
133+
134+
```clojure
135+
(defn combine-scanners [scanner0 scanner1]
136+
(let [[beacons0 beacons1] (map :beacons [scanner0 scanner1])]
137+
(first (keep (fn [f]
138+
(let [beacons1' (map f beacons1)]
139+
(when-some [path (combine-beacons beacons0 beacons1')]
140+
(-> scanner0
141+
(update :beacons set/union (set (map #(follow-path % path) beacons1')))
142+
(update :scanners (fn [s] (apply conj s (map #(follow-path (f %) path) (:scanners scanner1)))))))))
143+
orientation-fns))))
144+
```
145+
146+
Finally, `combine-all-scanners` takes in the map of parsed scanners, and keeps combining them until there's only a
147+
single scanner remaining. It starts by looking at all unique pairs of IDs in incrementing value, since there's no need
148+
to join scanner 2 to scanner 3 if we can't join scanner 3 to scanner 2. Then for each pair, if they can be joined, then
149+
remove the second scanner and update the first one to have the combined data. After going through all possible pairs,
150+
recurse back through the function again until there's only one scanner remaining. Note that since this function takes a
151+
long time to run, I left in some `println` statements to show progress.
152+
153+
```clojure
154+
(defn combine-all-scanners [scanners]
155+
(println "Examining" (count scanners) "scanners")
156+
(if (= (count scanners) 1)
157+
(scanners 0)
158+
(recur (reduce (fn [acc [id0 id1]] (if-not (and (acc id0) (acc id1))
159+
acc
160+
(if-some [scanner' (combine-scanners (acc id0) (acc id1))]
161+
(do (println "Combining" id0 "with" id1 "from keys" (keys acc))
162+
(-> acc
163+
(dissoc id1)
164+
(assoc id0 scanner')))
165+
acc)))
166+
scanners
167+
(for [id0 (keys scanners), id1 (keys scanners), :when (> id1 id0)] [id0 id1])))))
168+
```
169+
170+
Finally, for `part1`, we just have to count the number of beacons remaining in the merged scanner. So parse the
171+
data, combine the scanners, and get the count of beacons.
172+
173+
```clojure
174+
(defn part1 [input] (->> input parse-input combine-all-scanners :beacons count))
175+
```
176+
177+
---
178+
179+
## Part 2
180+
181+
For part 2, we need to again combine all of the scanners, but then calculate the maximum distance between any two
182+
scanners. For this, we'll implement a three-dimensional `manhattan-distance` function, which computes the path between
183+
two points, and adds together the sum of each path's dimension. Then we pair together each set of scanner points,
184+
calculate the `manhattan-distance`, and return the largest value seen.
185+
186+
```clojure
187+
(defn manhattan-distance [p1 p2]
188+
(->> (path-to p1 p2)
189+
(map utils/abs)
190+
(apply +)))
191+
192+
(defn greatest-distances [s]
193+
(->> (for [v1 s, v2 s, :when (not= v1 v2)] [v1 v2])
194+
(map (partial apply manhattan-distance))
195+
(apply max)))
196+
197+
(defn part2 [input] (->> input parse-input combine-all-scanners :scanners greatest-distances))
198+
```
199+
200+
So yeah - it's not a whole lot of code once we're all said and done, but I found the puzzle very difficult to
201+
understand. Still, it's done! The code is slow and verbose, but it's done!

resources/day19_sample_data.txt

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
--- scanner 0 ---
2+
404,-588,-901
3+
528,-643,409
4+
-838,591,734
5+
390,-675,-793
6+
-537,-823,-458
7+
-485,-357,347
8+
-345,-311,381
9+
-661,-816,-575
10+
-876,649,763
11+
-618,-824,-621
12+
553,345,-567
13+
474,580,667
14+
-447,-329,318
15+
-584,868,-557
16+
544,-627,-890
17+
564,392,-477
18+
455,729,728
19+
-892,524,684
20+
-689,845,-530
21+
423,-701,434
22+
7,-33,-71
23+
630,319,-379
24+
443,580,662
25+
-789,900,-551
26+
459,-707,401
27+
28+
--- scanner 1 ---
29+
686,422,578
30+
605,423,415
31+
515,917,-361
32+
-336,658,858
33+
95,138,22
34+
-476,619,847
35+
-340,-569,-846
36+
567,-361,727
37+
-460,603,-452
38+
669,-402,600
39+
729,430,532
40+
-500,-761,534
41+
-322,571,750
42+
-466,-666,-811
43+
-429,-592,574
44+
-355,545,-477
45+
703,-491,-529
46+
-328,-685,520
47+
413,935,-424
48+
-391,539,-444
49+
586,-435,557
50+
-364,-763,-893
51+
807,-499,-711
52+
755,-354,-619
53+
553,889,-390
54+
55+
--- scanner 2 ---
56+
649,640,665
57+
682,-795,504
58+
-784,533,-524
59+
-644,584,-595
60+
-588,-843,648
61+
-30,6,44
62+
-674,560,763
63+
500,723,-460
64+
609,671,-379
65+
-555,-800,653
66+
-675,-892,-343
67+
697,-426,-610
68+
578,704,681
69+
493,664,-388
70+
-671,-858,530
71+
-667,343,800
72+
571,-461,-707
73+
-138,-166,112
74+
-889,563,-600
75+
646,-828,498
76+
640,759,510
77+
-630,509,768
78+
-681,-892,-333
79+
673,-379,-804
80+
-742,-814,-386
81+
577,-820,562
82+
83+
--- scanner 3 ---
84+
-589,542,597
85+
605,-692,669
86+
-500,565,-823
87+
-660,373,557
88+
-458,-679,-417
89+
-488,449,543
90+
-626,468,-788
91+
338,-750,-386
92+
528,-832,-391
93+
562,-778,733
94+
-938,-730,414
95+
543,643,-506
96+
-524,371,-870
97+
407,773,750
98+
-104,29,83
99+
378,-903,-323
100+
-778,-728,485
101+
426,699,580
102+
-438,-605,-362
103+
-469,-447,-387
104+
509,732,623
105+
647,635,-688
106+
-868,-804,481
107+
614,-800,639
108+
595,780,-596
109+
110+
--- scanner 4 ---
111+
727,592,562
112+
-293,-554,779
113+
441,611,-461
114+
-714,465,-776
115+
-743,427,-804
116+
-660,-479,-426
117+
832,-632,460
118+
927,-485,-438
119+
408,393,-506
120+
466,436,-512
121+
110,16,151
122+
-258,-428,682
123+
-393,719,612
124+
-211,-452,876
125+
808,-476,-593
126+
-575,615,604
127+
-485,667,467
128+
-680,325,-822
129+
-627,-443,-432
130+
872,-547,-609
131+
833,512,582
132+
807,604,487
133+
839,-516,451
134+
891,-625,532
135+
-652,-548,-490
136+
30,-46,-14

0 commit comments

Comments
 (0)