From 68ca26e3aa8f50374cfeef84c962292bb0fb165e Mon Sep 17 00:00:00 2001 From: Parnic Date: Sat, 18 Jun 2022 22:47:30 -0500 Subject: [PATCH] Optimizations and minor cleanups Thanks to Go's profiling tools, I discovered that the memoization, while it was cutting down runtime significantly, was itself slow because it was using arrays. Swapping those arrays out for maps made a _massive_ difference (4s/14s part1/part2 to 1ms/2ms with no other changes). Lesson learned. Again. The IntHeap rename was long overdue since I took the code originally from Go's sample docs for priority queues. --- days/18.go | 82 +++++++++++++++++++++++------------------------------- 1 file changed, 35 insertions(+), 47 deletions(-) diff --git a/days/18.go b/days/18.go index a905cd8..8a33adc 100644 --- a/days/18.go +++ b/days/18.go @@ -106,7 +106,7 @@ func (d Day18) findAdjacentCells(inPos day18Vec, keys, doors map[day18Vec]int, g } queue := deque.NewDeque[u.Pair[int, day18Vec]]() - visited := []day18Vec{inPos} + visited := make(map[day18Vec]bool) for _, adjacent := range getAdjacent(inPos) { queue.PushBack(u.Pair[int, day18Vec]{First: 1, Second: adjacent}) } @@ -114,8 +114,8 @@ func (d Day18) findAdjacentCells(inPos day18Vec, keys, doors map[day18Vec]int, g for !queue.IsEmpty() { next := queue.PopFront() - if !u.ArrayContains(visited, next.Second) { - visited = append(visited, next.Second) + if _, exists := visited[next.Second]; !exists { + visited[next.Second] = true key, adjacentIsKey := keys[next.Second] door, adjacentIsDoor := doors[next.Second] @@ -141,7 +141,7 @@ func (d Day18) findAdjacentCells(inPos day18Vec, keys, doors map[day18Vec]int, g } for _, neighbor := range getAdjacent(next.Second) { - if !u.ArrayContains(visited, neighbor) { + if _, exists := visited[neighbor]; !exists { queue.PushBack(u.Pair[int, day18Vec]{First: next.First + 1, Second: neighbor}) } } @@ -155,19 +155,17 @@ type day18PriorityQueue struct { distance int neighbor rune } -type IntHeap []day18PriorityQueue +type PriorityQueueHeap []day18PriorityQueue -func (h IntHeap) Len() int { return len(h) } -func (h IntHeap) Less(i, j int) bool { return h[i].distance < h[j].distance } -func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } +func (h PriorityQueueHeap) Len() int { return len(h) } +func (h PriorityQueueHeap) Less(i, j int) bool { return h[i].distance < h[j].distance } +func (h PriorityQueueHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } -func (h *IntHeap) Push(x any) { - // Push and Pop use pointer receivers because they modify the slice's length, - // not just its contents. +func (h *PriorityQueueHeap) Push(x any) { *h = append(*h, x.(day18PriorityQueue)) } -func (h *IntHeap) Pop() any { +func (h *PriorityQueueHeap) Pop() any { old := *h n := len(old) x := old[n-1] @@ -176,25 +174,25 @@ func (h *IntHeap) Pop() any { } type reachableKeysMemo struct { - pos rune - keysFound int - reachableKeys []u.Pair[rune, int] + pos rune + keysFound int } -var knownReachableKeys = make([]reachableKeysMemo, 0) +var knownReachableKeys = make(map[reachableKeysMemo][]u.Pair[rune, int]) func (d Day18) reachableKeys(inPos rune, keysFound int, graph day18Graph) []u.Pair[rune, int] { - for _, m := range knownReachableKeys { - if m.pos == inPos && - m.keysFound == keysFound { - return m.reachableKeys - } + memo := reachableKeysMemo{ + pos: inPos, + keysFound: keysFound, + } + if v, exists := knownReachableKeys[memo]; exists { + return v } ret := make([]u.Pair[rune, int], 0) distance := make(map[rune]int) - ih := make(IntHeap, 0) + ih := make(PriorityQueueHeap, 0) for _, p := range graph[inPos] { ih = append(ih, day18PriorityQueue{ @@ -231,31 +229,26 @@ func (d Day18) reachableKeys(inPos rune, keysFound int, graph day18Graph) []u.Pa } } - memo := reachableKeysMemo{ - pos: inPos, - keysFound: keysFound, - reachableKeys: ret, - } - knownReachableKeys = append(knownReachableKeys, memo) - return memo.reachableKeys + knownReachableKeys[memo] = ret + return ret } type minStepsMemo struct { pos string keysToFind int keysFound int - dist int } -var knownMinimumSteps = make([]minStepsMemo, 0) +var knownMinimumSteps = make(map[minStepsMemo]int, 0) func (d Day18) minimumSteps(inPos string, keysToFind int, keysFound int, graph day18Graph) int { - for _, m := range knownMinimumSteps { - if m.pos == inPos && - m.keysToFind == keysToFind && - m.keysFound == keysFound { - return m.dist - } + memo := minStepsMemo{ + pos: inPos, + keysToFind: keysToFind, + keysFound: keysFound, + } + if v, exists := knownMinimumSteps[memo]; exists { + return v } if keysToFind == 0 { @@ -285,14 +278,8 @@ func (d Day18) minimumSteps(inPos string, keysToFind int, keysFound int, graph d } } - memo := minStepsMemo{ - pos: inPos, - keysToFind: keysToFind, - keysFound: keysFound, - dist: int(best), - } - knownMinimumSteps = append(knownMinimumSteps, memo) - return memo.dist + knownMinimumSteps[memo] = int(best) + return int(best) } func (d Day18) buildGraph(pos []day18Vec, keys map[day18Vec]int, doors map[day18Vec]int, grid [][]day18Cell) day18Graph { @@ -347,8 +334,9 @@ func (d *Day18) Part2() string { entrances := d.part2PatchMap(grid, d.entrance) // d.Draw(grid, d.keys, d.doors, entrances...) - knownMinimumSteps = make([]minStepsMemo, 0) - knownReachableKeys = make([]reachableKeysMemo, 0) + // clear memoized maps that (might have) came from part1 + knownMinimumSteps = make(map[minStepsMemo]int) + knownReachableKeys = make(map[reachableKeysMemo][]u.Pair[rune, int]) graph := d.buildGraph(entrances, d.keys, d.doors, grid) minSteps := d.minimumSteps("1234", len(d.keys), 0, graph)