package day21 import ( "fmt" "log" "os" "strings" ) func Run() (result int) { fmt.Print("hello day21") filename := "day21/input" field := ReadField(filename) log.Print(field) initialSaturatedFields := make(map[Coord]any) log.Print(initialSaturatedFields) // for i := 6; i <= 10; i++ { // reachableBySteps := field.ReachableBySteps(i, map[Coord]any{ // Coord{Row: field.RowStart, Col: field.ColStart}: struct{}{}, // }) // log.Print("reachable after steps : ", i, len(reachableBySteps)) // field.PrintCoord(reachableBySteps, 1) // } steps := 26501365 reachableBySteps := field.ReachableBySteps( steps, map[FieldPoint]any{ FieldPoint{ InField: Coord{Row: field.RowStart, Col: field.ColStart}, }: struct{}{}}, make(map[Coord]int), steps) result = reachableBySteps log.Print("reachable after steps : ", steps, result) return result } // let's do dijkstra? // i would need lots of space for edges? // let's use a map with minimal distances? // OR. just breath first traversal type Field struct { RowStart, ColStart int symbols [][]rune SaturatedEvenCount, SaturatedOddCount int } type Coord struct { Row, Col int } type FieldPoint struct { InField Coord MetaField Coord } func (f Field) ReachableBySteps(n int, startingAt map[FieldPoint]any, saturatedFields map[Coord]int, initialSteps int) (countReachable int) { if n%100 == 0 { log.Println("going step: ", n) } if n == 0 { sizeOfUnsaturated := len(startingAt) sizeOfSaturated := 0 // log.Printf("> before adding saturated fields. central is in even %t\n", CentralFieldIsInEven(initialSteps, n)) for saturatedField := range saturatedFields { isEven := FieldIsInEven(initialSteps, n, saturatedField) // log.Printf("> adding saturated field %+v. it is in even %t\n", saturatedField, isEven) if isEven { sizeOfSaturated += f.SaturatedEvenCount } else { sizeOfSaturated += f.SaturatedOddCount } } return sizeOfUnsaturated + sizeOfSaturated } // else collect directly available oneStepExpanded := make(map[FieldPoint]any) for cur := range startingAt { for _, neighbor := range f.Neighbors(cur, saturatedFields) { oneStepExpanded[neighbor] = struct{}{} } } metaFields := make(map[Coord]int) for next := range oneStepExpanded { metaFields[next.MetaField] += 1 } for workedUponFieldCoord, amount := range metaFields { isEven := FieldIsInEven(initialSteps, n, workedUponFieldCoord) if workedUponFieldCoord.Col == 0 && workedUponFieldCoord.Row == 0 { // log.Printf("checking %+v : %d as worked fields for saturation. isEven %t", workedUponFieldCoord, amount, isEven) } if isEven && amount == f.SaturatedEvenCount { log.Printf(">>> adding %+v to saturated, with amount %d\n", workedUponFieldCoord, amount) saturatedFields[workedUponFieldCoord] = n } if !isEven && amount == f.SaturatedOddCount { log.Printf(">>> adding %+v to saturated, with amount %d\n", workedUponFieldCoord, amount) saturatedFields[workedUponFieldCoord] = n } } for point := range oneStepExpanded { saturatedAtStep, fromSaturated := saturatedFields[point.MetaField] // hack. to not remove points from saturated fields too early if fromSaturated && (saturatedAtStep - n > 200) { delete(oneStepExpanded, point) } } // if n < 4 { // log.Print("reachable after steps : ", n, len(oneStepExpanded)) // f.PrintCoord(oneStepExpanded, 5) // } return f.ReachableBySteps(n-1, oneStepExpanded, saturatedFields, initialSteps) } func CentralFieldIsInEven(initialSteps, currentSteps int) bool { // off by one here because on initial step we first do 'neighbors' then comparicons return (initialSteps-currentSteps)%2 != 0 } func FieldIsInEven(initialSteps, currentSteps int, metaCoord Coord) bool { centralIsInEven := CentralFieldIsInEven(initialSteps, currentSteps) fieldIsInSyncWithCentral := (metaCoord.Col+metaCoord.Row)%2 == 0 if fieldIsInSyncWithCentral { return centralIsInEven } else { return !centralIsInEven } } func (f Field) Neighbors(c FieldPoint, saturatedFields map[Coord]int) (resut []FieldPoint) { closeCoords := []FieldPoint{ {InField: Coord{Row: c.InField.Row + 1, Col: c.InField.Col}, MetaField: c.MetaField}, {InField: Coord{Row: c.InField.Row - 1, Col: c.InField.Col}, MetaField: c.MetaField}, {InField: Coord{Row: c.InField.Row, Col: c.InField.Col + 1}, MetaField: c.MetaField}, {InField: Coord{Row: c.InField.Row, Col: c.InField.Col - 1}, MetaField: c.MetaField}, } for i, close := range closeCoords { height := len(f.symbols) width := len(f.symbols[0]) if close.InField.Row == height { close.InField.Row = 0 close.MetaField.Row += 1 } if close.InField.Row == -1 { close.InField.Row = height - 1 close.MetaField.Row -= 1 } if close.InField.Col == width { close.InField.Col = 0 close.MetaField.Col += 1 } if close.InField.Col == -1 { // log.Printf("moving COL to lefter field from %d to %d", close.Col, width-1) close.InField.Col = width - 1 close.MetaField.Col -= 1 } closeCoords[i] = close // but this is not it. i need to store the XX and YY // so that points in other 'fields' would count separately. yuk } for _, close := range closeCoords { if f.ValidCoord(close.InField.Row, close.InField.Col) { symb := f.symbols[close.InField.Row][close.InField.Col] _, fieldIsAlreadySaturated := saturatedFields[close.MetaField] if (symb == '.' || symb == 'S') && !fieldIsAlreadySaturated { resut = append(resut, close) } } } // log.Print("getting neighbors for ", c, resut) return } func (f Field) ValidCoord(row, col int) bool { // log.Print("check valid ", row, col, row >= 0 && row < len(f.symbols) && col >= 0 && col < len(f.symbols[0])) valid := row >= 0 && row < len(f.symbols) && col >= 0 && col < len(f.symbols[0]) if !valid { panic(fmt.Sprint("getting invalid coord: ", row, col)) } return valid } func (f Field) String() (result string) { result += "\n" for _, line := range f.symbols { result += string(line) result += "\n" } return } func ReadField(filename string) (result Field) { bytes, err := os.ReadFile(filename) if err != nil { panic(err) } text := strings.TrimSpace(string(bytes)) lines := strings.Split(text, "\n") rows := make([][]rune, len(lines)) for rowNum, line := range lines { rows[rowNum] = []rune(line) for colNum, symb := range line { if symb == 'S' { result.RowStart = rowNum result.ColStart = colNum } } } result.symbols = rows odd, even := result.PointsInEachPhase() result.SaturatedEvenCount = even result.SaturatedOddCount = odd return } func (f Field) PrintCoord(coords map[FieldPoint]any, expandByField int) { for fieldRow := -expandByField; fieldRow <= expandByField; fieldRow++ { lines := make([]string, len(f.symbols)) for fieldCol := -expandByField; fieldCol <= expandByField; fieldCol++ { for rowNum, row := range f.symbols { for colNum, col := range row { _, marked := coords[FieldPoint{InField: Coord{Row: rowNum, Col: colNum}, MetaField: Coord{Row: fieldRow, Col: fieldCol}}] if marked { lines[rowNum] += "O" } else { lines[rowNum] += string(col) } } } } for _, line := range lines { fmt.Println(line) } } return } // if the field is fully saturated, what is amount of 'visited' points? // odd - meaning one step around 'S', even - meaning with standing on 'S' func (f Field) PointsInEachPhase() (pointsIfOddPhase, pointsIfEvenPhase int) { remainderOfEvenPhase := (f.RowStart + f.ColStart) % 2 text := "\n" for i, row := range f.symbols { for j, cell := range row { if cell != '#' { if (i+j)%2 == remainderOfEvenPhase { pointsIfEvenPhase += 1 text += "E" } else { pointsIfOddPhase += 1 text += "O" } } else { text += "#" } } text += "\n" } fmt.Println(text) log.Printf("calculating points in even and odd phases", pointsIfEvenPhase, pointsIfOddPhase) return }