mirror of
https://github.com/Threnklyn/advent-of-code-go.git
synced 2026-05-19 03:23:27 +02:00
286 lines
7.1 KiB
Go
286 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/alexchao26/advent-of-code-go/data-structures/heap"
|
|
"github.com/alexchao26/advent-of-code-go/util"
|
|
)
|
|
|
|
func main() {
|
|
var part int
|
|
flag.IntVar(&part, "part", 1, "part 1 or 2")
|
|
flag.Parse()
|
|
fmt.Println("Running part", part)
|
|
|
|
if part == 1 {
|
|
ans := part1(util.ReadFile("./input.txt"))
|
|
util.CopyToClipboard(fmt.Sprintf("%v", ans))
|
|
fmt.Println("Output:", ans)
|
|
} else {
|
|
ans := part2(util.ReadFile("./input.txt"))
|
|
util.CopyToClipboard(fmt.Sprintf("%v", ans))
|
|
fmt.Println("Output:", ans)
|
|
}
|
|
}
|
|
|
|
func part1(input string) int {
|
|
depth, targetX, targetY := parseInput(input)
|
|
|
|
regionTypeCalculator := memoRegionTypeCalculator(depth, targetX, targetY)
|
|
var ans int
|
|
|
|
for x := 0; x <= targetX; x++ {
|
|
for y := 0; y <= targetY; y++ {
|
|
riskLevel := int(regionTypeCalculator(x, y)) % 3
|
|
ans += riskLevel
|
|
}
|
|
}
|
|
|
|
return ans
|
|
}
|
|
|
|
func parseInput(input string) (int, int, int) {
|
|
lines := strings.Split(input, "\n")
|
|
var depth, targetX, targetY int
|
|
_, err := fmt.Sscanf(lines[0], "depth: %d", &depth)
|
|
if err != nil {
|
|
panic("parsing depth from input" + err.Error())
|
|
}
|
|
_, err = fmt.Sscanf(lines[1], "target: %d,%d", &targetX, &targetY)
|
|
if err != nil {
|
|
panic("parsing targetX and targetY from input" + err.Error())
|
|
}
|
|
|
|
return depth, targetX, targetY
|
|
}
|
|
|
|
func memoErosionLevelCalculator(depth, targetX, targetY int) func(x, y int) int {
|
|
// map to memoize results and prevent branch recursion
|
|
xyToErosion := map[[2]int]int{}
|
|
|
|
var closureGetErosionFunc func(x, y int) int
|
|
closureGetErosionFunc = func(x, y int) int {
|
|
coords := [2]int{x, y}
|
|
if e, ok := xyToErosion[coords]; ok {
|
|
return e
|
|
}
|
|
|
|
var geologicIndex int
|
|
if coords == [2]int{0, 0} || coords == [2]int{targetX, targetY} {
|
|
geologicIndex = 0
|
|
} else if y == 0 {
|
|
geologicIndex = x * 16807
|
|
} else if x == 0 {
|
|
geologicIndex = y * 48271
|
|
} else {
|
|
geologicIndex = closureGetErosionFunc(x-1, y) * closureGetErosionFunc(x, y-1)
|
|
}
|
|
erosionLevel := (geologicIndex + depth) % 20183
|
|
|
|
xyToErosion[coords] = erosionLevel
|
|
return erosionLevel
|
|
}
|
|
|
|
return closureGetErosionFunc
|
|
}
|
|
|
|
func memoRegionTypeCalculator(depth, targetX, targetY int) func(x, y int) regionType {
|
|
erosionCalculator := memoErosionLevelCalculator(depth, targetX, targetY)
|
|
|
|
xyToRegionType := map[[2]int]regionType{}
|
|
var closureRegionFunc func(x, y int) regionType
|
|
closureRegionFunc = func(x, y int) regionType {
|
|
if x < 0 || y < 0 {
|
|
return -1
|
|
}
|
|
coords := [2]int{x, y}
|
|
if rt, ok := xyToRegionType[coords]; ok {
|
|
return rt
|
|
}
|
|
erosion := erosionCalculator(x, y)
|
|
|
|
rt := regionType(erosion % 3)
|
|
xyToRegionType[coords] = rt
|
|
return rt
|
|
}
|
|
|
|
return closureRegionFunc
|
|
}
|
|
|
|
func part2(input string) int {
|
|
depth, targetX, targetY := parseInput(input)
|
|
regionCalculator := memoRegionTypeCalculator(depth, targetX, targetY)
|
|
|
|
heap := heap.NewMinHeap()
|
|
firstNode := node{
|
|
coords: [2]int{0, 0},
|
|
regionType: regionCalculator(0, 0),
|
|
equipped: torch,
|
|
totalTime: 0,
|
|
}
|
|
heap.Add(firstNode)
|
|
|
|
var eqCoordsToMinDist = map[equipmentType]map[[2]int]int{
|
|
neither: map[[2]int]int{},
|
|
climbingGear: map[[2]int]int{},
|
|
torch: map[[2]int]int{},
|
|
}
|
|
|
|
var currentNode node
|
|
for !(currentNode.coords[0] == targetX && currentNode.coords[1] == targetY) {
|
|
currentNode = step(heap, eqCoordsToMinDist, regionCalculator)
|
|
}
|
|
|
|
if currentNode.regionType != rocky {
|
|
panic("target must be rocky")
|
|
}
|
|
|
|
if currentNode.equipped != torch {
|
|
finalTime := currentNode.totalTime + 7
|
|
return finalTime
|
|
}
|
|
|
|
return currentNode.totalTime
|
|
}
|
|
|
|
type regionType int
|
|
type equipmentType int
|
|
|
|
const (
|
|
rocky regionType = iota // climbing gear or torch
|
|
wet // neither or climbing gear
|
|
narrow // neither or torch
|
|
)
|
|
const (
|
|
neither equipmentType = iota
|
|
climbingGear
|
|
torch
|
|
)
|
|
|
|
type node struct {
|
|
coords [2]int
|
|
equipped equipmentType
|
|
regionType regionType
|
|
totalTime int
|
|
}
|
|
|
|
func (n node) Value() int {
|
|
return n.totalTime
|
|
}
|
|
|
|
func step(heap *heap.MinHeap, eqCoordsToMinDist map[equipmentType]map[[2]int]int, regionCalculator func(x, y int) regionType) node {
|
|
// remove node from heap, this will be returned at the end
|
|
minNodeInterface := heap.Remove()
|
|
if minNodeInterface == nil {
|
|
panic("Heap is empty, it shouldn't be empty...")
|
|
}
|
|
minNode, ok := minNodeInterface.(node)
|
|
if !ok {
|
|
panic("interface conversion error")
|
|
}
|
|
|
|
// if we've already visited these coordinates with this equipment, check
|
|
// that the current time is
|
|
t, ok := eqCoordsToMinDist[minNode.equipped][minNode.coords]
|
|
if ok && t <= minNode.totalTime {
|
|
return node{}
|
|
}
|
|
eqCoordsToMinDist[minNode.equipped][minNode.coords] = minNode.totalTime
|
|
// fmt.Println("coords, region, equipped, time", minNode.coords, minNode.regionType, minNode.equipped, minNode.totalTime)
|
|
// fmt.Println(" Steps", minNode.steps)
|
|
// first check if movement is possible because this requires less time
|
|
// add those steps to the heap
|
|
nodesToTravelTo := getNextNodes(minNode, regionCalculator)
|
|
// fmt.Println("nodes to travel to", nodesToTravelTo)
|
|
for _, n := range nodesToTravelTo {
|
|
heap.Add(n)
|
|
}
|
|
|
|
// then try to change equipment if possible
|
|
newEq := getSwitchableEquipment(minNode)
|
|
|
|
// find travelable-to nodes with new equipment on & append if applicable
|
|
swappedEquipmentNode := node{
|
|
coords: minNode.coords,
|
|
equipped: newEq,
|
|
regionType: minNode.regionType,
|
|
totalTime: minNode.totalTime + 7, // swapping takes 7 minutes
|
|
}
|
|
// in scope of for loop, just to be safe...
|
|
nodesToTravelTo = getNextNodes(swappedEquipmentNode, regionCalculator)
|
|
|
|
for _, n := range nodesToTravelTo {
|
|
heap.Add(n)
|
|
}
|
|
|
|
return minNode
|
|
}
|
|
|
|
var directions = [4][2]int{
|
|
{-1, 0},
|
|
{1, 0},
|
|
{0, -1},
|
|
{0, 1},
|
|
}
|
|
|
|
// returns a slice of nodes that represent traveling in the four directions from
|
|
// the currentNode
|
|
// includes adding 1 to the time
|
|
func getNextNodes(currentNode node, regionCalculator func(x, y int) regionType) []node {
|
|
var nodesToTravelTo []node
|
|
for _, d := range directions {
|
|
nextX := currentNode.coords[0] + d[0]
|
|
nextY := currentNode.coords[1] + d[1]
|
|
nextCoord := [2]int{nextX, nextY}
|
|
nextRegionType := regionCalculator(nextX, nextY)
|
|
|
|
// ensure next region is not out of range
|
|
if nextRegionType != -1 {
|
|
// check if it can be traveled to
|
|
if canTravelTo(nextRegionType, currentNode.equipped) {
|
|
nodesToTravelTo = append(nodesToTravelTo, node{
|
|
coords: nextCoord,
|
|
equipped: currentNode.equipped,
|
|
regionType: nextRegionType,
|
|
totalTime: currentNode.totalTime + 1,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return nodesToTravelTo
|
|
}
|
|
|
|
var mapRegionsToToolsMap = map[regionType]map[equipmentType]bool{
|
|
rocky: map[equipmentType]bool{
|
|
climbingGear: true,
|
|
torch: true,
|
|
},
|
|
wet: map[equipmentType]bool{
|
|
climbingGear: true,
|
|
neither: true,
|
|
},
|
|
narrow: map[equipmentType]bool{
|
|
torch: true,
|
|
neither: true,
|
|
},
|
|
}
|
|
|
|
func canTravelTo(nextRegionType regionType, equippedTool equipmentType) bool {
|
|
return mapRegionsToToolsMap[nextRegionType][equippedTool]
|
|
}
|
|
|
|
func getSwitchableEquipment(n node) equipmentType {
|
|
for i := 0; i < 3; i++ {
|
|
eq := equipmentType(i)
|
|
if eq != n.equipped && mapRegionsToToolsMap[n.regionType][eq] {
|
|
return eq
|
|
}
|
|
}
|
|
|
|
panic("must be one piece of equipment to switch to")
|
|
}
|