From 267146ed8eaebab58d993f7ff9b2835dbc5c718f Mon Sep 17 00:00:00 2001 From: Parnic Date: Sat, 18 Jun 2022 01:18:30 -0500 Subject: [PATCH] Day 18 part 2 solution (rewrite) This solves both parts much faster than before, but still on the order of 3-20 seconds, so more improvements (or another rewrite?) are needed. But we're getting there... --- days/18.go | 436 +++++++++++++++++++++++++++-------------------------- go.mod | 2 +- go.sum | 4 +- 3 files changed, 225 insertions(+), 217 deletions(-) diff --git a/days/18.go b/days/18.go index 4f09032..a905cd8 100644 --- a/days/18.go +++ b/days/18.go @@ -1,17 +1,18 @@ package days import ( + "container/heap" "fmt" "math" - "sort" "strings" - "github.com/beefsack/go-astar" + "github.com/edwingeng/deque/v2" 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 @@ -27,68 +28,18 @@ var ( } ) -// variables needed for pathfinding -// a better solution would be to keep the current path search state in its -// own struct and store these on there, but i don't need to worry about -// thread safety right now, so this will do fine -var ( - pathGrid [][]day18Cell - pathDoors map[day18Vec]int -) - -type day18KeySearchResult struct { - keyType int - path []astar.Pather - distance float64 - found bool -} - type Day18 struct { - entrance day18Vec - grid [][]day18Cell - doors map[day18Vec]int - keys map[day18Vec]int - knownPaths map[u.Pair[day18Vec, string]]u.Pair[int, bool] -} - -func (v day18Vec) PathNeighbors() []astar.Pather { - ret := make([]astar.Pather, 0) - - for _, offset := range day18AdjacentOffsets { - vOffset := day18Vec{X: v.X + offset.X, Y: v.Y + offset.Y} - if vOffset.X < 0 || vOffset.Y < 0 || vOffset.Y >= len(pathGrid) || vOffset.X >= len(pathGrid[0]) { - continue - } - - if _, exists := pathDoors[vOffset]; exists { - continue - } - - if pathGrid[vOffset.Y][vOffset.X] == day18CellOpen { - ret = append(ret, vOffset) - } - } - - return ret -} - -func (v day18Vec) PathNeighborCost(to astar.Pather) float64 { - return 1 -} - -func (v day18Vec) PathEstimatedCost(to astar.Pather) float64 { - // this is insanely more complicated than i feel like it should be. - // i guess generics aren't quite as flexible as i'd hoped. Go doesn't - // seem to know that a day18Vec is a vec2i which is a vec2[int]. - return float64(u.ManhattanDistance(u.Vec2[int]{X: v.X, Y: v.Y}, u.Vec2[int]{X: to.(day18Vec).X, Y: to.(day18Vec).Y})) + entrance day18Vec + grid [][]day18Cell + doors map[day18Vec]int + keys map[day18Vec]int } func (d *Day18) Parse() { d.doors = make(map[day18Vec]int) d.keys = make(map[day18Vec]int) - d.knownPaths = make(map[u.Pair[day18Vec, string]]u.Pair[int, bool]) - lines := u.GetStringLines("18s6") + lines := u.GetStringLines("18p") d.grid = make([][]day18Cell, len(lines)) for i, line := range lines { d.grid[i] = make([]day18Cell, len(line)) @@ -109,8 +60,6 @@ func (d *Day18) Parse() { } } } - - pathGrid = d.grid } func (d Day18) Num() int { @@ -140,128 +89,226 @@ func (d Day18) Draw(grid [][]day18Cell, keys, doors map[day18Vec]int, entrances } } -func (d *Day18) pickUpKey(atPosition day18Vec, keys, doors map[day18Vec]int) { - key, exists := keys[atPosition] - if !exists { - panic("tried to pick up a key that either doesn't exist or was already picked up") - } +func (d Day18) findAdjacentCells(inPos day18Vec, keys, doors map[day18Vec]int, grid [][]day18Cell) []u.Pair[rune, int] { + found := make([]u.Pair[rune, int], 0) - var matchingDoorPos day18Vec - foundDoor := false - for doorPos, doorType := range doors { - if doorType == key { - matchingDoorPos = doorPos - foundDoor = true - break - } - } - - delete(keys, atPosition) - // the last key has no door, so we should only fail this once. should. - if foundDoor { - delete(doors, matchingDoorPos) - } -} - -func astarPathContains(arr []astar.Pather, find astar.Pather) bool { - for _, v := range arr { - if v == find { - return true - } - } - - return false -} - -func (d Day18) findReachableKeys(pos day18Vec, keys, doors map[day18Vec]int) map[day18Vec]day18KeySearchResult { - pathDoors = doors - - ret := make(map[day18Vec]day18KeySearchResult) - for keyPos, keyType := range keys { - path, distance, found := astar.Path(pos, keyPos) - if found { - ret[keyPos] = day18KeySearchResult{ - keyType: keyType, - path: path, - distance: distance, - found: found, - } - } - } - - if len(ret) < 2 { - return ret - } - - filtered := make(map[day18Vec]day18KeySearchResult) - // filter out keys that require you to walk through another key to get to them - for keyPos, key := range ret { - eligible := true - for checkKeyPos := range ret { - if keyPos == checkKeyPos { + 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) + } - if astarPathContains(key.path, checkKeyPos) { - eligible = false - break + return retAdjacent + } + + queue := deque.NewDeque[u.Pair[int, day18Vec]]() + visited := []day18Vec{inPos} + for _, adjacent := range getAdjacent(inPos) { + queue.PushBack(u.Pair[int, day18Vec]{First: 1, Second: adjacent}) + } + + for !queue.IsEmpty() { + next := queue.PopFront() + + if !u.ArrayContains(visited, next.Second) { + visited = append(visited, next.Second) + + 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 !u.ArrayContains(visited, neighbor) { + queue.PushBack(u.Pair[int, day18Vec]{First: next.First + 1, Second: neighbor}) + } } } - - if eligible { - filtered[keyPos] = key - } } - return filtered + return found } -func (d Day18) tryPickupKey(inKeys, inDoors map[day18Vec]int, keyPos day18Vec, keyResult day18KeySearchResult) (int, bool) { - keys := u.CopyMap(inKeys) - doors := u.CopyMap(inDoors) +type day18PriorityQueue struct { + distance int + neighbor rune +} +type IntHeap []day18PriorityQueue - d.pickUpKey(keyPos, keys, doors) - totalDist := int(keyResult.distance) +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] } - if len(keys) == 0 { - return totalDist, true - } +func (h *IntHeap) Push(x any) { + // Push and Pop use pointer receivers because they modify the slice's length, + // not just its contents. + *h = append(*h, x.(day18PriorityQueue)) +} - sb := strings.Builder{} - keyVals := u.MapValues(keys) - sort.Slice(keyVals, func(i, j int) bool { return keyVals[i] < keyVals[j] }) - for i, v := range keyVals { - if i > 0 { - sb.WriteRune(',') - } - sb.WriteString(fmt.Sprintf("%d", v)) - } - vecToKey := u.Pair[day18Vec, string]{First: keyPos, Second: sb.String()} - if v, exists := d.knownPaths[vecToKey]; exists { - return totalDist + v.First, v.Second - } +func (h *IntHeap) Pop() any { + old := *h + n := len(old) + x := old[n-1] + *h = old[0 : n-1] + return x +} - shortestDist := math.MaxInt - reachableKeys := d.findReachableKeys(keyPos, keys, doors) - if len(reachableKeys) == 0 { - // panic("no reachable keys") - return math.MaxInt, false - } - // fmt.Println("in.", len(reachableKeys), "reachable keys") - done := false - for reachableKeyPos, keyResult := range reachableKeys { - bestDist, finished := d.tryPickupKey(keys, doors, reachableKeyPos, keyResult) - if bestDist < shortestDist { - shortestDist = bestDist - done = finished +type reachableKeysMemo struct { + pos rune + keysFound int + reachableKeys []u.Pair[rune, int] +} + +var knownReachableKeys = make([]reachableKeysMemo, 0) + +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 } } - // fmt.Println("out") - d.knownPaths[vecToKey] = u.Pair[int, bool]{First: shortestDist, Second: done} - totalDist += shortestDist + ret := make([]u.Pair[rune, int], 0) + distance := make(map[rune]int) - return totalDist, done + ih := make(IntHeap, 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<