Skip to content

Commit 38c63ca

Browse files
committed
Day 23 in Raku and Go (more than an order of magnitude faster)
1 parent 4de119f commit 38c63ca

File tree

6 files changed

+724
-0
lines changed

6 files changed

+724
-0
lines changed

2021/day23/day23.go

+396
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,396 @@
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

Comments
 (0)