mirror of
https://github.com/Threnklyn/advent-of-code-go.git
synced 2026-06-01 01:38:28 +02:00
308 lines
7.7 KiB
Go
308 lines
7.7 KiB
Go
package main
|
|
|
|
import (
|
|
_ "embed"
|
|
"flag"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/alexchao26/advent-of-code-go/data-structures/heap"
|
|
"github.com/alexchao26/advent-of-code-go/mathy"
|
|
"github.com/alexchao26/advent-of-code-go/util"
|
|
)
|
|
|
|
//go:embed input.txt
|
|
var input string
|
|
|
|
func init() {
|
|
// do this in init (not main) so test file has same input
|
|
input = strings.TrimRight(input, "\n")
|
|
if len(input) == 0 {
|
|
panic("empty input.txt file")
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
var part int
|
|
flag.IntVar(&part, "part", 1, "part 1 or 2")
|
|
flag.Parse()
|
|
fmt.Println("Running part", part)
|
|
|
|
ans := amphipodDay23(input, part)
|
|
util.CopyToClipboard(fmt.Sprintf("%v", ans))
|
|
fmt.Println("Output:", ans)
|
|
}
|
|
|
|
var roomCoordToWantCharPart1 = map[[2]int]string{
|
|
{2, 3}: "A", {3, 3}: "A",
|
|
{2, 5}: "B", {3, 5}: "B",
|
|
{2, 7}: "C", {3, 7}: "C",
|
|
{2, 9}: "D", {3, 9}: "D",
|
|
}
|
|
var roomCoordToWantCharPart2 = map[[2]int]string{
|
|
{2, 3}: "A", {3, 3}: "A", {4, 3}: "A", {5, 3}: "A",
|
|
{2, 5}: "B", {3, 5}: "B", {4, 5}: "B", {5, 5}: "B",
|
|
{2, 7}: "C", {3, 7}: "C", {4, 7}: "C", {5, 7}: "C",
|
|
{2, 9}: "D", {3, 9}: "D", {4, 9}: "D", {5, 9}: "D",
|
|
}
|
|
|
|
func amphipodDay23(input string, part int) int {
|
|
start := parseInput(input)
|
|
|
|
roomCoordToWantChar := roomCoordToWantCharPart1
|
|
if part == 2 {
|
|
roomCoordToWantChar = roomCoordToWantCharPart2
|
|
|
|
// update the grid with the 2 new rows, move old ones down
|
|
start.grid = append(start.grid, nil, nil)
|
|
start.grid[6] = start.grid[4]
|
|
start.grid[5] = start.grid[3]
|
|
|
|
start.grid[3] = strings.Split(" #D#C#B#A# ", "")
|
|
start.grid[4] = strings.Split(" #D#B#A#C# ", "")
|
|
}
|
|
|
|
minHeap := heap.NewMinHeap()
|
|
|
|
minHeap.Add(start)
|
|
seenGrids := map[string]bool{}
|
|
for minHeap.Length() > 0 {
|
|
front := minHeap.Remove().(*state)
|
|
|
|
key := fmt.Sprint(front.grid)
|
|
if seenGrids[key] {
|
|
continue
|
|
}
|
|
seenGrids[key] = true
|
|
|
|
if front.allDone(roomCoordToWantChar) {
|
|
return front.energyUsed
|
|
}
|
|
|
|
unsettledCoords := front.getUnsettledCoords(roomCoordToWantChar)
|
|
for _, unsettledCoord := range unsettledCoords {
|
|
ur, uc := unsettledCoord[0], unsettledCoord[1]
|
|
nextMoves := front.getNextPossibleMoves(unsettledCoord, roomCoordToWantChar)
|
|
for _, nextCoord := range nextMoves {
|
|
nr, nc := nextCoord[0], nextCoord[1]
|
|
if front.grid[nr][nc] != "." {
|
|
panic(fmt.Sprintf("should only be moving to walkable spaces, got %q at %d,%d", front.grid[nr][nc], nr, nc))
|
|
}
|
|
|
|
cp := front.copy()
|
|
// add the energy that will be used, swap the two coords
|
|
cp.energyUsed += calcEnergy(cp.grid[ur][uc], unsettledCoord, nextCoord)
|
|
cp.path += fmt.Sprintf("%s%v->%v{%d},", front.grid[ur][uc], unsettledCoord, nextCoord, cp.energyUsed)
|
|
cp.grid[nr][nc], cp.grid[ur][uc] = cp.grid[ur][uc], cp.grid[nr][nc]
|
|
|
|
// add it to the min heap
|
|
minHeap.Add(cp)
|
|
}
|
|
}
|
|
}
|
|
|
|
panic("should return from loop")
|
|
}
|
|
|
|
type state struct {
|
|
grid [][]string
|
|
energyUsed int
|
|
path string // for debugging
|
|
}
|
|
|
|
func parseInput(input string) *state {
|
|
grid := [][]string{}
|
|
for _, line := range strings.Split(input, "\n") {
|
|
grid = append(grid, strings.Split(line, ""))
|
|
}
|
|
return &state{
|
|
grid: grid,
|
|
}
|
|
}
|
|
|
|
// Value is to implement the heap.heapNode interface so I can dump states into a Min Heap
|
|
func (s *state) Value() int {
|
|
return s.energyUsed
|
|
}
|
|
|
|
func (s *state) String() string {
|
|
var sb strings.Builder
|
|
for _, row := range s.grid {
|
|
for _, unsettledChar := range row {
|
|
sb.WriteString(unsettledChar)
|
|
}
|
|
sb.WriteRune('\n')
|
|
}
|
|
|
|
sb.WriteString(fmt.Sprintf("nrg: %d, ,path: %s\n", s.energyUsed, s.path))
|
|
|
|
return sb.String()
|
|
}
|
|
|
|
// copy method to generate copies to make future heap nodes
|
|
func (s *state) copy() *state {
|
|
cp := state{
|
|
grid: make([][]string, len(s.grid)),
|
|
energyUsed: s.energyUsed,
|
|
path: s.path,
|
|
}
|
|
|
|
// need to directly copy grid or else underlying arrays will be the same & interfere
|
|
for i := range cp.grid {
|
|
cp.grid[i] = make([]string, len(s.grid[i]))
|
|
copy(cp.grid[i], s.grid[i])
|
|
}
|
|
|
|
return &cp
|
|
}
|
|
|
|
func (s *state) allDone(roomCoordToWantChar map[[2]int]string) bool {
|
|
for coord, want := range roomCoordToWantChar {
|
|
if s.grid[coord[0]][coord[1]] != want {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *state) getUnsettledCoords(roomCoordToWantChar map[[2]int]string) [][2]int {
|
|
var unsettled [][2]int
|
|
// check entire hallway
|
|
for col := 1; col < len(s.grid[0]); col++ {
|
|
if strings.Contains("ABCD", s.grid[1][col]) {
|
|
unsettled = append(unsettled, [2]int{1, col})
|
|
}
|
|
}
|
|
|
|
for _, col := range []int{3, 5, 7, 9} {
|
|
roomFullFromBack := true
|
|
for row := len(s.grid) - 2; row >= 2; row-- {
|
|
coord := [2]int{row, col}
|
|
wantChar := roomCoordToWantChar[coord]
|
|
gotChar := s.grid[row][col]
|
|
if gotChar != "." {
|
|
if gotChar != wantChar {
|
|
roomFullFromBack = false
|
|
unsettled = append(unsettled, coord)
|
|
} else if gotChar == wantChar && !roomFullFromBack {
|
|
// need to get out of the way of someone in the wrong room
|
|
unsettled = append(unsettled, coord)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return unsettled
|
|
}
|
|
|
|
// cannot stop in front of a room, still applicable for part2
|
|
var coordsInFrontOfRooms = map[[2]int]bool{
|
|
{1, 3}: true,
|
|
{1, 5}: true,
|
|
{1, 7}: true,
|
|
{1, 9}: true,
|
|
}
|
|
|
|
func isInHallway(coord [2]int) bool {
|
|
return coord[0] == 1
|
|
}
|
|
|
|
func (s *state) getNextPossibleMoves(unsettledCoord [2]int, roomCoordToWantChar map[[2]int]string) [][2]int {
|
|
// get all the eligible locations for this coord to go to
|
|
unsettledChar := s.grid[unsettledCoord[0]][unsettledCoord[1]]
|
|
|
|
if !strings.Contains("ABCD", unsettledChar) {
|
|
panic("unexpected character to get next moves for " + unsettledChar)
|
|
}
|
|
|
|
var possible [][2]int
|
|
|
|
startedInHallway := isInHallway(unsettledCoord)
|
|
|
|
queue := [][2]int{unsettledCoord}
|
|
seen := map[[2]int]bool{}
|
|
for len(queue) > 0 {
|
|
front := queue[0]
|
|
queue = queue[1:]
|
|
|
|
if seen[front] {
|
|
continue
|
|
}
|
|
seen[front] = true
|
|
|
|
if front != unsettledCoord {
|
|
// is not a coord in front of a room
|
|
if !coordsInFrontOfRooms[front] {
|
|
wantChar, isRoomCoord := roomCoordToWantChar[front]
|
|
// if NOT in a room, append it
|
|
if !isRoomCoord {
|
|
// ONLY add a hallway if it started in a room bc of rule 3
|
|
if !startedInHallway {
|
|
possible = append(possible, front)
|
|
}
|
|
} else if wantChar == unsettledChar {
|
|
// found the correct room
|
|
// check if there is a deeper part of the room (aka lower)
|
|
|
|
// if there is a "stuck" amphipod deeper in the room, cannot stop here
|
|
// if not deepest empty coord, cannot stop here
|
|
// in both cases walking further is handles all cases, whether that's
|
|
// to walk further in or out of the room
|
|
isStuckAmphipod := false
|
|
roomHasDeeperOpenSpaces := false
|
|
for r := front[0] + 1; r < len(s.grid)-1; r++ {
|
|
char := s.grid[r][front[1]]
|
|
if char == "." {
|
|
roomHasDeeperOpenSpaces = true
|
|
}
|
|
if char != "." && char != unsettledChar {
|
|
isStuckAmphipod = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !roomHasDeeperOpenSpaces && !isStuckAmphipod {
|
|
possible = append(possible, front)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, d := range [][2]int{
|
|
// up down left right
|
|
{-1, 0},
|
|
{1, 0},
|
|
{0, -1},
|
|
{0, 1},
|
|
} {
|
|
// do not need to check in range because the entire walkable area is surrounded by walls
|
|
next := [2]int{front[0] + d[0], front[1] + d[1]}
|
|
if s.grid[next[0]][next[1]] == "." {
|
|
// add to queue to keep walking regardless of whether or not it gets added to the possible slice
|
|
queue = append(queue, next)
|
|
}
|
|
}
|
|
}
|
|
|
|
return possible
|
|
}
|
|
|
|
func calcEnergy(char string, start, end [2]int) int {
|
|
// start with cols distance
|
|
dist := mathy.AbsInt(end[1] - start[1])
|
|
// add distance to hallway for start and end?
|
|
dist += start[0] - 1
|
|
dist += end[0] - 1
|
|
|
|
energyPerType := map[string]int{
|
|
"A": 1,
|
|
"B": 10,
|
|
"C": 100,
|
|
"D": 1000,
|
|
}
|
|
|
|
if _, ok := energyPerType[char]; !ok {
|
|
panic(char + " should not call calcEnergy()")
|
|
}
|
|
return energyPerType[char] * dist
|
|
}
|