Files
aoc2024/day16/main.go
2024-12-20 15:33:16 +01:00

139 lines
3.2 KiB
Go

package main
import (
"bufio"
"container/heap"
"fmt"
"math"
"os"
"strings"
)
func main() {
fmt.Println(7036, 45)
fmt.Println(part1And2(readData("example.txt")))
fmt.Println(11048, 64)
fmt.Println(part1And2(readData("example2.txt")))
fmt.Println(89460, 504)
fmt.Println(part1And2(readData("data.txt")))
}
type Position struct {
x, y int
}
type PositionDir struct {
x, y int
dir int
}
type Candidate struct {
x, y int
dir int
cost int
visited []PositionDir
}
type CandidateQueue struct {
elements []Candidate
}
func (queue *CandidateQueue) Len() int {
return len(queue.elements)
}
func (queue *CandidateQueue) Less(i, j int) bool {
return queue.elements[i].cost < queue.elements[j].cost
}
func (queue *CandidateQueue) Swap(i, j int) {
queue.elements[i], queue.elements[j] = queue.elements[j], queue.elements[i]
}
func (queue *CandidateQueue) Push(x any) {
queue.elements = append(queue.elements, x.(Candidate))
}
func (queue *CandidateQueue) AddElement(candidate Candidate) {
heap.Push(queue, candidate)
}
func (queue *CandidateQueue) Pop() any {
if len(queue.elements) == 0 {
return nil
} else {
elmt := queue.elements[len(queue.elements)-1]
queue.elements = queue.elements[:len(queue.elements)-1]
return elmt
}
}
func cloneVisited(v []PositionDir) []PositionDir {
clone := make([]PositionDir, len(v))
for i, p := range v {
clone[i] = p
}
return clone
}
var directions = [4][2]int{{1, 0}, {0, 1}, {-1, 0}, {0, -1}}
// Originally I had implemented A* for part1. But because of part2 I modified it to be
// dumber. I wish we had full requirements from the start so I wouldn't waste time...
func part1And2(maze [][]byte) (scorePart1, scorePart2 int) {
endX, endY := len(maze[0])-2, 1
startX, startY := 1, len(maze)-2
queue := new(CandidateQueue)
heap.Push(queue, Candidate{startX, startY, 0, 0, []PositionDir{{startX, startY, 0}}})
minCost := math.MaxInt
visitedCost := make(map[PositionDir]int)
onOptimalPath := make(map[Position]bool)
for queue.Len() > 0 {
candidate := heap.Pop(queue).(Candidate)
posDir := PositionDir{candidate.x, candidate.y, candidate.dir}
alreadyVisitedCost, alreadyVisited := visitedCost[posDir]
if alreadyVisited && alreadyVisitedCost < candidate.cost {
continue
}
visitedCost[posDir] = candidate.cost
tryDirection := func(dir int, cost int) {
fwdX := candidate.x + directions[dir][0]
fwdY := candidate.y + directions[dir][1]
realCostFwd := candidate.cost + cost
if maze[fwdY][fwdX] != '#' && realCostFwd <= minCost {
if fwdX == endX && fwdY == endY && realCostFwd <= minCost {
for _, p := range candidate.visited {
onOptimalPath[Position{p.x, p.y}] = true
}
minCost = realCostFwd
} else {
visited := append(cloneVisited(candidate.visited), PositionDir{fwdX, fwdY, dir})
c := Candidate{fwdX, fwdY, dir, realCostFwd, visited}
heap.Push(queue, c)
}
}
}
tryDirection(candidate.dir, 1)
tryDirection((candidate.dir+1)%4, 1001)
tryDirection((candidate.dir+3)%4, 1001)
}
return minCost, len(onOptimalPath) + 1
}
func readData(fileName string) (maze [][]byte) {
fp, _ := os.Open(fileName)
scanner := bufio.NewScanner(fp)
for scanner.Scan() {
maze = append(maze, []byte(strings.TrimSpace(scanner.Text())))
}
return
}