From 554d8bb2f55ea2a9132639128201d37aec7992ad Mon Sep 17 00:00:00 2001 From: Nick Krichevsky Date: Tue, 12 Dec 2023 22:06:10 -0500 Subject: [PATCH] Solve day 10 part 2 --- day10/main.go | 247 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 233 insertions(+), 14 deletions(-) diff --git a/day10/main.go b/day10/main.go index eca2fea..4d22996 100644 --- a/day10/main.go +++ b/day10/main.go @@ -1,3 +1,5 @@ +// I definitely overcomplicated this problem, but it took me a very long time to visualize things properly + package main import ( @@ -27,9 +29,16 @@ const ( PipeF ) +type ScanDirection int + +const ( + ScanDirectionHorizontal ScanDirection = iota + ScanDirectionVertical +) + type PipeMap map[Coordinate]Pipe -var ErrNoPipe = errors.New("no pipe at location") +var ErrMissingPipe = errors.New("no pipe at location") func (coordinate Coordinate) North() Coordinate { return Coordinate{row: coordinate.row - 1, col: coordinate.col} @@ -47,8 +56,8 @@ func (coordinate Coordinate) West() Coordinate { return Coordinate{row: coordinate.row, col: coordinate.col - 1} } -// Neighbors gets all the neighbors of the given coordinate (cardinal directions) -func (coordinate Coordinate) Neighbors() []Coordinate { +// CardinalNeighbors gets all the neighbors of the given coordinate (cardinal directions) +func (coordinate Coordinate) CardinalNeighbors() []Coordinate { return []Coordinate{ coordinate.North(), coordinate.South(), @@ -57,6 +66,18 @@ func (coordinate Coordinate) Neighbors() []Coordinate { } } +// AllNeighbors gets all the neighbors of the given coordinate in all directions +func (coordinate Coordinate) AllNeighbors() []Coordinate { + diagonalNeighbors := []Coordinate{ + {row: coordinate.row - 1, col: coordinate.col - 1}, + {row: coordinate.row - 1, col: coordinate.col + 1}, + {row: coordinate.row + 1, col: coordinate.col - 1}, + {row: coordinate.row + 1, col: coordinate.col + 1}, + } + + return append(coordinate.CardinalNeighbors(), diagonalNeighbors...) +} + // ConnectsNorth will determine if the given pipe can connect to a pipe to its north func (pipe Pipe) ConnectsNorth() bool { return pipe == PipeVertical || pipe == PipeL || pipe == PipeJ @@ -77,8 +98,44 @@ func (pipe Pipe) ConnectsWest() bool { return pipe == PipeHorizontal || pipe == Pipe7 || pipe == PipeJ } -// Print will print the entire map in the form the puzzle presents it -func (pipeMap PipeMap) Print() { +// IsCorner indicates whether or not a pipe is a corner +func (pipe Pipe) IsCorner() bool { + return pipe == PipeJ || pipe == Pipe7 || pipe == PipeF || pipe == PipeL +} + +// IsStraight indicates whether or not a pipe is straight +func (pipe Pipe) IsStraight() bool { + return pipe == PipeHorizontal || pipe == PipeVertical +} + +// IsParallelToScan indicates whether or not a pipe moves only parallel to the scan direction +func (pipe Pipe) IsParallelToScan(scanDir ScanDirection) bool { + if scanDir == ScanDirectionHorizontal { + return pipe == PipeHorizontal + } else { + return pipe == PipeVertical + } +} + +// ConnectedNeighbors gets only the connected neighbors to a pipe at a position +func (pipeMap PipeMap) ConnectedNeighbors(position Coordinate) []Coordinate { + _, ok := pipeMap[position] + if !ok { + return []Coordinate{} + } + + connectedNeighbors := []Coordinate{} + for _, neighbor := range position.CardinalNeighbors() { + if pipeMap.PipesConnect(position, neighbor) { + connectedNeighbors = append(connectedNeighbors, neighbor) + } + } + + return connectedNeighbors +} + +// PipeBounds finds the bounds of the pieps on the map +func (pipeMap PipeMap) PipeBounds() (Coordinate, Coordinate) { positions := mapKeys(pipeMap) compareRow := func(a, b Coordinate) int { return cmp.Compare(a.row, b.row) @@ -87,14 +144,19 @@ func (pipeMap PipeMap) Print() { compareCol := func(a, b Coordinate) int { return cmp.Compare(a.col, b.col) } - minRow := slices.MinFunc(positions, compareRow).row maxRow := slices.MaxFunc(positions, compareRow).row minCol := slices.MinFunc(positions, compareCol).col maxCol := slices.MaxFunc(positions, compareCol).col - for row := minRow; row <= maxRow; row++ { - for col := minCol; col <= maxCol; col++ { + return Coordinate{row: minRow, col: minCol}, Coordinate{row: maxRow, col: maxCol} +} + +// Print will print the entire map in the form the puzzle presents it +func (pipeMap PipeMap) Print() { + minCorner, maxCorner := pipeMap.PipeBounds() + for row := minCorner.row; row <= maxCorner.row; row++ { + for col := minCorner.col; col <= maxCorner.col; col++ { location := Coordinate{row: row, col: col} pipe, ok := pipeMap[location] if !ok { @@ -162,6 +224,7 @@ func main() { } fmt.Printf("Part 1: %d\n", part1(pipeMap, startPosition)) + fmt.Printf("Part 2: %d\n", part2(pipeMap, startPosition)) } func part1(pipeMap PipeMap, startPosition Coordinate) int { @@ -181,10 +244,8 @@ func part1(pipeMap PipeMap, startPosition Coordinate) int { maxDistance = visiting.distance } - for _, neighbor := range visiting.position.Neighbors() { - if pipeMap[neighbor] == PipeUnknown || !pipeMap.PipesConnect(visiting.position, neighbor) { - continue - } else if _, ok := visited[neighbor]; ok { + for _, neighbor := range pipeMap.ConnectedNeighbors(visiting.position) { + if _, ok := visited[neighbor]; ok { continue } @@ -200,13 +261,171 @@ func part1(pipeMap PipeMap, startPosition Coordinate) int { return maxDistance } +func part2(pipeMap PipeMap, startPosition Coordinate) int { + mainLoopMap := traceMainLoop(pipeMap, startPosition) + + regions := findEmptyRegions(mainLoopMap, startPosition) + area := 0 + for _, region := range regions { + isAccessible := isRegionExternallyAccessible(mainLoopMap, region) + if !isAccessible { + area += len(region) + } + } + + return area +} + +// traceMainLoop walks the pipes and finds the pipes relevant to the problem +func traceMainLoop(pipeMap PipeMap, startPosition Coordinate) PipeMap { + visited := map[Coordinate]Pipe{} + toVisit := []Coordinate{startPosition} + for len(toVisit) > 0 { + visitingPosition := toVisit[0] + toVisit = toVisit[1:] + + visited[visitingPosition] = pipeMap[visitingPosition] + + for _, neighbor := range pipeMap.ConnectedNeighbors(visitingPosition) { + if _, ok := visited[neighbor]; ok { + continue + } + + toVisit = append(toVisit, neighbor) + } + } + + return visited +} + +// isRegionExternallyAccessible indicates whether or not all of the given coordinates are internal to the loop +func isRegionExternallyAccessible(pipeMap PipeMap, region []Coordinate) bool { + for _, pos := range region { + if !isInsideViaRay(pipeMap, pos, ScanDirectionHorizontal) || !isInsideViaRay(pipeMap, pos, ScanDirectionVertical) { + return true + } + } + + return false +} + +// isInsideViaRay casts a ray in the given direction, counting the number of edge crossings to determine +// if a tile is inside +func isInsideViaRay(pipeMap PipeMap, target Coordinate, direction ScanDirection) bool { + cursor := target + if direction == ScanDirectionHorizontal { + cursor.col = 0 + } else { + cursor.row = 0 + } + + pipeBuffer := []Pipe{} + crossings := 0 + for cursor != target { + if pipeMap[cursor] != PipeUnknown { + pipeBuffer = append(pipeBuffer, pipeMap[cursor]) + } + + if direction == ScanDirectionHorizontal { + cursor.col++ + } else { + cursor.row++ + } + } + crossings += numRayCrossings(direction, pipeBuffer) + + return crossings%2 == 1 +} + +// numRayCrossings counts the number of times a ray crosses a pipe +func numRayCrossings(scanDirection ScanDirection, scannedPipes []Pipe) int { + crossings := 0 + for _, pipe := range scannedPipes { + if pipe.IsStraight() && !pipe.IsParallelToScan(scanDirection) { + crossings++ + } else if pipe.IsCorner() { + if scanDirection == ScanDirectionHorizontal && (pipe == PipeL || pipe == PipeJ) { + // If we are scanning horizontally, and we cross one of these chars, we are internal if we cross something of this variety only once + crossings++ + } else if scanDirection == ScanDirectionVertical && (pipe == PipeF || pipe == PipeL) { + // ditto for vertically + crossings++ + } + } + } + + return crossings +} + +// findEmptyRegions finds all of the locations where there are empty positions on the graph +func findEmptyRegions(pipeMap PipeMap, startPosition Coordinate) [][]Coordinate { + emptyPositions := findEmptyPositions(pipeMap) + regions := [][]Coordinate{} + visited := map[Coordinate]struct{}{} + for _, position := range emptyPositions { + if _, ok := visited[position]; ok { + continue + } + + floodedPositions := flood(pipeMap, position) + regions = append(regions, floodedPositions) + for _, flooded := range floodedPositions { + visited[flooded] = struct{}{} + } + } + + return regions +} + +// findEmptyPositions finds all empty positions on the graph +func findEmptyPositions(pipeMap PipeMap) []Coordinate { + minCorner, maxCorner := pipeMap.PipeBounds() + empty := []Coordinate{} + for row := minCorner.row; row < maxCorner.row; row++ { + for col := minCorner.col; col < maxCorner.col; col++ { + pos := Coordinate{row: row, col: col} + if pipeMap[pos] == PipeUnknown { + empty = append(empty, pos) + } + } + } + + return empty +} + +// flood performs a flood fill to locate neighboring empty spots +func flood(pipeMap PipeMap, start Coordinate) []Coordinate { + minCorner, maxCorner := pipeMap.PipeBounds() + toVisit := []Coordinate{start} + visited := map[Coordinate]struct{}{} + for len(toVisit) > 0 { + visiting := toVisit[0] + toVisit = toVisit[1:] + if _, ok := pipeMap[visiting]; ok { + // If we've hit a pipe on the bounding box, don't keep filling + continue + } else if visiting.row < minCorner.row || visiting.col < minCorner.col || visiting.row > maxCorner.row || visiting.col > maxCorner.col { + // if we've moved out of bounds, don't continue either + continue + } else if _, ok := visited[visiting]; ok { + // If we've already visited this, we don't need to try again + continue + } + + visited[visiting] = struct{}{} + toVisit = append(toVisit, visiting.CardinalNeighbors()...) + } + + return mapKeys(visited) +} + func parsePipeMap(inputLines []string) (PipeMap, Coordinate, error) { pipeMap := PipeMap{} startPosition := (*Coordinate)(nil) for row, line := range inputLines { for col, char := range line { pipe, err := parsePipeChar(char) - if errors.Is(err, ErrNoPipe) { + if errors.Is(err, ErrMissingPipe) { continue } else if err != nil { return nil, Coordinate{}, fmt.Errorf("malformed at (%d, %d): %w", row, col, err) @@ -237,7 +456,7 @@ func parsePipeMap(inputLines []string) (PipeMap, Coordinate, error) { func parsePipeChar(c rune) (Pipe, error) { switch c { case '.': - return PipeUnknown, ErrNoPipe + return PipeUnknown, ErrMissingPipe case '|': return PipeVertical, nil case '-':