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:
436
days/18.go
436
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<<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
2
go.mod
@ -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
4
go.sum
@ -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=
|
||||
|
Reference in New Issue
Block a user