2023-12-24 18:27:51 +00:00
|
|
|
// This code is horrible and repetitive, I'm sorry
|
|
|
|
|
2023-12-23 21:00:51 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-12-24 18:27:51 +00:00
|
|
|
type GraphNode struct {
|
|
|
|
Position Coordinate
|
|
|
|
Weight int
|
|
|
|
}
|
|
|
|
|
2023-12-23 21:00:51 +00:00
|
|
|
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))
|
2023-12-24 18:27:51 +00:00
|
|
|
fmt.Printf("Part 2: %d\n", part2(grid))
|
2023-12-23 21:00:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func part1(grid [][]Tile) int {
|
2023-12-24 18:27:51 +00:00
|
|
|
return solve(grid, true)
|
|
|
|
}
|
|
|
|
|
|
|
|
func part2(grid [][]Tile) int {
|
|
|
|
return solve(grid, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
func solve(grid [][]Tile, respectSlopes bool) int {
|
2023-12-23 21:00:51 +00:00
|
|
|
startCol, err := findStartingTile(grid[0])
|
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Sprintf("could not find starting tile: %s", err))
|
|
|
|
}
|
|
|
|
|
2023-12-24 18:27:51 +00:00
|
|
|
endCol, err := findStartingTile(grid[len(grid)-1])
|
|
|
|
if err != nil {
|
|
|
|
panic(fmt.Sprintf("could not find starting tile: %s", err))
|
|
|
|
}
|
|
|
|
|
|
|
|
graph := buildCondensedGraph(
|
2023-12-23 21:00:51 +00:00
|
|
|
Coordinate{Row: 0, Col: startCol},
|
2023-12-24 18:27:51 +00:00
|
|
|
Coordinate{Row: len(grid) - 1, Col: endCol},
|
2023-12-23 21:00:51 +00:00
|
|
|
grid,
|
2023-12-24 18:27:51 +00:00
|
|
|
respectSlopes,
|
2023-12-23 21:00:51 +00:00
|
|
|
)
|
|
|
|
|
2023-12-24 18:27:51 +00:00
|
|
|
return findLongestPath(
|
|
|
|
Coordinate{Row: 0, Col: startCol},
|
|
|
|
Coordinate{Row: len(grid) - 1, Col: endCol},
|
|
|
|
grid,
|
|
|
|
graph,
|
|
|
|
)
|
2023-12-23 21:00:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-12-24 18:27:51 +00:00
|
|
|
func buildCondensedGraph(start, end Coordinate, grid [][]Tile, respectSlopes bool) map[Coordinate][]GraphNode {
|
2023-12-23 21:00:51 +00:00
|
|
|
inBounds := func(pos Coordinate) bool {
|
|
|
|
return pos.Row >= 0 && pos.Row < len(grid) && pos.Col >= 0 && pos.Col < len(grid[0])
|
|
|
|
}
|
|
|
|
|
2023-12-24 18:27:51 +00:00
|
|
|
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
|
2023-12-23 21:00:51 +00:00
|
|
|
}
|
2023-12-24 18:27:51 +00:00
|
|
|
|
|
|
|
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{}{}
|
2023-12-23 21:00:51 +00:00
|
|
|
}
|
2023-12-24 18:27:51 +00:00
|
|
|
|
|
|
|
if _, ok := visited[neighbor]; !ok {
|
|
|
|
toVisit = append(
|
|
|
|
toVisit,
|
|
|
|
neighbor,
|
|
|
|
)
|
2023-12-23 21:00:51 +00:00
|
|
|
}
|
2023-12-24 18:27:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2023-12-23 21:00:51 +00:00
|
|
|
if !inBounds(neighbor) {
|
|
|
|
continue
|
2023-12-24 18:27:51 +00:00
|
|
|
} else if grid[neighbor.Row][neighbor.Col] == TileWall {
|
2023-12-23 21:00:51 +00:00
|
|
|
continue
|
|
|
|
}
|
2023-12-24 18:27:51 +00:00
|
|
|
|
|
|
|
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)
|
2023-12-23 21:00:51 +00:00
|
|
|
}
|
|
|
|
}
|
2023-12-24 18:27:51 +00:00
|
|
|
}
|
|
|
|
}
|
2023-12-23 21:00:51 +00:00
|
|
|
|
2023-12-24 18:27:51 +00:00
|
|
|
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
|
|
|
|
}
|
2023-12-23 21:00:51 +00:00
|
|
|
}
|
|
|
|
|
2023-12-24 18:27:51 +00:00
|
|
|
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}
|
2023-12-23 21:00:51 +00:00
|
|
|
}
|
|
|
|
|
2023-12-24 18:27:51 +00:00
|
|
|
return []Coordinate{upNeighbor, downNeighbor, leftNeighbor, rightNeighbor}
|
2023-12-23 21:00:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|