advent-of-code-2023/day23/main.go

277 lines
6.6 KiB
Go

// This code is horrible and repetitive, I'm sorry
package main
import (
"errors"
"fmt"
"io"
"os"
"slices"
"strings"
)
type Tile rune
const (
TileEmpty Tile = '.'
TileWall Tile = '#'
TileRight Tile = '>'
TileLeft Tile = '<'
TileUp Tile = '^'
TileDown Tile = 'v'
)
type Coordinate struct {
Row int
Col int
}
type GraphNode struct {
Position Coordinate
Weight int
}
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s inputfile\n", os.Args[0])
os.Exit(1)
}
inputFilename := os.Args[1]
inputFile, err := os.Open(inputFilename)
if err != nil {
panic(fmt.Sprintf("could not open input file: %s", err))
}
defer inputFile.Close()
inputBytes, err := io.ReadAll(inputFile)
if err != nil {
panic(fmt.Sprintf("could not read input file: %s", err))
}
input := strings.TrimSpace(string(inputBytes))
inputLines := strings.Split(input, "\n")
grid, err := parseGrid(inputLines)
if err != nil {
panic(fmt.Sprintf("could not parse input: %s", err))
}
fmt.Printf("Part 1: %d\n", part1(grid))
fmt.Printf("Part 2: %d\n", part2(grid))
}
func part1(grid [][]Tile) int {
return solve(grid, true)
}
func part2(grid [][]Tile) int {
return solve(grid, false)
}
func solve(grid [][]Tile, respectSlopes bool) int {
startCol, err := findStartingTile(grid[0])
if err != nil {
panic(fmt.Sprintf("could not find starting tile: %s", err))
}
endCol, err := findStartingTile(grid[len(grid)-1])
if err != nil {
panic(fmt.Sprintf("could not find starting tile: %s", err))
}
graph := buildCondensedGraph(
Coordinate{Row: 0, Col: startCol},
Coordinate{Row: len(grid) - 1, Col: endCol},
grid,
respectSlopes,
)
return findLongestPath(
Coordinate{Row: 0, Col: startCol},
Coordinate{Row: len(grid) - 1, Col: endCol},
grid,
graph,
)
}
func findStartingTile(firstRow []Tile) (int, error) {
candidate := (*int)(nil)
for col, item := range firstRow {
if item == TileEmpty && candidate == nil {
savedCol := col
candidate = &savedCol
} else if item == TileEmpty && candidate != nil {
return 0, errors.New("more than one open position")
}
}
if candidate == nil {
return 0, errors.New("no open positions")
}
return *candidate, nil
}
func buildCondensedGraph(start, end Coordinate, grid [][]Tile, respectSlopes bool) map[Coordinate][]GraphNode {
inBounds := func(pos Coordinate) bool {
return pos.Row >= 0 && pos.Row < len(grid) && pos.Col >= 0 && pos.Col < len(grid[0])
}
toVisit := []Coordinate{start}
visited := map[Coordinate]struct{}{}
distances := map[Coordinate]int{
start: 0,
}
intersections := map[Coordinate]struct{}{start: {}, end: {}}
for len(toVisit) > 0 {
visiting := toVisit[0]
toVisit = toVisit[1:]
visited[visiting] = struct{}{}
validNeighbors := []Coordinate{}
for _, neighbor := range findNeighbors(grid, visiting, respectSlopes) {
if !inBounds(neighbor) {
continue
} else if grid[neighbor.Row][neighbor.Col] == TileWall {
continue
}
distances[neighbor] = distances[visiting] + 1
validNeighbors = append(validNeighbors, neighbor)
}
if len(validNeighbors) > 2 {
intersections[visiting] = struct{}{}
}
for _, neighbor := range validNeighbors {
if neighbor == end {
intersections[neighbor] = struct{}{}
}
if _, ok := visited[neighbor]; !ok {
toVisit = append(
toVisit,
neighbor,
)
}
}
}
res := map[Coordinate][]GraphNode{}
for intersection := range intersections {
toVisit := []Coordinate{intersection}
visited := map[Coordinate]struct{}{}
distances := map[Coordinate]int{
intersection: 0,
}
for len(toVisit) > 0 {
visiting := toVisit[0]
toVisit = toVisit[1:]
visited[visiting] = struct{}{}
for _, neighbor := range findNeighbors(grid, visiting, respectSlopes) {
if !inBounds(neighbor) {
continue
} else if grid[neighbor.Row][neighbor.Col] == TileWall {
continue
}
distances[neighbor] = distances[visiting] + 1
if _, ok := intersections[neighbor]; ok && neighbor != intersection {
res[intersection] = append(
res[intersection],
GraphNode{Position: neighbor, Weight: distances[neighbor]},
)
} else if _, ok := visited[neighbor]; !ok {
toVisit = append(toVisit, neighbor)
}
}
}
}
return res
}
func findLongestPath(start Coordinate, end Coordinate, grid [][]Tile, graph map[Coordinate][]GraphNode) int {
var dfs func(Coordinate, []GraphNode) []GraphNode
dfs = func(coordinate Coordinate, path []GraphNode) []GraphNode {
children := graph[coordinate]
longestPath := path
for _, child := range children {
if slices.ContainsFunc(path, func(node GraphNode) bool { return node.Position == child.Position }) {
continue
}
nextPath := slices.Clone(path)
nextPath = append(nextPath, child)
fullPath := dfs(child.Position, nextPath)
if sumWeights(fullPath) > sumWeights(longestPath) && fullPath[len(fullPath)-1].Position == end {
longestPath = fullPath
}
}
return longestPath
}
res := dfs(start, []GraphNode{})
return sumWeights(res)
}
func sumWeights(nodes []GraphNode) int {
total := 0
for _, node := range nodes {
total += node.Weight
}
return total
}
func findNeighbors(grid [][]Tile, position Coordinate, respectSlopes bool) []Coordinate {
upNeighbor := Coordinate{Row: position.Row - 1, Col: position.Col}
downNeighbor := Coordinate{Row: position.Row + 1, Col: position.Col}
leftNeighbor := Coordinate{Row: position.Row, Col: position.Col - 1}
rightNeighbor := Coordinate{Row: position.Row, Col: position.Col + 1}
if respectSlopes && grid[position.Row][position.Col] == TileLeft {
return []Coordinate{leftNeighbor}
} else if respectSlopes && grid[position.Row][position.Col] == TileRight {
return []Coordinate{rightNeighbor}
} else if respectSlopes && grid[position.Row][position.Col] == TileUp {
return []Coordinate{upNeighbor}
} else if respectSlopes && grid[position.Row][position.Col] == TileDown {
return []Coordinate{downNeighbor}
}
return []Coordinate{upNeighbor, downNeighbor, leftNeighbor, rightNeighbor}
}
func parseGrid(inputLines []string) ([][]Tile, error) {
if len(inputLines) == 0 {
return nil, errors.New("cannot parse grid of no length")
}
height := len(inputLines)
width := len(inputLines[0])
grid := make([][]Tile, height)
for i, line := range inputLines {
if len(line) != width {
return nil, errors.New("grid's rows are not of consistent length")
}
for _, char := range line {
if Tile(char) != TileWall && Tile(char) != TileEmpty && Tile(char) != TileUp && Tile(char) != TileDown && Tile(char) != TileLeft && Tile(char) != TileRight {
return nil, fmt.Errorf("invalid tile char %c", char)
}
grid[i] = append(grid[i], Tile(char))
}
}
return grid, nil
}