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...
This commit is contained in:
2022-06-18 01:18:30 -05:00
parent 8f0dc931c1
commit 267146ed8e
3 changed files with 225 additions and 217 deletions

View File

@ -1,17 +1,18 @@
package days package days
import ( import (
"container/heap"
"fmt" "fmt"
"math" "math"
"sort"
"strings" "strings"
"github.com/beefsack/go-astar" "github.com/edwingeng/deque/v2"
u "parnic.com/aoc2019/utilities" u "parnic.com/aoc2019/utilities"
) )
type day18Cell int type day18Cell int
type day18Vec u.Vec2[int] type day18Vec u.Vec2[int]
type day18Graph map[rune][]u.Pair[rune, int]
const ( const (
day18CellWall day18Cell = iota 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 { type Day18 struct {
entrance day18Vec entrance day18Vec
grid [][]day18Cell grid [][]day18Cell
doors map[day18Vec]int doors map[day18Vec]int
keys 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}))
} }
func (d *Day18) Parse() { func (d *Day18) Parse() {
d.doors = make(map[day18Vec]int) d.doors = make(map[day18Vec]int)
d.keys = 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)) d.grid = make([][]day18Cell, len(lines))
for i, line := range lines { for i, line := range lines {
d.grid[i] = make([]day18Cell, len(line)) d.grid[i] = make([]day18Cell, len(line))
@ -109,8 +60,6 @@ func (d *Day18) Parse() {
} }
} }
} }
pathGrid = d.grid
} }
func (d Day18) Num() int { 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) { func (d Day18) findAdjacentCells(inPos day18Vec, keys, doors map[day18Vec]int, grid [][]day18Cell) []u.Pair[rune, int] {
key, exists := keys[atPosition] found := make([]u.Pair[rune, int], 0)
if !exists {
panic("tried to pick up a key that either doesn't exist or was already picked up") 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)
} }
var matchingDoorPos day18Vec return retAdjacent
foundDoor := false }
for doorPos, doorType := range doors {
if doorType == key { queue := deque.NewDeque[u.Pair[int, day18Vec]]()
matchingDoorPos = doorPos visited := []day18Vec{inPos}
foundDoor = true 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 break
} }
} }
if !alreadyFound {
delete(keys, atPosition) found = append(found, u.Pair[rune, int]{First: rVal, Second: next.First})
// the last key has no door, so we should only fail this once. should. continue
if foundDoor {
delete(doors, matchingDoorPos)
} }
} }
func astarPathContains(arr []astar.Pather, find astar.Pather) bool { for _, neighbor := range getAdjacent(next.Second) {
for _, v := range arr { if !u.ArrayContains(visited, neighbor) {
if v == find { queue.PushBack(u.Pair[int, day18Vec]{First: next.First + 1, Second: neighbor})
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 found
return ret
} }
filtered := make(map[day18Vec]day18KeySearchResult) type day18PriorityQueue struct {
// filter out keys that require you to walk through another key to get to them distance int
for keyPos, key := range ret { neighbor rune
eligible := true }
for checkKeyPos := range ret { type IntHeap []day18PriorityQueue
if keyPos == checkKeyPos {
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 *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))
}
func (h *IntHeap) Pop() any {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
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
}
}
ret := make([]u.Pair[rune, int], 0)
distance := make(map[rune]int)
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<<int(node.neighbor-'a')&keysFound) == 0 {
ret = append(ret, u.Pair[rune, int]{First: node.neighbor, Second: node.distance})
continue continue
} }
if astarPathContains(key.path, checkKeyPos) { // it's a door but we don't have the key yet...
eligible = false if node.neighbor >= 'A' && node.neighbor <= 'Z' && ((1<<int(node.neighbor-'A'))&keysFound) == 0 {
break continue
}
for _, p := range graph[node.neighbor] {
newDistance := node.distance + p.Second
if dist, exists := distance[p.First]; !exists || newDistance < dist {
distance[p.First] = newDistance
heap.Push(&ih, day18PriorityQueue{
distance: newDistance,
neighbor: p.First,
})
}
} }
} }
if eligible { memo := reachableKeysMemo{
filtered[keyPos] = key pos: inPos,
keysFound: keysFound,
reachableKeys: ret,
}
knownReachableKeys = append(knownReachableKeys, memo)
return memo.reachableKeys
}
type minStepsMemo struct {
pos string
keysToFind int
keysFound int
dist int
}
var knownMinimumSteps = make([]minStepsMemo, 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
} }
} }
return filtered if keysToFind == 0 {
} return 0
func (d Day18) tryPickupKey(inKeys, inDoors map[day18Vec]int, keyPos day18Vec, keyResult day18KeySearchResult) (int, bool) {
keys := u.CopyMap(inKeys)
doors := u.CopyMap(inDoors)
d.pickUpKey(keyPos, keys, doors)
totalDist := int(keyResult.distance)
if len(keys) == 0 {
return totalDist, true
} }
best := math.Inf(1)
for _, item := range inPos {
for _, p := range d.reachableKeys(item, keysFound, graph) {
sb := strings.Builder{} sb := strings.Builder{}
keyVals := u.MapValues(keys) oldIdx := strings.IndexRune(inPos, item)
sort.Slice(keyVals, func(i, j int) bool { return keyVals[i] < keyVals[j] }) for i := range inPos {
for i, v := range keyVals { if i == oldIdx {
if i > 0 { sb.WriteRune(p.First)
sb.WriteRune(',') } else {
sb.WriteByte(inPos[i])
}
}
newKeys := keysFound + (1 << (p.First - 'a'))
dist := p.Second
dist += d.minimumSteps(sb.String(), keysToFind-1, newKeys, graph)
if float64(dist) < best {
best = float64(dist)
} }
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
} }
shortestDist := math.MaxInt memo := minStepsMemo{
reachableKeys := d.findReachableKeys(keyPos, keys, doors) pos: inPos,
if len(reachableKeys) == 0 { keysToFind: keysToFind,
// panic("no reachable keys") keysFound: keysFound,
return math.MaxInt, false dist: int(best),
} }
// fmt.Println("in.", len(reachableKeys), "reachable keys") knownMinimumSteps = append(knownMinimumSteps, memo)
done := false return memo.dist
for reachableKeyPos, keyResult := range reachableKeys {
bestDist, finished := d.tryPickupKey(keys, doors, reachableKeyPos, keyResult)
if bestDist < shortestDist {
shortestDist = bestDist
done = finished
} }
}
// fmt.Println("out")
d.knownPaths[vecToKey] = u.Pair[int, bool]{First: shortestDist, Second: done}
totalDist += shortestDist func (d Day18) buildGraph(pos []day18Vec, keys map[day18Vec]int, doors map[day18Vec]int, grid [][]day18Cell) day18Graph {
graph := make(day18Graph)
for i, p := range pos {
adjacent := d.findAdjacentCells(p, keys, doors, grid)
graph[rune('1'+i)] = adjacent
}
for keyPos, keyType := range keys {
graph[rune('a'+keyType)] = d.findAdjacentCells(keyPos, keys, doors, grid)
}
for doorPos, doorType := range doors {
graph[rune('A'+doorType)] = d.findAdjacentCells(doorPos, keys, doors, grid)
}
return totalDist, done return graph
} }
func (d Day18) part2PatchMap(grid [][]day18Cell, entrance day18Vec) []day18Vec { func (d Day18) part2PatchMap(grid [][]day18Cell, entrance day18Vec) []day18Vec {
@ -280,70 +327,31 @@ func (d Day18) part2PatchMap(grid [][]day18Cell, entrance day18Vec) []day18Vec {
} }
func (d *Day18) Part1() string { func (d *Day18) Part1() string {
pos := d.entrance // fmt.Println("initial state:")
keys := u.CopyMap(d.keys) // d.Draw(d.grid, d.keys, d.doors, d.entrance)
doors := u.CopyMap(d.doors)
fmt.Println("initial state:") graph := d.buildGraph([]day18Vec{d.entrance}, d.keys, d.doors, d.grid)
d.Draw(d.grid, keys, doors, pos) minSteps := d.minimumSteps("1", len(d.keys), 0, graph)
bestDist := math.MaxInt return fmt.Sprintf("Total distance traveled: %s%d%s", u.TextBold, minSteps, u.TextReset)
reachableKeys := d.findReachableKeys(pos, keys, doors)
if len(reachableKeys) == 0 {
panic("failed to find a key to pick up")
}
for keyPos, keyResult := range reachableKeys {
shortestPath, _ := d.tryPickupKey(keys, doors, keyPos, keyResult)
if shortestPath < bestDist {
bestDist = shortestPath
}
}
// fmt.Printf("Moving to pick up key %c (cost: %d, total cost so far: %d)\n", rune(closestKey+'a'), closestDist, totalDist)
// d.Draw(pos, keys, doors)
return fmt.Sprintf("Total distance traveled: %s%d%s", u.TextBold, bestDist, u.TextReset)
} }
func (d *Day18) Part2() string { func (d *Day18) Part2() string {
keys := u.CopyMap(d.keys) // fmt.Println("initial state:")
doors := u.CopyMap(d.doors)
d.knownPaths = make(map[u.Pair[day18Vec, string]]u.Pair[int, bool])
fmt.Println("initial state:")
grid := make([][]day18Cell, len(d.grid)) grid := make([][]day18Cell, len(d.grid))
for i := range d.grid { for i := range d.grid {
grid[i] = make([]day18Cell, len(d.grid[i])) grid[i] = make([]day18Cell, len(d.grid[i]))
copy(grid[i], d.grid[i]) copy(grid[i], d.grid[i])
} }
pathGrid = grid
entrances := d.part2PatchMap(grid, d.entrance) entrances := d.part2PatchMap(grid, d.entrance)
d.Draw(grid, keys, doors, entrances...) // d.Draw(grid, d.keys, d.doors, entrances...)
bestDist := math.MaxInt knownMinimumSteps = make([]minStepsMemo, 0)
knownReachableKeys = make([]reachableKeysMemo, 0)
// reachableKeys := d.findReachableKeys(entrances[0], keys, doors) graph := d.buildGraph(entrances, d.keys, d.doors, grid)
// if len(reachableKeys) == 0 { minSteps := d.minimumSteps("1234", len(d.keys), 0, graph)
// panic("failed to find a key to pick up")
// }
done := true
for !done {
for _, entrance := range entrances {
reachableKeys := d.findReachableKeys(entrance, keys, doors)
for keyPos, keyResult := range reachableKeys {
shortestPath, finished := d.tryPickupKey(keys, doors, keyPos, keyResult)
if shortestPath < bestDist {
bestDist = shortestPath
done = finished
}
}
}
}
// fmt.Printf("Moving to pick up key %c (cost: %d, total cost so far: %d)\n", rune(closestKey+'a'), closestDist, totalDist) return fmt.Sprintf("Total distance traveled: %s%d%s", u.TextBold, minSteps, u.TextReset)
// d.Draw(pos, keys, doors)
return fmt.Sprintf("Total distance traveled: %s%d%s", u.TextBold, bestDist, u.TextReset)
} }

2
go.mod
View File

@ -2,4 +2,4 @@ module parnic.com/aoc2019
go 1.18 go 1.18
require github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482 require github.com/edwingeng/deque/v2 v2.0.1

4
go.sum
View File

@ -1,2 +1,2 @@
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482 h1:p4g4uok3+r6Tg6fxXEQUAcMAX/WdK6WhkQW9s0jaT7k= github.com/edwingeng/deque/v2 v2.0.1 h1:yNEsA9tUImO0vyw2hmVGiK4nnkoxBQ8stMYpdVq2ZmQ=
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482/go.mod h1:Cu3t5VeqE8kXjUBeNXWQprfuaP5UCIc5ggGjgMx9KFc= github.com/edwingeng/deque/v2 v2.0.1/go.mod h1:HukI8CQe9KDmZCcURPZRYVYjH79Zy2tIjTF9sN3Bgb0=