300 lines
7.8 KiB
Go
300 lines
7.8 KiB
Go
package day17
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"math"
|
|
"os"
|
|
"slices"
|
|
"strings"
|
|
// "time"
|
|
)
|
|
|
|
const ExampleResult string = ">>v>>>^>>>vv>>vv>vvv>vvv<vv>"
|
|
|
|
func Run() int {
|
|
fmt.Println("hello from day 17")
|
|
filename := "day17/example"
|
|
field := NewField(filename)
|
|
fmt.Printf("%+v\n", field)
|
|
|
|
startSegment := PathSegmentEnd{
|
|
endsAt: Coord{0, 0},
|
|
totalLength: 0,
|
|
lastSteps: make(map[Direction]int),
|
|
done: true,
|
|
lastDirection: Downward, // fake, caution
|
|
}
|
|
|
|
end := Coord{field.Height - 1, field.Width - 1}
|
|
field.runSearch(startSegment, end)
|
|
|
|
pathsFoundToEnd := field.Paths[end]
|
|
fmt.Println("check visually:")
|
|
// fmt.Println(field.Paths[end].stringPathSoFar)
|
|
minimal := math.MaxInt
|
|
for _, path := range pathsFoundToEnd {
|
|
fmt.Printf("%+v ; len is %d\n", path, path.totalLength)
|
|
if path.totalLength < minimal {
|
|
minimal = path.totalLength
|
|
}
|
|
}
|
|
// fmt.Printf("i'm looking for %s\n", ExampleResult)
|
|
return minimal
|
|
}
|
|
|
|
// let's do dijkstra. it also needs a priority queue
|
|
|
|
// priority queue would be over vertice. and would have to have enough information to
|
|
// calc the distance from neighbors.
|
|
// how to check condition of max 3 in one row?
|
|
// with each vertice store [horizontal:n|vertical:n] and if it's 3 just dont consider?
|
|
|
|
// so in iteration, i have some vertice, with horizontal:2 for example,
|
|
// i check all neighbors, if path through 'this' is shorter, set that as path,
|
|
// but also mark the path with len of straight.
|
|
//
|
|
// so priority queue is with 'path to next'
|
|
// or rather 'path to i,j'
|
|
// then check for neighbors (non finished), calc distance to them through this
|
|
// checking neighbors via 'path get directions' 'path get geighbors from directions'
|
|
// if shorter - update
|
|
// mark current as 'finished'
|
|
// so, i'll be checking cost to enter directly from this table,
|
|
// but check path len
|
|
|
|
func ReadEnterCosts(filename string) [][]int {
|
|
bytes, err := os.ReadFile(filename)
|
|
if err != nil {
|
|
panic(fmt.Sprint("error reading file ", filename))
|
|
}
|
|
text := strings.TrimSpace(string(bytes))
|
|
result := make([][]int, 0)
|
|
for _, line := range strings.Split(text, "\n") {
|
|
numbers := make([]int, 0)
|
|
for _, digit := range line {
|
|
num := int(digit - '0')
|
|
numbers = append(numbers, num)
|
|
}
|
|
result = append(result, numbers)
|
|
}
|
|
return result
|
|
}
|
|
|
|
type Coord struct {
|
|
Row, Col int
|
|
}
|
|
|
|
func (c Coord) applyDirection(d Direction) (result Coord) {
|
|
result = c
|
|
switch d {
|
|
case Upward:
|
|
result.Row -= 1
|
|
case Downward:
|
|
result.Row += 1
|
|
case Leftward:
|
|
result.Col -= 1
|
|
case Rightward:
|
|
result.Col += 1
|
|
}
|
|
return
|
|
}
|
|
|
|
type Direction int
|
|
|
|
const (
|
|
Upward Direction = iota
|
|
Downward
|
|
Leftward
|
|
Rightward
|
|
)
|
|
|
|
func (d Direction) String() string {
|
|
strings := []string{"Up", "Down", "Left", "Right"}
|
|
return strings[d]
|
|
}
|
|
|
|
func (d Direction) AsSymbol() string {
|
|
strings := []string{"^", "v", "<", ">"}
|
|
return strings[d]
|
|
}
|
|
|
|
func (d Direction) GetPerpendicular() (directions []Direction) {
|
|
switch d {
|
|
case Upward:
|
|
directions = []Direction{Leftward, Rightward}
|
|
case Downward:
|
|
directions = []Direction{Leftward, Rightward}
|
|
case Leftward:
|
|
directions = []Direction{Upward, Downward}
|
|
case Rightward:
|
|
directions = []Direction{Upward, Downward}
|
|
}
|
|
return
|
|
}
|
|
|
|
type PathSegmentEnd struct {
|
|
endsAt Coord
|
|
totalLength int
|
|
lastSteps map[Direction]int
|
|
lastDirection Direction
|
|
stringPathSoFar string
|
|
done bool
|
|
}
|
|
|
|
func (p *PathSegmentEnd) IsExamplePathPrefix() bool {
|
|
return strings.HasPrefix(ExampleResult, p.stringPathSoFar)
|
|
}
|
|
|
|
func (p *PathSegmentEnd) NextDirections() (next []Direction) {
|
|
next = append(next, p.lastDirection.GetPerpendicular()...)
|
|
|
|
// last steps of 2 is max allowed 3 tiles in row
|
|
lastSteps := p.lastSteps[p.lastDirection]
|
|
if lastSteps < 3 {
|
|
next = append(next, p.lastDirection)
|
|
}
|
|
|
|
// if p.IsExamplePathPrefix() {
|
|
// log.Printf("getting directions from %+v they are %+v", p, next)
|
|
// }
|
|
// log.Printf("getting directions from %+v they are %+v", p, next)
|
|
return
|
|
}
|
|
|
|
func (p *PathSegmentEnd) isDominating(other PathSegmentEnd) bool {
|
|
if p.endsAt != other.endsAt {
|
|
panic(fmt.Sprintf("comparing domination of paths on different ells: %+v %+v",
|
|
p, other))
|
|
}
|
|
var thisDirection Direction
|
|
var thisSteps int
|
|
for thisDirection, thisSteps = range p.lastSteps {
|
|
break
|
|
}
|
|
|
|
var otherDirection Direction
|
|
var otherSteps int
|
|
for otherDirection, otherSteps = range other.lastSteps {
|
|
break
|
|
}
|
|
|
|
sameDirection := thisDirection == otherDirection
|
|
// if other has less steps, then current total length is not important
|
|
// other can still be more efficient in the future
|
|
lessOrSameLastSteps := thisSteps <= otherSteps
|
|
|
|
return sameDirection && lessOrSameLastSteps && p.totalLength < other.totalLength
|
|
}
|
|
|
|
type Field struct {
|
|
Paths map[Coord][]*PathSegmentEnd
|
|
Costs [][]int
|
|
Height, Width int
|
|
Start Coord
|
|
}
|
|
|
|
func NewField(filename string) Field {
|
|
enterCosts := ReadEnterCosts(filename)
|
|
startSegment := PathSegmentEnd{
|
|
endsAt: Coord{0, 0},
|
|
totalLength: 0,
|
|
lastSteps: make(map[Direction]int),
|
|
done: true,
|
|
lastDirection: Downward, // fake, caution
|
|
}
|
|
initialPaths := make(map[Coord][]*PathSegmentEnd)
|
|
initialPaths[Coord{0, 0}] = []*PathSegmentEnd{&startSegment}
|
|
|
|
return Field{
|
|
Paths: initialPaths,
|
|
Costs: enterCosts,
|
|
Height: len(enterCosts),
|
|
Width: len(enterCosts[0]),
|
|
Start: Coord{0, 0},
|
|
}
|
|
}
|
|
|
|
func (f *Field) isValid(c Coord) bool {
|
|
return c.Col >= 0 && c.Row >= 0 && c.Row < f.Height && c.Col < f.Width
|
|
}
|
|
|
|
// presupposes that direction is valid
|
|
func (f *Field) continuePathInDirection(curPath PathSegmentEnd, d Direction, finish Coord) (result PathSegmentEnd) {
|
|
nextCoord := curPath.endsAt.applyDirection(d)
|
|
moveCost := f.Costs[nextCoord.Row][nextCoord.Col]
|
|
newCost := curPath.totalLength + moveCost
|
|
lastSteps := make(map[Direction]int)
|
|
|
|
curPathStepsIntoThisDirection, found := curPath.lastSteps[d]
|
|
if !found {
|
|
lastSteps[d] = 1
|
|
} else {
|
|
lastSteps[d] = curPathStepsIntoThisDirection + 1
|
|
}
|
|
|
|
return PathSegmentEnd{
|
|
endsAt: nextCoord,
|
|
totalLength: newCost,
|
|
lastDirection: d,
|
|
lastSteps: lastSteps,
|
|
stringPathSoFar: curPath.stringPathSoFar + d.AsSymbol(),
|
|
done: nextCoord == finish,
|
|
}
|
|
}
|
|
|
|
func (f *Field) runSearch(curPath PathSegmentEnd, finish Coord) {
|
|
|
|
// if len(curPath.stringPathSoFar) > f.Height+f.Width {
|
|
// if curPath.IsExamplePathPrefix() {
|
|
// log.Printf(">> CUTOFF %+v\n", curPath)
|
|
// }
|
|
// return
|
|
// }
|
|
// if start is also finish : this path is done
|
|
// record the length, for the coord?
|
|
// get directions,
|
|
// if no directions : this path is done
|
|
// get neighbords,
|
|
// for each neighbor calc what whould be path if we went to the neighbor
|
|
// if neighbor already known path which DOMINATES current - do not continue
|
|
//
|
|
// in the end for each vertice there will be map[direction][]PathSegmentEnd
|
|
current := curPath.endsAt
|
|
if current == finish {
|
|
if curPath.IsExamplePathPrefix() {
|
|
log.Printf(">> reached end with %+v\n", curPath)
|
|
}
|
|
return
|
|
}
|
|
directions := curPath.NextDirections()
|
|
|
|
if len(directions) == 0 {
|
|
return
|
|
}
|
|
|
|
checkingNeighbors:
|
|
for _, d := range directions {
|
|
nextCoord := curPath.endsAt.applyDirection(d)
|
|
if !f.isValid(nextCoord) {
|
|
continue
|
|
}
|
|
ponentialPath := f.continuePathInDirection(curPath, d, finish)
|
|
knownPathsToNeighbor := f.Paths[ponentialPath.endsAt]
|
|
for _, knownPath := range knownPathsToNeighbor {
|
|
if knownPath.isDominating(ponentialPath) {
|
|
continue checkingNeighbors
|
|
}
|
|
// if our potential path is not dominated, then save as potential path
|
|
// and would be nice to remove all known paths which are dominated by this potential
|
|
}
|
|
filteredKnownPaths := slices.DeleteFunc(knownPathsToNeighbor, func(previous *PathSegmentEnd) bool {
|
|
return ponentialPath.isDominating(*previous)
|
|
})
|
|
filteredKnownPaths = append(filteredKnownPaths, &ponentialPath)
|
|
f.Paths[ponentialPath.endsAt] = filteredKnownPaths
|
|
|
|
f.runSearch(ponentialPath, finish)
|
|
}
|
|
}
|