https://adventofcode.com/2021/day/19
Like a variety of my work on previous days, my code here is rather bulky. Did I mention that Advent of Code was something of a bad habit for me? Anyways, here, not only is the code bulky, even the sample data is bulky. Basically, for this puzzle, we were given a bunch of coordinates beacons reported by a small set of scanners. These coordinates contained three "flaws": (1) They used a coordinate system which was centered on the scanner -- not a universal coordinate system. (2) Each scanner had limited range, so no single scan had locations for all beacons. (3) Each scanner had an undetermined orientation. Luckily for us, each probe "originally" had the same coordinate system, and each probe's rotations were multiples of 90 degrees along each axis. So there were only 24 possible orientations for each scanner. And, sample data looked like this: sample=:{{)n --- scanner 0 --- 404,-588,-901 528,-643,409 -838,591,734 390,-675,-793 -537,-823,-458 -485,-357,347 -345,-311,381 -661,-816,-575 -876,649,763 -618,-824,-621 553,345,-567 474,580,667 -447,-329,318 -584,868,-557 544,-627,-890 564,392,-477 455,729,728 -892,524,684 -689,845,-530 423,-701,434 7,-33,-71 630,319,-379 443,580,662 -789,900,-551 459,-707,401 --- scanner 1 --- 686,422,578 605,423,415 515,917,-361 -336,658,858 95,138,22 -476,619,847 -340,-569,-846 567,-361,727 -460,603,-452 669,-402,600 729,430,532 -500,-761,534 -322,571,750 -466,-666,-811 -429,-592,574 -355,545,-477 703,-491,-529 -328,-685,520 413,935,-424 -391,539,-444 586,-435,557 -364,-763,-893 807,-499,-711 755,-354,-619 553,889,-390 --- scanner 2 --- 649,640,665 682,-795,504 -784,533,-524 -644,584,-595 -588,-843,648 -30,6,44 -674,560,763 500,723,-460 609,671,-379 -555,-800,653 -675,-892,-343 697,-426,-610 578,704,681 493,664,-388 -671,-858,530 -667,343,800 571,-461,-707 -138,-166,112 -889,563,-600 646,-828,498 640,759,510 -630,509,768 -681,-892,-333 673,-379,-804 -742,-814,-386 577,-820,562 --- scanner 3 --- -589,542,597 605,-692,669 -500,565,-823 -660,373,557 -458,-679,-417 -488,449,543 -626,468,-788 338,-750,-386 528,-832,-391 562,-778,733 -938,-730,414 543,643,-506 -524,371,-870 407,773,750 -104,29,83 378,-903,-323 -778,-728,485 426,699,580 -438,-605,-362 -469,-447,-387 509,732,623 647,635,-688 -868,-804,481 614,-800,639 595,780,-596 --- scanner 4 --- 727,592,562 -293,-554,779 441,611,-461 -714,465,-776 -743,427,-804 -660,-479,-426 832,-632,460 927,-485,-438 408,393,-506 466,436,-512 110,16,151 -258,-428,682 -393,719,612 -211,-452,876 808,-476,-593 -575,615,604 -485,667,467 -680,325,-822 -627,-443,-432 872,-547,-609 833,512,582 807,604,487 839,-516,451 891,-625,532 -652,-548,-490 30,-46,-14 }} There will be a quiz later. (Ok, maybe not...) So, first thing we need here is a mechanism to parse this into something J can handle. And, it's not obvious here, but the number of beacons reported by each scanner was not the same. This means we need to put the scans in boxes so they are not padded. I decided I also wanted my parsing routine (which I named 'get') to be idempotent. This would let me use parsed or unparsed data on the command line -- if it was parsed already, 'get' would leave it alone. get=: {{ if. -.'literal'-:datatype y do. y return. end. s=. getscan;._2 rplc&(',';' ';(LF,'---');LF,'!---') y,'!' {:"1 s assert. (i.#s) -: ;{."1 s }} digits=: ".@([ -. -.)&(~.":i.10) getscan=: {{ (id=. digits 0{::lines); coords=. __&".@>@}. lines=. (<;._2 y)-.a: }} I could have made get and getscan a little simpler, by ignoring the reported scan number. But I was working on this puzzle late at night and it was easy for me to make mistakes. The next problem was to piece scans together to form the complete list of beacons (using the coordinate system of the scanner #0). There's a variety of approaches which could have been used here. Mine was this: (a) I would measure the distance between each pair of *beacons* reported by each scanner. The distance between any beacon pair was guaranteed to be fixed for every scanner which included both of those beacons. (b) For each pair of *scanners*, I would find and count the beacon distance lists which they had in common. Scanners which were closer together should have more beacons in common with each other than scanners which were farther apart. Looking at the data, I saw that most scanners had at least one other scanner which reported 67 or 68 of the same beacon distances, the next most frequent commonality was 17 or 18 of the same beacon distances. So, in code, I only concerned myself with scanners which had at least half as many beacons in common as the largest reported commonality. (c) For these pairs, I would find the orientation of the second based on the orientation of the first, using the beacons which they had in common (the ones whose distances were the in each of the pair of scans). In particular, I decided that I could ignore two problems: (*) I was not concerned with "spoofed distances". I had enough information that I believed that coincidental distance matches would not be a problem for me. (*) I was not concerned with "spoofed orientations". I had enough information that I believed that there would be one and only one orientation that best fit the beacons for the scanner pairs I was considering. That left me with the problem of representing intermediate results. The first step -- getting distance pairs between beacons reported by a single scanner -- was straightforward: signature=:{{+/&.:*: |:-"1/~ y}} usigs=: ~.@,@signature each scans rough=: +/@(e.,)&>/~ usigs The next step - identifying pairs of scanners which were close to each other - had one minor trick: each scanner was very, very close to itself. And I needed to ignore that. In other words, I multiplied by the logical negation of the identity matrix rough=: (* -.@=@i.@#)+/@(e.,)&>/~ usigs Actually, though, I did not need the counts of close beacons -- I just wanted to identify the scanners which had a lot of beacons in common rough=: (> -:@(>./)@,) (* -.@=@i.@#)+/@(e.,)&>/~ usigs Next, I wanted to visit those pairs in sequence, starting from scanner zero. There's a variety of ways of doing this, but one way is to use rough as a connection matrix. Then (+./ .*.)&rough given a selection mask would lead us to the connected scanners. These connections are bidirectional, and we want to know how soon we can visit a scanner. We can use ^: to iterate some number of times and then </\ to find the first 1 in each column. My approach here included a lot of extra blank rows, which I suppressed using ~. ~.</\(+./ .*)&rough^:(i.#rough) (#rough){.1 Finally, I can't sort on a list of bitmasks, but I can convert them to an ordered list of indices that represent the order I want to visit the scanners. order=: ;<@I. ~.</\(+./ .*)&rough^:(i.#rough) (#rough){.1 I'm not quite ready to compare scanner pairs yet, though -- I need to extract the "close scanner pair" list from rough, and sort them: close=: (/: order&i.) ($#:I.@,)rough But that approach actually lists each scanner pair twice (0 7 and 7 0), and that's not necessary. So, I could do: close=: ~. /:~"1 (/: order&i.) ($#:I.@,)rough or close=: (/: order&i.) ($#:I.@,) (* </~@i.@#)rough With that setup, I am *almost* ready to walk through my scanner pairs. But I still need to represent their orientation and offset. Each scanner has both an offset from the global origin (scanner 0), and an orientation with respect to scanner 0. The offset is easy to represent -- that's just an x,y,z value. The orientation, though, ... that I represented as a permutation of the axis along with a sign (positive or negative) for each axis. reorient=:{{ 's a'=.8 6#:{.x off=. }.4{.x sign=. _1^2 2 2#:s off+"1 sign*"1 (a A. i.3) {"1 y }} Here, x is the orientation and offset for the scanner, and y is the list of beacon coordinates to be manipulated. This is not an optimal representation. The problem with this approach is that it can represent both right handed and left handed coordinate systems, and I only wanted coordinate systems which could result from a rotation of the origin coordinate system. I did not know whether that origin system was right handed or left handed (since I didn't have any external reference data), all I cared was that my alternate orientations matched. To solve this I did a dry run, using an identity matrix in place of beacon coordinates, and 0 offsets. If a cross product of the first two result coordinates matched the third, I had a viable orientation: plausible=: I.({:-:cross/@{.~&2)@reorient&(=i.3)"1 ,.i.48 Still with me? I got to this point mostly by trial and error, with lots of asserts in my code to make me aware of invalid assumptions. I have since deleted most of those mistakes and the assertions which made me aware of those problems. And, I've cleaned up some of my code a bit from my initial puzzle solution, so that it would be readable. But I am not sure how well I am explaining my resultant understanding... The next stage of puzzle solving was, for each pair of scans: (1) find the beacon coordinate pairs in each which share a distance between the scanner pair. (2) Try each of the plausible orientations (with zero offsets), subtracting the selected parts of the scanner set. Since the beacon coordinate pairs are ordered by distance between the pair, they are ordered the same for each scanner set. And, when I get the orientation right, distance between pairs from the two sets will mostly be the same distance apart (this will be the distance between the two scanners). My implementation is still a bit sloppy here, but it gets the job done: whichori=:{{ d=. #@~.@:(-&x)@reorient&y"1,.plausible plausible{~d i.<./d }} relori=: {{ 'c0 c1'=. y sigs=. signature@>y k=. +/4=#/.~,sigs ref=. k{.}. (~. \: #/.~),sigs ids=. (* k&>) ref i. sigs pairs0=. pairs1=. i.0 0 for_id. 1+i.k-1 do. j=. (}.$sigs)#:I.,id=ids assert.4=#j pairs0=. pairs0,(2{.j) { c0 pairs1=. pairs1,(2}.j) { c1 end. o=. pairs0 whichori pairs1 candidates=./:~(#/.~,.~.),/pairs0 -"1 o reorient pairs1 assert. ~:_2{.{."1 candidates off=.}.{:candidates o,off }} Note that when I wrote this code I was not completely sure that I would not have "spoofed distances". So I am not actually completely ignoring that problem -- the assert. 4=#j in the middle of relori would fail if I somehow had the same distance between more than a pair of beacons. (It's 4, rather than 2, because each pairing is represented twice -- I found the distance between each pair of beacons and beaconA is the same distance from beaconB that beaconB is from beaconA.) It would be fun working out how to express this more concisely, but I haven't thought enough about that issue. And, that's all the pieces I needed for part A, which was to count the total number of scanned beacons: a19=:{{ scans=: get y heritage=:(,;0);(<:#scans)#<i.0 0 usigs=: ~.@,@signature each scans rough=: (> -:@(>./)@,) (* -.@=@i.@#)+/@(e.,)&>/~ usigs order=: ;<@I. ~.</\(+./ .*)&rough^:(i.#rough) (#rough){.1 close=: ~. /:~"1 (/: order&i.) ($#:I.@,)rough known=:,:0 0 0 0 0 for_scanpair.close do. 'kid jid'=. scanpair /: (0,,scanpair_index{.close) i. scanpair kori=. }.(({."1 known) i.kid){known known=: known,jid,t=.relori (kori reorient kid{::scans); jid{::scans end. #~.>,each/(}."1/:~~.known) reorient L:0"_1 scans }} I've left a variety of global assignments here, because looking at example data really helps when understanding the code. Also For part B, the problem was to find the largest "manhattan distance" between two scanners. In other words, given the location of each scanner 2}."1~.known take the sum of the absolute values of the difference between each pair of locations, then find the largest of those sums b19=:{{ a19 y >./,+/"1|-"1/~2}."1~.known }} If there's something here which I should have explained better, and you can think of a way to phrase a question about it, let me know... Thanks, -- Raul ---------------------------------------------------------------------- For information about J forums see http://www.jsoftware.com/forums.htm