|
| 1 | +/* Copyright 2021 Google LLC |
| 2 | +
|
| 3 | +Use of this source code is governed by an MIT-style |
| 4 | +license that can be found in the LICENSE file or at |
| 5 | +https://opensource.org/licenses/MIT. |
| 6 | +*/ |
| 7 | +// https://adventofcode.com/2021/day/19 implementation in Go because day19.raku |
| 8 | +// was really slow and nondeterministically got the wrong answer. The Go |
| 9 | +// implementation also nondeterministically got the wrong answer because |
| 10 | +// iteration order through a map isn't consistent and I incorrectly believed |
| 11 | +// that rotation was a commutative operation. |
| 12 | +package main |
| 13 | + |
| 14 | +import ( |
| 15 | + "bufio" |
| 16 | + "fmt" |
| 17 | + "log" |
| 18 | + "os" |
| 19 | + "strings" |
| 20 | +) |
| 21 | + |
| 22 | +type Direction int |
| 23 | + |
| 24 | +const ( |
| 25 | + Deosil Direction = 1 // clockwise |
| 26 | + Widdershins Direction = -1 // not actually used, but makes math more robust |
| 27 | +) |
| 28 | + |
| 29 | +type Rotation struct{ x, y, z int } |
| 30 | + |
| 31 | +var ( |
| 32 | + face1 = Rotation{} |
| 33 | + face2 = Rotation{y: 1} |
| 34 | + face3 = Rotation{y: 2} |
| 35 | + face4 = Rotation{y: 3} |
| 36 | + face5 = Rotation{z: 1} |
| 37 | + face6 = Rotation{z: 3} |
| 38 | + allFaces = []Rotation{face1, face2, face3, face4, face5, face6} |
| 39 | + xTurns = []Rotation{Rotation{}, Rotation{x: 1}, Rotation{x: 2}, Rotation{x: 3}} |
| 40 | +) |
| 41 | + |
| 42 | +func (r Rotation) String() string { |
| 43 | + return fmt.Sprintf("Rotation{x:%d,y:%d,z:%d}", r.x, r.y, r.z) |
| 44 | +} |
| 45 | + |
| 46 | +func (r Rotation) union(o Rotation) Rotation { |
| 47 | + return Rotation{r.x + o.x, r.y + o.y, r.z + o.z} |
| 48 | +} |
| 49 | + |
| 50 | +type Point struct{ x, y, z int } |
| 51 | + |
| 52 | +func (p Point) rotate(axis string, dir Direction) Point { |
| 53 | + switch axis { |
| 54 | + case "x": |
| 55 | + return Point{x: p.x, y: p.z * int(dir), z: -p.y * int(dir)} |
| 56 | + case "y": |
| 57 | + return Point{x: -p.z * int(dir), y: p.y, z: p.x * int(dir)} |
| 58 | + case "z": |
| 59 | + return Point{x: p.y * int(dir), y: -p.x * int(dir), z: p.z} |
| 60 | + default: |
| 61 | + log.Fatalf("Unknown axis %s", axis) |
| 62 | + return p |
| 63 | + } |
| 64 | +} |
| 65 | + |
| 66 | +func (p Point) rotateMultiple(axes Rotation) Point { |
| 67 | + q := p |
| 68 | + if axes.x != 0 { |
| 69 | + dir := Deosil |
| 70 | + if axes.x < 0 { |
| 71 | + dir = Widdershins |
| 72 | + } |
| 73 | + for i := 0; i < int(dir)*axes.x; i++ { |
| 74 | + q = q.rotate("x", dir) |
| 75 | + } |
| 76 | + } |
| 77 | + if axes.y != 0 { |
| 78 | + dir := Deosil |
| 79 | + if axes.y < 0 { |
| 80 | + dir = Widdershins |
| 81 | + } |
| 82 | + for i := 0; i < int(dir)*axes.y; i++ { |
| 83 | + q = q.rotate("y", dir) |
| 84 | + } |
| 85 | + } |
| 86 | + if axes.z != 0 { |
| 87 | + dir := Deosil |
| 88 | + if axes.z < 0 { |
| 89 | + dir = Widdershins |
| 90 | + } |
| 91 | + for i := 0; i < int(dir)*axes.z; i++ { |
| 92 | + q = q.rotate("z", dir) |
| 93 | + } |
| 94 | + } |
| 95 | + return q |
| 96 | +} |
| 97 | + |
| 98 | +func (p Point) plus(o Point) Point { |
| 99 | + return Point{x: p.x + o.x, y: p.y + o.y, z: p.z + o.z} |
| 100 | +} |
| 101 | + |
| 102 | +func (p Point) minus(o Point) Point { |
| 103 | + return Point{x: p.x - o.x, y: p.y - o.y, z: p.z - o.z} |
| 104 | +} |
| 105 | + |
| 106 | +func (p Point) String() string { |
| 107 | + return fmt.Sprintf("%d,%d,%d", p.x, p.y, p.z) |
| 108 | +} |
| 109 | + |
| 110 | +type Pointset struct { |
| 111 | + points map[Point]bool |
| 112 | + origin Point |
| 113 | + rotation Rotation |
| 114 | +} |
| 115 | + |
| 116 | +func (s *Pointset) rotate(axes Rotation) *Pointset { |
| 117 | + res := make(map[Point]bool) |
| 118 | + for p := range s.points { |
| 119 | + res[p.rotateMultiple(axes)] = true |
| 120 | + } |
| 121 | + return &Pointset{points: res, origin: s.origin.rotateMultiple(axes), rotation: s.rotation.union(axes)} |
| 122 | +} |
| 123 | + |
| 124 | +func (s *Pointset) allOrientations() []*Pointset { |
| 125 | + res := make([]*Pointset, 0, 24) |
| 126 | + for _, f := range allFaces { |
| 127 | + for _, x := range xTurns { |
| 128 | + res = append(res, s.rotate(f.union(x))) |
| 129 | + } |
| 130 | + } |
| 131 | + return res |
| 132 | +} |
| 133 | + |
| 134 | +func (s *Pointset) offset(p Point) *Pointset { |
| 135 | + res := make(map[Point]bool) |
| 136 | + for q := range s.points { |
| 137 | + res[q.plus(p)] = true |
| 138 | + } |
| 139 | + return &Pointset{points: res, origin: s.origin.plus(p), rotation: s.rotation} |
| 140 | +} |
| 141 | + |
| 142 | +func (s *Pointset) overlap(o *Pointset) *Pointset { |
| 143 | + for p := range s.points { |
| 144 | + for q := range o.points { |
| 145 | + t := o.offset(p.minus(q)) |
| 146 | + matches := 0 |
| 147 | + for r := range t.points { |
| 148 | + if s.points[r] { |
| 149 | + matches++ |
| 150 | + } |
| 151 | + } |
| 152 | + if matches >= 12 { |
| 153 | + return t |
| 154 | + } |
| 155 | + } |
| 156 | + } |
| 157 | + return nil |
| 158 | +} |
| 159 | + |
| 160 | +func (s *Pointset) String() string { |
| 161 | + points := make([]string, 0, len(s.points)) |
| 162 | + for p := range s.points { |
| 163 | + points = append(points, p.String()) |
| 164 | + } |
| 165 | + return fmt.Sprintf("Pointset( %s )", strings.Join(points, " ")) |
| 166 | +} |
| 167 | + |
| 168 | +type Device struct { |
| 169 | + name string |
| 170 | + pointset *Pointset |
| 171 | + orientations []*Pointset |
| 172 | +} |
| 173 | + |
| 174 | +func (d *Device) String() string { |
| 175 | + points := make([]string, 0, len(d.pointset.points)) |
| 176 | + for p := range d.pointset.points { |
| 177 | + points = append(points, p.String()) |
| 178 | + } |
| 179 | + return fmt.Sprintf("%s\n%s", d.name, strings.Join(points, "\n")) |
| 180 | +} |
| 181 | + |
| 182 | +func align(devices []*Device) map[int]*Pointset { |
| 183 | + found := make(map[int]*Pointset) |
| 184 | + found[0] = devices[0].pointset |
| 185 | + checked := make(map[int]map[int]bool) |
| 186 | + remaining := len(devices) - 1 |
| 187 | + for remaining > 0 { |
| 188 | + thisloop := 0 |
| 189 | + eachdevice: |
| 190 | + for i := range devices { |
| 191 | + if checked[i] == nil { |
| 192 | + checked[i] = make(map[int]bool) |
| 193 | + } |
| 194 | + if found[i] != nil { |
| 195 | + continue |
| 196 | + } |
| 197 | + for j := range devices { |
| 198 | + if i == j || found[j] == nil || checked[i][j] { |
| 199 | + continue |
| 200 | + } |
| 201 | + checked[i][j] = true |
| 202 | + source := found[j] |
| 203 | + for _, orient := range devices[i].orientations { |
| 204 | + o := source.overlap(orient) |
| 205 | + if o == nil { |
| 206 | + continue |
| 207 | + } |
| 208 | + found[i] = o |
| 209 | + remaining-- |
| 210 | + thisloop++ |
| 211 | + continue eachdevice |
| 212 | + } |
| 213 | + } |
| 214 | + } |
| 215 | + if thisloop == 0 { |
| 216 | + log.Fatalf("Couldn't make any progress after %d matches\n%v", len(found), found) |
| 217 | + } |
| 218 | + } |
| 219 | + return found |
| 220 | +} |
| 221 | + |
| 222 | +// part1 counts the number of distinct points after aligning all scanner devices. |
| 223 | +func part1(devices []*Device) int { |
| 224 | + found := align(devices) |
| 225 | + all := make(map[Point]int) |
| 226 | + for _, s := range found { |
| 227 | + for p := range s.points { |
| 228 | + all[p]++ |
| 229 | + } |
| 230 | + } |
| 231 | + return len(all) |
| 232 | +} |
| 233 | + |
| 234 | +// part1 counts the maximum Manhattan distance between two scanner devices. |
| 235 | +func part2(devices []*Device) int { |
| 236 | + found := align(devices) |
| 237 | + max := 0 |
| 238 | + for _, ps1 := range found { |
| 239 | + for _, ps2 := range found { |
| 240 | + p := ps1.origin.minus(ps2.origin) |
| 241 | + max = maxInt(max, absInt(p.x)+absInt(p.y)+absInt(p.z)) |
| 242 | + } |
| 243 | + } |
| 244 | + return max |
| 245 | +} |
| 246 | + |
| 247 | +func absInt(a int) int { |
| 248 | + if a >= 0 { |
| 249 | + return a |
| 250 | + } |
| 251 | + return -1 * a |
| 252 | +} |
| 253 | +func maxInt(a, b int) int { |
| 254 | + if b > a { |
| 255 | + return b |
| 256 | + } |
| 257 | + return a |
| 258 | +} |
| 259 | + |
| 260 | +func runFile(fname string) { |
| 261 | + f, err := os.Open(fname) |
| 262 | + if err != nil { |
| 263 | + log.Fatalf("Error opening %s: %v", fname, err) |
| 264 | + } |
| 265 | + defer f.Close() |
| 266 | + devices := make([]*Device, 0) |
| 267 | + var dev *Device |
| 268 | + scanner := bufio.NewScanner(f) |
| 269 | + for scanner.Scan() { |
| 270 | + line := scanner.Text() |
| 271 | + if strings.Contains(line, "scanner") { |
| 272 | + dev = &Device{name: line, pointset: &Pointset{points: make(map[Point]bool)}} |
| 273 | + devices = append(devices, dev) |
| 274 | + } else if strings.ContainsRune(line, ',') { |
| 275 | + var x, y, z int |
| 276 | + if _, err := fmt.Sscanf(line, "%d,%d,%d", &x, &y, &z); err != nil { |
| 277 | + log.Fatalf("Could not parse %s", line) |
| 278 | + continue |
| 279 | + } |
| 280 | + dev.pointset.points[Point{x: x, y: y, z: z}] = true |
| 281 | + } |
| 282 | + } |
| 283 | + for _, d := range devices { |
| 284 | + d.orientations = d.pointset.allOrientations() |
| 285 | + } |
| 286 | + fmt.Printf("Running Part 1 on %s\n", fname) |
| 287 | + res1 := part1(devices) |
| 288 | + log.Printf("Part 1 got %d\n", res1) |
| 289 | + fmt.Printf("Running Part 2 on %s\n", fname) |
| 290 | + res2 := part2(devices) |
| 291 | + log.Printf("Part 2 got %d\n", res2) |
| 292 | +} |
| 293 | + |
| 294 | +func main() { |
| 295 | + for _, fname := range os.Args[1:] { |
| 296 | + log.Printf("Running day19 on %s\n", fname) |
| 297 | + runFile(fname) |
| 298 | + } |
| 299 | +} |
0 commit comments