From 4db2d1140174260c6fb48a1275175d17f118f643 Mon Sep 17 00:00:00 2001 From: Parnic Date: Tue, 12 Jul 2022 08:45:47 -0500 Subject: [PATCH] Day 18 solution This one was a doozy and took far more time than I'd like to admit. I hope it's the hardest problem of the year. My original solution gave correct answers, but took on the order of 3+ minutes to run, even with memoization (it wasn't finishing any time soon without it). I then tried dropping in some A* and Dijkstra libraries, but wasn't really happy with how things were progressing with them. Some research pointed me toward double-ended queues and priority queues as better solutions, which I should have come up with my own since they've been used along with memoization in other AoC's, and dropping those in took the runtimes down to 4-15 seconds on my m1 macbook air. Once I swapped out the memoized data structures from arrays to maps, the runtime finally dropped to a much more palatable 50-180 millisecond range. I'm always suspicious when my solution is hundreds of lines of code, though, since you tend to see much more terse solutions from others in the AoC subreddit, but I attribute at least some of the bloat to "Go things" like how maps and arrays usually need to be "make"d first, how there's no easy "list comprehension" or "linq" style data structure queries, etc. Finally: note that I've seen _dramatically_ different runtimes based on the input. One set of input I checked against ran in 30ms/10ms while another ran in 180ms/54ms. I guess it's never been promised that all inputs are created equally... --- days/18.go | 345 ++++++++++++++++++++++++++++++++++++++++++++ inputs/18p.txt | 81 +++++++++++ inputs/18s1.txt | 3 + inputs/18s2.txt | 5 + inputs/18s3.txt | 5 + inputs/18s4.txt | 9 ++ inputs/18s5.txt | 6 + inputs/18s6.txt | 7 + inputs/18s7.txt | 7 + inputs/18s8.txt | 7 + inputs/18s9.txt | 9 ++ main.go | 1 + utilities/map.go | 10 ++ utilities/vector.go | 13 +- 14 files changed, 504 insertions(+), 4 deletions(-) create mode 100644 days/18.go create mode 100644 inputs/18p.txt create mode 100644 inputs/18s1.txt create mode 100644 inputs/18s2.txt create mode 100644 inputs/18s3.txt create mode 100644 inputs/18s4.txt create mode 100644 inputs/18s5.txt create mode 100644 inputs/18s6.txt create mode 100644 inputs/18s7.txt create mode 100644 inputs/18s8.txt create mode 100644 inputs/18s9.txt diff --git a/days/18.go b/days/18.go new file mode 100644 index 0000000..931d6b4 --- /dev/null +++ b/days/18.go @@ -0,0 +1,345 @@ +package days + +import ( + "container/heap" + "fmt" + "math" + "strings" + + u "parnic.com/aoc2019/utilities" +) + +type day18Cell int +type day18Vec u.Vec2[int] +type day18Graph map[rune][]u.Pair[rune, int] + +const ( + day18CellWall day18Cell = iota + day18CellOpen +) + +var ( + day18AdjacentOffsets = []day18Vec{ + {X: -1, Y: 0}, + {X: 1, Y: 0}, + {X: 0, Y: -1}, + {X: 0, Y: 1}, + } +) + +type reachableKeysMemo struct { + pos rune + keysFound int +} + +type minStepsMemo struct { + pos string + keysToFind int + keysFound int +} + +type Day18 struct { + entrance day18Vec + grid [][]day18Cell + doors map[day18Vec]int + keys map[day18Vec]int + knownReachableKeys map[reachableKeysMemo][]u.Pair[rune, int] + knownMinimumSteps map[minStepsMemo]int +} + +func (d *Day18) Parse() { + d.doors = make(map[day18Vec]int) + d.keys = make(map[day18Vec]int) + d.knownReachableKeys = make(map[reachableKeysMemo][]u.Pair[rune, int]) + d.knownMinimumSteps = make(map[minStepsMemo]int, 0) + + lines := u.GetStringLines("18p") + d.grid = make([][]day18Cell, len(lines)) + for i, line := range lines { + d.grid[i] = make([]day18Cell, len(line)) + for j, char := range line { + if char == '#' { + d.grid[i][j] = day18CellWall + } else if char == '.' { + d.grid[i][j] = day18CellOpen + } else if char == '@' { + d.grid[i][j] = day18CellOpen + d.entrance = day18Vec{X: j, Y: i} + } else if char >= 'A' && char <= 'Z' { + d.grid[i][j] = day18CellOpen + d.doors[day18Vec{X: j, Y: i}] = int(char - 'A') + } else if char >= 'a' && char <= 'z' { + d.grid[i][j] = day18CellOpen + d.keys[day18Vec{X: j, Y: i}] = int(char - 'a') + } + } + } +} + +func (d Day18) Num() int { + return 18 +} + +func (d Day18) Draw(grid [][]day18Cell, keys, doors map[day18Vec]int, entrances ...day18Vec) { + for y := range grid { + for x := range grid[y] { + switch grid[y][x] { + case day18CellWall: + fmt.Print("█") + case day18CellOpen: + posVec := day18Vec{X: x, Y: y} + if _, exists := doors[posVec]; exists { + fmt.Printf("%c", rune(doors[posVec]+'A')) + } else if _, exists := keys[posVec]; exists { + fmt.Printf("%c", rune(keys[posVec]+'a')) + } else if u.ArrayContains(entrances, posVec) { + fmt.Print("@") + } else { + fmt.Print(".") + } + } + } + fmt.Println() + } +} + +func (d Day18) findAdjacentCells(inPos day18Vec, keys, doors map[day18Vec]int, grid [][]day18Cell) []u.Pair[rune, int] { + found := make([]u.Pair[rune, int], 0) + + getAdjacent := func(pos day18Vec) []day18Vec { + retAdjacent := make([]day18Vec, 0, len(day18AdjacentOffsets)) + for _, off := range day18AdjacentOffsets { + offVec := day18Vec{X: pos.X + off.X, Y: pos.Y + off.Y} + if grid[offVec.Y][offVec.X] == day18CellWall { + continue + } + retAdjacent = append(retAdjacent, offVec) + } + + return retAdjacent + } + + queue := make([]u.Pair[int, day18Vec], 0) + visited := make(map[day18Vec]bool) + for _, adjacent := range getAdjacent(inPos) { + queue = append(queue, u.Pair[int, day18Vec]{First: 1, Second: adjacent}) + } + + for len(queue) > 0 { + next := queue[0] + queue = queue[1:] + + if _, exists := visited[next.Second]; !exists { + visited[next.Second] = true + + key, adjacentIsKey := keys[next.Second] + door, adjacentIsDoor := doors[next.Second] + if adjacentIsKey || adjacentIsDoor { + var rVal rune + if adjacentIsKey { + rVal = rune('a' + key) + } else if adjacentIsDoor { + rVal = rune('A' + door) + } + + alreadyFound := false + for _, p := range found { + if p.First == rVal { + alreadyFound = true + break + } + } + if !alreadyFound { + found = append(found, u.Pair[rune, int]{First: rVal, Second: next.First}) + continue + } + } + + for _, neighbor := range getAdjacent(next.Second) { + if _, exists := visited[neighbor]; !exists { + queue = append(queue, u.Pair[int, day18Vec]{First: next.First + 1, Second: neighbor}) + } + } + } + } + + return found +} + +type day18PriorityQueue struct { + distance int + neighbor rune +} +type day18PriorityQueueHeap []day18PriorityQueue + +func (h day18PriorityQueueHeap) Len() int { return len(h) } +func (h day18PriorityQueueHeap) Less(i, j int) bool { return h[i].distance < h[j].distance } +func (h day18PriorityQueueHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h *day18PriorityQueueHeap) Push(x any) { + *h = append(*h, x.(day18PriorityQueue)) +} + +func (h *day18PriorityQueueHeap) Pop() any { + old := *h + n := len(old) + x := old[n-1] + *h = old[0 : n-1] + return x +} + +func (d Day18) reachableKeys(inPos rune, keysFound int, graph day18Graph) []u.Pair[rune, int] { + memo := reachableKeysMemo{ + pos: inPos, + keysFound: keysFound, + } + if v, exists := d.knownReachableKeys[memo]; exists { + return v + } + + ret := make([]u.Pair[rune, int], 0) + distance := make(map[rune]int) + + ih := make(day18PriorityQueueHeap, 0) + + for _, p := range graph[inPos] { + ih = append(ih, day18PriorityQueue{ + distance: p.Second, + neighbor: p.First, + }) + } + + heap.Init(&ih) + + for ih.Len() > 0 { + node := heap.Pop(&ih).(day18PriorityQueue) + + // it's a key and we haven't picked it up yet... + if node.neighbor >= 'a' && node.neighbor <= 'z' && (1<= 'A' && node.neighbor <= 'Z' && ((1<