|
| 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/23 |
| 8 | +package main |
| 9 | + |
| 10 | +/* day23 computes the cost to move amphipods from four rooms into their proper |
| 11 | +rooms; moves are blocked if there's an amphipod in the way. Amphipods have |
| 12 | +letter-based kinds. Each space move costs A=1, B=10, C=100, D=1000. |
| 13 | +Go implementation because I had trouble finding a bug in my Raku solution, |
| 14 | +and each run took tens of minutes. Board structure is hard-coded for |
| 15 | +example and actual input because I didn't want to focus on parsing; this |
| 16 | +turned out to be convenient since part 2 added new input. */ |
| 17 | + |
| 18 | +import ( |
| 19 | + "flag" |
| 20 | + "fmt" |
| 21 | + "log" |
| 22 | + "os" |
| 23 | + "strings" |
| 24 | + "time" |
| 25 | +) |
| 26 | + |
| 27 | +var printWinner = flag.Bool("print-winner", false, "Show winning moves") |
| 28 | + |
| 29 | +func absInt(a int) int { |
| 30 | + if a >= 0 { |
| 31 | + return a |
| 32 | + } |
| 33 | + return -1 * a |
| 34 | +} |
| 35 | + |
| 36 | +type Position struct{ hall, room, slot int } |
| 37 | + |
| 38 | +func (p Position) X() int { |
| 39 | + if p.hall > 0 { |
| 40 | + return p.hall |
| 41 | + } |
| 42 | + return p.room |
| 43 | +} |
| 44 | + |
| 45 | +func (p Position) dist(o Position) int { |
| 46 | + if p.hall > 0 && p.room > 0 { |
| 47 | + log.Fatalf("Invalid position: %v", p) |
| 48 | + } |
| 49 | + if o.hall > 0 && o.room > 0 { |
| 50 | + log.Fatalf("Invalid position: %v", o) |
| 51 | + } |
| 52 | + if p.hall > 0 && o.room > 0 { |
| 53 | + return absInt(p.hall-o.room) + o.slot |
| 54 | + } |
| 55 | + if o.hall > 0 && p.room > 0 { |
| 56 | + return absInt(o.hall-p.room) + p.slot |
| 57 | + } |
| 58 | + if p.hall > 0 && o.hall > 0 { // not actually a legal move |
| 59 | + return absInt(p.hall - o.hall) |
| 60 | + } |
| 61 | + return absInt(p.room-o.room) + p.slot + o.slot |
| 62 | +} |
| 63 | + |
| 64 | +func (p Position) String() string { |
| 65 | + if p.hall > 0 && p.room > 0 { |
| 66 | + log.Fatalf("Invalid position hall %d room %d slot %d", p.hall, p.room, p.slot) |
| 67 | + } |
| 68 | + if p.hall > 0 { |
| 69 | + return fmt.Sprintf("{hall: %d}", p.hall) |
| 70 | + } |
| 71 | + return fmt.Sprintf("{room: %d,slot: %d}", p.room, p.slot) |
| 72 | +} |
| 73 | + |
| 74 | +var validHall = []Position{ |
| 75 | + Position{hall: 1}, Position{hall: 2}, Position{hall: 4}, Position{hall: 6}, |
| 76 | + Position{hall: 8}, Position{hall: 10}, Position{hall: 11}, |
| 77 | +} |
| 78 | + |
| 79 | +type Amphipod struct { |
| 80 | + kind byte |
| 81 | + pos Position |
| 82 | + target, cost int |
| 83 | +} |
| 84 | + |
| 85 | +func newAmphipod(kind byte, pos Position) Amphipod { |
| 86 | + a := Amphipod{kind: kind, pos: pos} |
| 87 | + switch kind { |
| 88 | + case 'A': |
| 89 | + a.target = 3 |
| 90 | + a.cost = 1 |
| 91 | + case 'B': |
| 92 | + a.target = 5 |
| 93 | + a.cost = 10 |
| 94 | + case 'C': |
| 95 | + a.target = 7 |
| 96 | + a.cost = 100 |
| 97 | + case 'D': |
| 98 | + a.target = 9 |
| 99 | + a.cost = 1000 |
| 100 | + } |
| 101 | + return a |
| 102 | +} |
| 103 | + |
| 104 | +func (a Amphipod) String() string { |
| 105 | + return fmt.Sprintf("Amphipod{kind: %q, pos: %s, target: %d, cost: %d}", a.kind, a.pos, a.target, a.cost) |
| 106 | +} |
| 107 | + |
| 108 | +type BoardKey string |
| 109 | +type Board struct { |
| 110 | + pods []Amphipod |
| 111 | + depth, cost int |
| 112 | +} |
| 113 | + |
| 114 | +func newBoard(cost int, depth int, pods ...Amphipod) *Board { |
| 115 | + aps := make([]Amphipod, len(pods)) |
| 116 | + copy(aps, pods) |
| 117 | + return &Board{pods: aps, depth: depth, cost: cost} |
| 118 | +} |
| 119 | + |
| 120 | +func (b *Board) key() BoardKey { |
| 121 | + parts := make([]string, len(b.pods)) |
| 122 | + for i, a := range b.pods { |
| 123 | + parts[i] = a.pos.String() |
| 124 | + } |
| 125 | + return BoardKey(strings.Join(parts, ";")) |
| 126 | +} |
| 127 | + |
| 128 | +func (b *Board) validMoves() []*Board { |
| 129 | + res := make([]*Board, 0, 8) |
| 130 | + for i, a := range b.pods { |
| 131 | + if a.pos.room != a.target { |
| 132 | + for j := b.depth; j > 0; j-- { |
| 133 | + p := Position{room: a.target, slot: j} |
| 134 | + if b.validMove(a, p) { |
| 135 | + res = append(res, b.move(i, p)) |
| 136 | + break |
| 137 | + } |
| 138 | + } |
| 139 | + } |
| 140 | + if a.pos.room > 0 && (a.pos.room != a.target || !b.roomSatisfied(a.target)) { |
| 141 | + for _, p := range validHall { |
| 142 | + if b.validMove(a, p) { |
| 143 | + res = append(res, b.move(i, p)) |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + } |
| 148 | + return res |
| 149 | +} |
| 150 | + |
| 151 | +func (b *Board) validMove(a Amphipod, p Position) bool { |
| 152 | + ap := a.pos |
| 153 | + if p == ap { |
| 154 | + return false |
| 155 | + } |
| 156 | + if p.hall > 0 && ap.hall > 0 { |
| 157 | + return false |
| 158 | + } |
| 159 | + if p.room > 0 && a.target != p.room { |
| 160 | + return false |
| 161 | + } |
| 162 | + if p.hall == 3 || p.hall == 5 || p.hall == 7 || p.hall == 9 { |
| 163 | + return false // can't stop outside a room |
| 164 | + } |
| 165 | + sawSlot := make([]bool, b.depth+1) |
| 166 | + for _, o := range b.pods { |
| 167 | + if o == a { |
| 168 | + continue |
| 169 | + } |
| 170 | + op := o.pos |
| 171 | + if op == p { |
| 172 | + return false |
| 173 | + } |
| 174 | + if op.hall > 0 { |
| 175 | + if ap.X() < p.X() && ap.X() <= op.X() && op.X() <= p.X() { |
| 176 | + return false |
| 177 | + } |
| 178 | + if ap.X() > p.X() && ap.X() >= op.X() && op.X() >= p.X() { |
| 179 | + return false |
| 180 | + } |
| 181 | + } |
| 182 | + if op.room > 0 && op.room == p.room { |
| 183 | + if o.kind != a.kind { |
| 184 | + return false |
| 185 | + } |
| 186 | + if op.slot < p.slot { |
| 187 | + return false |
| 188 | + } |
| 189 | + sawSlot[op.slot] = true |
| 190 | + } |
| 191 | + if op.room > 0 && op.room == ap.room && op.slot < ap.slot { |
| 192 | + return false |
| 193 | + } |
| 194 | + } |
| 195 | + if p.slot > 0 { |
| 196 | + for i := p.slot + 1; i <= b.depth; i++ { |
| 197 | + if !sawSlot[i] { |
| 198 | + return false |
| 199 | + } |
| 200 | + } |
| 201 | + } |
| 202 | + return true |
| 203 | +} |
| 204 | + |
| 205 | +func (b *Board) move(i int, p Position) *Board { |
| 206 | + a := b.pods[i] |
| 207 | + anew := a |
| 208 | + anew.pos = p |
| 209 | + bnew := newBoard(a.cost*a.pos.dist(p)+b.cost, b.depth, b.pods...) |
| 210 | + bnew.pods[i] = anew |
| 211 | + return bnew |
| 212 | +} |
| 213 | + |
| 214 | +func (b *Board) satisfied() bool { |
| 215 | + for _, a := range b.pods { |
| 216 | + if a.pos.room != a.target { |
| 217 | + return false |
| 218 | + } |
| 219 | + } |
| 220 | + return true |
| 221 | +} |
| 222 | + |
| 223 | +func (b *Board) roomSatisfied(room int) bool { |
| 224 | + for _, a := range b.pods { |
| 225 | + if a.target == room && a.pos.room != a.target { |
| 226 | + return false |
| 227 | + } |
| 228 | + } |
| 229 | + return true |
| 230 | +} |
| 231 | + |
| 232 | +func (b *Board) minRemainingCost() int { |
| 233 | + res := 0 |
| 234 | + targets := make(map[int][]Amphipod) |
| 235 | + for _, a := range b.pods { |
| 236 | + if _, ok := targets[a.target]; !ok { |
| 237 | + targets[a.target] = make([]Amphipod, 0, 4) |
| 238 | + } |
| 239 | + targets[a.target] = append(targets[a.target], a) |
| 240 | + } |
| 241 | + for t, pods := range targets { |
| 242 | + d := b.depth |
| 243 | + for i := b.depth; i > 0; i-- { |
| 244 | + for _, a := range pods { |
| 245 | + if a.pos.room == t && a.pos.slot == i { |
| 246 | + d-- |
| 247 | + break |
| 248 | + } |
| 249 | + } |
| 250 | + } |
| 251 | + if d == 0 { |
| 252 | + continue // room fully satisfied |
| 253 | + } |
| 254 | + dc := d |
| 255 | + for _, a := range pods { |
| 256 | + if a.pos.room != t || a.pos.slot < d { |
| 257 | + res += a.cost * (absInt(a.pos.X()-t) + dc) |
| 258 | + dc-- |
| 259 | + } |
| 260 | + } |
| 261 | + } |
| 262 | + return res |
| 263 | +} |
| 264 | + |
| 265 | +func (b *Board) String() string { |
| 266 | + lines := make([]string, b.depth+3) |
| 267 | + lines[0] = "#############" |
| 268 | + lines[b.depth+2] = " ######### " |
| 269 | + hall := []byte{'#', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '.', '#'} |
| 270 | + rooms := make([][]byte, b.depth) |
| 271 | + for i := range rooms { |
| 272 | + rooms[i] = []byte{' ', ' ', '#', '.', '#', '.', '#', '.', '#', '.', '#', ' ', ' '} |
| 273 | + } |
| 274 | + rooms[0] = []byte{'#', '#', '#', '.', '#', '.', '#', '.', '#', '.', '#', '#', '#'} |
| 275 | + for _, p := range b.pods { |
| 276 | + if p.pos.hall > 0 { |
| 277 | + hall[p.pos.hall] = p.kind |
| 278 | + } |
| 279 | + if p.pos.room > 0 { |
| 280 | + rooms[p.pos.slot-1][p.pos.room] = p.kind |
| 281 | + } |
| 282 | + } |
| 283 | + lines[1] = string(hall) |
| 284 | + for i, r := range rooms { |
| 285 | + lines[2+i] = string(r) |
| 286 | + } |
| 287 | + return strings.Join(lines, "\n") |
| 288 | +} |
| 289 | + |
| 290 | +func solve(initial *Board) int { |
| 291 | + seen := make(map[BoardKey]int) |
| 292 | + seen[initial.key()] = 0 |
| 293 | + parent := make(map[*Board]*Board) |
| 294 | + q := make(map[int][]*Board) |
| 295 | + q[0] = []*Board{initial} |
| 296 | + var pri, seenSkipped int |
| 297 | + for { |
| 298 | + for q[pri] == nil || len(q[pri]) == 0 { |
| 299 | + pri++ |
| 300 | + if pri > 1000000 { |
| 301 | + log.Fatalf("Somehow got to cost %d, seen %d boards", pri, len(seen)) |
| 302 | + } |
| 303 | + } |
| 304 | + for i := 0; i < len(q[pri]); i++ { |
| 305 | + b := q[pri][i] |
| 306 | + if b.satisfied() { |
| 307 | + log.Printf("Found winner at cost %d with %d seen skipped:\n", pri, seenSkipped) |
| 308 | + if *printWinner { |
| 309 | + for x := b; x != nil; x = parent[x] { |
| 310 | + log.Printf("Cost %d\n%s\n", x.cost, x) |
| 311 | + } |
| 312 | + } |
| 313 | + return b.cost |
| 314 | + } |
| 315 | + for _, m := range b.validMoves() { |
| 316 | + key := m.key() |
| 317 | + rem := m.minRemainingCost() |
| 318 | + if prev, ok := seen[key]; ok && prev <= m.cost+rem { |
| 319 | + seenSkipped++ |
| 320 | + continue |
| 321 | + } |
| 322 | + seen[key] = m.cost + rem |
| 323 | + parent[m] = b |
| 324 | + c := m.cost + rem |
| 325 | + if q[c] == nil { |
| 326 | + q[c] = make([]*Board, 0) |
| 327 | + } |
| 328 | + q[c] = append(q[c], m) |
| 329 | + } |
| 330 | + } |
| 331 | + delete(q, pri) |
| 332 | + } |
| 333 | + log.Fatalf("Somehow ran out of boards at cost %d, seen %d boards", pri, len(seen)) |
| 334 | + return 0 |
| 335 | +} |
| 336 | + |
| 337 | +func runPart(part int, initial *Board, expected int, name string) bool { |
| 338 | + fmt.Printf("Running part %d on %s expecting %d\n", part, name, expected) |
| 339 | + start := time.Now() |
| 340 | + res := solve(initial) |
| 341 | + dur := time.Since(start) |
| 342 | + m := "❌" |
| 343 | + if res == expected { |
| 344 | + m = "✓" |
| 345 | + } |
| 346 | + fmt.Printf("%s Part %d on %s got %d in %s\n", m, part, name, res, dur) |
| 347 | + if expected != 0 && res != expected { |
| 348 | + fmt.Printf("❌ got %d but want %d\n\n", res, expected) |
| 349 | + return false |
| 350 | + } |
| 351 | + fmt.Println() |
| 352 | + return true |
| 353 | +} |
| 354 | + |
| 355 | +var part1Example = newBoard(0, 2, |
| 356 | + newAmphipod('B', Position{room: 3, slot: 1}), newAmphipod('A', Position{room: 3, slot: 2}), |
| 357 | + newAmphipod('C', Position{room: 5, slot: 1}), newAmphipod('D', Position{room: 5, slot: 2}), |
| 358 | + newAmphipod('B', Position{room: 7, slot: 1}), newAmphipod('C', Position{room: 7, slot: 2}), |
| 359 | + newAmphipod('D', Position{room: 9, slot: 1}), newAmphipod('A', Position{room: 9, slot: 2}), |
| 360 | +) |
| 361 | + |
| 362 | +var part1Actual = newBoard(0, 2, |
| 363 | + newAmphipod('C', Position{room: 3, slot: 1}), newAmphipod('B', Position{room: 3, slot: 2}), |
| 364 | + newAmphipod('B', Position{room: 5, slot: 1}), newAmphipod('D', Position{room: 5, slot: 2}), |
| 365 | + newAmphipod('D', Position{room: 7, slot: 1}), newAmphipod('A', Position{room: 7, slot: 2}), |
| 366 | + newAmphipod('A', Position{room: 9, slot: 1}), newAmphipod('C', Position{room: 9, slot: 2}), |
| 367 | +) |
| 368 | + |
| 369 | +var part2Example = newBoard(0, 4, |
| 370 | + newAmphipod('B', Position{room: 3, slot: 1}), newAmphipod('D', Position{room: 3, slot: 2}), newAmphipod('D', Position{room: 3, slot: 3}), newAmphipod('A', Position{room: 3, slot: 4}), |
| 371 | + newAmphipod('C', Position{room: 5, slot: 1}), newAmphipod('C', Position{room: 5, slot: 2}), newAmphipod('B', Position{room: 5, slot: 3}), newAmphipod('D', Position{room: 5, slot: 4}), |
| 372 | + newAmphipod('B', Position{room: 7, slot: 1}), newAmphipod('B', Position{room: 7, slot: 2}), newAmphipod('A', Position{room: 7, slot: 3}), newAmphipod('C', Position{room: 7, slot: 4}), |
| 373 | + newAmphipod('D', Position{room: 9, slot: 1}), newAmphipod('A', Position{room: 9, slot: 2}), newAmphipod('C', Position{room: 9, slot: 3}), newAmphipod('A', Position{room: 9, slot: 4}), |
| 374 | +) |
| 375 | + |
| 376 | +var part2Actual = newBoard(0, 4, |
| 377 | + newAmphipod('C', Position{room: 3, slot: 1}), newAmphipod('D', Position{room: 3, slot: 2}), newAmphipod('D', Position{room: 3, slot: 3}), newAmphipod('B', Position{room: 3, slot: 4}), |
| 378 | + newAmphipod('B', Position{room: 5, slot: 1}), newAmphipod('C', Position{room: 5, slot: 2}), newAmphipod('B', Position{room: 5, slot: 3}), newAmphipod('D', Position{room: 5, slot: 4}), |
| 379 | + newAmphipod('D', Position{room: 7, slot: 1}), newAmphipod('B', Position{room: 7, slot: 2}), newAmphipod('A', Position{room: 7, slot: 3}), newAmphipod('A', Position{room: 7, slot: 4}), |
| 380 | + newAmphipod('A', Position{room: 9, slot: 1}), newAmphipod('A', Position{room: 9, slot: 2}), newAmphipod('C', Position{room: 9, slot: 3}), newAmphipod('C', Position{room: 9, slot: 4}), |
| 381 | +) |
| 382 | + |
| 383 | +func main() { |
| 384 | + var success [4]bool |
| 385 | + success[0] = runPart(1, part1Example, 12521, "input.example.txt") |
| 386 | + success[1] = runPart(1, part1Actual, 13520, "input.actual.txt") |
| 387 | + success[2] = runPart(2, part2Example, 44169, "input.example.txt") |
| 388 | + success[3] = runPart(2, part2Actual, 48708, "input.actual.txt") |
| 389 | + fmt.Printf("Success? %v\n", success) |
| 390 | + for _, s := range success { |
| 391 | + if !s { |
| 392 | + os.Exit(1) |
| 393 | + } |
| 394 | + } |
| 395 | + os.Exit(0) |
| 396 | +} |
0 commit comments