192 lines
4.3 KiB
Go
192 lines
4.3 KiB
Go
package day20
|
|
|
|
import (
|
|
"container/heap"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
)
|
|
|
|
var directions = [][]int{
|
|
{0, 1}, // Right
|
|
{1, 0}, // Down
|
|
{0, -1}, // Left
|
|
{-1, 0}, // Up
|
|
}
|
|
|
|
type State struct {
|
|
row, col, dir, score int
|
|
path []string
|
|
//Would rather use tuple as key but cant get to work
|
|
costMap map[[2]int]int
|
|
}
|
|
|
|
type PriorityQueue []State
|
|
|
|
func Part1(input string) int {
|
|
maze := parseInput(input)
|
|
_, path, costMap := solveMaze(maze)
|
|
cheats := findCheats(path, costMap)
|
|
return cheats
|
|
}
|
|
|
|
func Part2(input string) int {
|
|
maze := parseInput(input)
|
|
_, path, costMap := solveMaze(maze)
|
|
cheats := findCheatsPart2(path, costMap)
|
|
return cheats
|
|
}
|
|
|
|
func parseInput(input string) []string {
|
|
parts := strings.Split(input, "\n")
|
|
maze := []string{}
|
|
maze = append(maze, parts...)
|
|
|
|
return maze
|
|
}
|
|
|
|
func (pq PriorityQueue) Len() int { return len(pq) }
|
|
func (pq PriorityQueue) Less(i, j int) bool {
|
|
return pq[i].score < pq[j].score
|
|
}
|
|
func (pq PriorityQueue) Swap(i, j int) { pq[i], pq[j] = pq[j], pq[i] }
|
|
|
|
func (pq *PriorityQueue) Push(x interface{}) {
|
|
*pq = append(*pq, x.(State))
|
|
}
|
|
|
|
func (pq *PriorityQueue) Pop() interface{} {
|
|
old := *pq
|
|
n := len(old)
|
|
item := old[n-1]
|
|
*pq = old[0 : n-1]
|
|
return item
|
|
}
|
|
|
|
func solveMaze(maze []string) (int, []string, map[[2]int]int) {
|
|
rows := len(maze)
|
|
cols := len(maze[0])
|
|
|
|
// Locate start and end points
|
|
var startRow, startCol, endRow, endCol int
|
|
for i := 0; i < rows; i++ {
|
|
for j := 0; j < cols; j++ {
|
|
if maze[i][j] == 'S' {
|
|
startRow, startCol = i, j
|
|
} else if maze[i][j] == 'E' {
|
|
endRow, endCol = i, j
|
|
}
|
|
}
|
|
}
|
|
|
|
// Priority queue and visited set
|
|
pq := &PriorityQueue{}
|
|
heap.Init(pq)
|
|
startPath := []string{fmt.Sprintf("(%d,%d)", startRow, startCol)}
|
|
startCostMap := map[[2]int]int{
|
|
{startRow, startCol}: 0,
|
|
}
|
|
heap.Push(pq, State{startRow, startCol, 0, 0, startPath, startCostMap})
|
|
visited := make(map[[3]int]bool)
|
|
|
|
for pq.Len() > 0 {
|
|
cur := heap.Pop(pq).(State)
|
|
|
|
// If reached the end point, return the score and path
|
|
if cur.row == endRow && cur.col == endCol {
|
|
return cur.score, cur.path, cur.costMap
|
|
}
|
|
|
|
// Mark as visited
|
|
key := [3]int{cur.row, cur.col, cur.dir}
|
|
if visited[key] {
|
|
continue
|
|
}
|
|
visited[key] = true
|
|
|
|
// Explore moves
|
|
for i := 0; i < 4; i++ {
|
|
// Compute new direction
|
|
newDir := (cur.dir + i) % 4
|
|
|
|
// Compute new position
|
|
newRow := cur.row + directions[newDir][0]
|
|
newCol := cur.col + directions[newDir][1]
|
|
|
|
// Check bounds and wall
|
|
if newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols && maze[newRow][newCol] != '#' {
|
|
newCost := cur.score + 1
|
|
if oldCost, exists := cur.costMap[[2]int{newRow, newCol}]; !exists || newCost < oldCost {
|
|
// Update cost map
|
|
newCostMap := make(map[[2]int]int)
|
|
for k, v := range cur.costMap {
|
|
newCostMap[k] = v
|
|
}
|
|
newCostMap[[2]int{newRow, newCol}] = newCost
|
|
|
|
// Update path
|
|
newPath := append([]string{}, cur.path...)
|
|
newPath = append(newPath, fmt.Sprintf("(%d,%d)", newRow, newCol))
|
|
|
|
heap.Push(pq, State{
|
|
newRow,
|
|
newCol,
|
|
newDir,
|
|
newCost,
|
|
newPath,
|
|
newCostMap,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return -1, nil, nil // No path found
|
|
}
|
|
|
|
func findCheats(path []string, costLookup map[[2]int]int) int {
|
|
|
|
totalCheats := 0
|
|
for index, position := range path {
|
|
|
|
var row, col int
|
|
//Should be a better way to do this
|
|
fmt.Sscanf(position, "(%d,%d)", &row, &col)
|
|
|
|
for _, dir := range directions {
|
|
newRow := row + (dir[0] * 2)
|
|
newCol := col + (dir[1] * 2)
|
|
|
|
if cost, exists := costLookup[[2]int{newRow, newCol}]; exists {
|
|
if cost > index && (cost-index-2) >= 100 {
|
|
totalCheats++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return totalCheats
|
|
}
|
|
|
|
func findCheatsPart2(path []string, costLookup map[[2]int]int) int {
|
|
totalCheats := 0
|
|
|
|
for index, position := range path {
|
|
var row, col int
|
|
//Should be a better way to do this
|
|
fmt.Sscanf(position, "(%d,%d)", &row, &col)
|
|
|
|
for x1 := row - 20; x1 <= row+20; x1++ {
|
|
for y1 := col - 20 + int(math.Abs(float64(row-x1))); y1 <= col+20-int(math.Abs(float64(row-x1))); y1++ {
|
|
if cost, exists := costLookup[[2]int{x1, y1}]; exists && cost != -1 {
|
|
diff := cost - index - int(math.Abs(float64(row-x1))) - int(math.Abs(float64(col-y1)))
|
|
if cost > index && diff >= 100 {
|
|
totalCheats++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return totalCheats
|
|
}
|