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") }