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
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<<int(node.neighbor-'a')&keysFound) == 0 {
ret = append(ret, u.Pair[rune, int]{First: node.neighbor, Second: node.distance})
continue
}
// it's a door but we don't have the key yet...
if node.neighbor >= 'A' && node.neighbor <= 'Z' && ((1<<int(node.neighbor-'A'))&keysFound) == 0 {
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,
})
}
}
}
memo := reachableKeysMemo{
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
}
}
if keysToFind == 0 {
return 0
}
best := math.Inf(1)
for _, item := range inPos {
for _, p := range d.reachableKeys(item, keysFound, graph) {
sb := strings.Builder{}
oldIdx := strings.IndexRune(inPos, item)
for i := range inPos {
if i == oldIdx {
sb.WriteRune(p.First)
} 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)
}
}
}
memo := minStepsMemo{
pos: inPos,
keysToFind: keysToFind,
keysFound: keysFound,
dist: int(best),
}
knownMinimumSteps = append(knownMinimumSteps, memo)
return memo.dist
}
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 graph
}
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 {
pos := d.entrance
keys := u.CopyMap(d.keys)
doors := u.CopyMap(d.doors)
// fmt.Println("initial state:")
// d.Draw(d.grid, d.keys, d.doors, d.entrance)
fmt.Println("initial state:")
d.Draw(d.grid, keys, doors, pos)
graph := d.buildGraph([]day18Vec{d.entrance}, d.keys, d.doors, d.grid)
minSteps := d.minimumSteps("1", len(d.keys), 0, graph)
bestDist := math.MaxInt
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)
return fmt.Sprintf("Total distance traveled: %s%d%s", u.TextBold, minSteps, u.TextReset)
}
func (d *Day18) Part2() string {
keys := u.CopyMap(d.keys)
doors := u.CopyMap(d.doors)
d.knownPaths = make(map[u.Pair[day18Vec, string]]u.Pair[int, bool])
fmt.Println("initial state:")
// fmt.Println("initial state:")
grid := make([][]day18Cell, len(d.grid))
for i := range d.grid {
grid[i] = make([]day18Cell, len(d.grid[i]))
copy(grid[i], d.grid[i])
}
pathGrid = grid
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)
// if len(reachableKeys) == 0 {
// 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
}
}
}
}
graph := d.buildGraph(entrances, d.keys, d.doors, grid)
minSteps := d.minimumSteps("1234", len(d.keys), 0, graph)
// 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)
return fmt.Sprintf("Total distance traveled: %s%d%s", u.TextBold, minSteps, u.TextReset)
}

2
go.mod
View File

@ -2,4 +2,4 @@ module parnic.com/aoc2019
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/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482/go.mod h1:Cu3t5VeqE8kXjUBeNXWQprfuaP5UCIc5ggGjgMx9KFc=
github.com/edwingeng/deque/v2 v2.0.1 h1:yNEsA9tUImO0vyw2hmVGiK4nnkoxBQ8stMYpdVq2ZmQ=
github.com/edwingeng/deque/v2 v2.0.1/go.mod h1:HukI8CQe9KDmZCcURPZRYVYjH79Zy2tIjTF9sN3Bgb0=