package day17 import ( "fmt" "log" "math" "os" "slices" "strings" // "time" ) const ExampleResult string = ">>v>>>^>>>vv>>vv>vvv>vvv" 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) } }