From dd88e03f2ce99ce3f784208de59a65c5e3654af7 Mon Sep 17 00:00:00 2001 From: alexchao26 Date: Sat, 25 Dec 2021 22:34:04 -0500 Subject: [PATCH] 2021 day23, its slow... but it works :) lots of small bugs that got hunted via tests --- .gitignore | 3 + 2021/day23/main.go | 210 ++++++++++++++++------------------- 2021/day23/main_test.go | 235 ++++++++++++++++++++++++++++------------ 3 files changed, 265 insertions(+), 183 deletions(-) diff --git a/.gitignore b/.gitignore index 353bb48..9f243f0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ input*.txt !scripts/skeleton/tmpls/input.txt prompt.md prompt.txt + +// ignore files that are prefixed with an underscore +_* \ No newline at end of file diff --git a/2021/day23/main.go b/2021/day23/main.go index 9af9365..4e6452b 100644 --- a/2021/day23/main.go +++ b/2021/day23/main.go @@ -28,20 +28,40 @@ func main() { flag.Parse() fmt.Println("Running part", part) - if part == 1 { - ans := part1(input) - util.CopyToClipboard(fmt.Sprintf("%v", ans)) - fmt.Println("Output:", ans) - } else { - ans := part2(input) - util.CopyToClipboard(fmt.Sprintf("%v", ans)) - fmt.Println("Output:", ans) - } + ans := amphipodDay23(input, part) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) } -func part1(input string) int { +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) @@ -53,22 +73,16 @@ func part1(input string) int { if seenGrids[key] { continue } - fmt.Println(minHeap.Length(), "\n", front) seenGrids[key] = true - if front.allDone() { + if front.allDone(roomCoordToWantChar) { return front.energyUsed } - unsettledCoords := front.getUnsettledCoords() + unsettledCoords := front.getUnsettledCoords(roomCoordToWantChar) for _, unsettledCoord := range unsettledCoords { - // // do not try to move the last one that was moved, otherwise it'll infinite loop - // if front.coordOfLastMoved == unsettledCoord { - // continue - // } - ur, uc := unsettledCoord[0], unsettledCoord[1] - nextMoves := front.getNextPossibleMoves(unsettledCoord) + nextMoves := front.getNextPossibleMoves(unsettledCoord, roomCoordToWantChar) for _, nextCoord := range nextMoves { nr, nc := nextCoord[0], nextCoord[1] if front.grid[nr][nc] != "." { @@ -80,7 +94,6 @@ func part1(input string) int { 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] - cp.coordOfLastMoved = nextCoord // add it to the min heap minHeap.Add(cp) @@ -88,23 +101,26 @@ func part1(input string) int { } } - // 10901 TOO LOW - panic("should return from loop") } -func part2(input string) int { - return 0 -} - type state struct { - grid [][]string - coordOfLastMoved [2]int // store so you don't try to move the same one twice in a row - energyUsed int - path string + grid [][]string + energyUsed int + path string // for debugging } -// Value is to implement the heap.Node interface so I can dump states into a Min Heap +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 } @@ -118,7 +134,7 @@ func (s *state) String() string { sb.WriteRune('\n') } - sb.WriteString(fmt.Sprintf("nrg: %d, last_moved: %v, path: %s\n", s.energyUsed, s.coordOfLastMoved, s.path)) + sb.WriteString(fmt.Sprintf("nrg: %d, ,path: %s\n", s.energyUsed, s.path)) return sb.String() } @@ -126,10 +142,9 @@ func (s *state) String() string { // copy method to generate copies to make future heap nodes func (s *state) copy() *state { cp := state{ - grid: make([][]string, len(s.grid)), - coordOfLastMoved: s.coordOfLastMoved, - energyUsed: s.energyUsed, - path: s.path, + 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 @@ -141,14 +156,7 @@ func (s *state) copy() *state { return &cp } -var roomCoordToWantChar = 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", -} - -func (s *state) allDone() bool { +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 @@ -157,45 +165,36 @@ func (s *state) allDone() bool { return true } -func (s *state) getUnsettledCoords() [][2]int { +func (s *state) getUnsettledCoords(roomCoordToWantChar map[[2]int]string) [][2]int { var unsettled [][2]int - for r, row := range s.grid { - for c, v := range row { - // iterate through the entire grid, for every letter "/[A-D]/" - if strings.Contains("ABCD", v) { - // add it to the unsettled list - coord := [2]int{r, c} - // IF not in coords map - if want, ok := roomCoordToWantChar[coord]; !ok { - unsettled = append(unsettled, coord) - continue // these are all probably unnecessary but helpful for my brain - } else { - // IF in coords map but not matching the wantChar - if want != v { - unsettled = append(unsettled, coord) - continue - } else { - // IF it matches wantChar but the cell below is - // ALSO in coords->want map AND it is the wrong want unsettledChar - // this means that it is in the right place but needs to get out - // of the way for a cell below - below := [2]int{r + 1, c} - wantBelow, ok := roomCoordToWantChar[below] - // ok means that it is a "room" cell, not wall - if ok && wantBelow != s.grid[r+1][c] { - unsettled = append(unsettled, coord) - continue - } - } - } + // 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 +// cannot stop in front of a room, still applicable for part2 var coordsInFrontOfRooms = map[[2]int]bool{ {1, 3}: true, {1, 5}: true, @@ -207,7 +206,7 @@ func isInHallway(coord [2]int) bool { return coord[0] == 1 } -func (s *state) getNextPossibleMoves(unsettledCoord [2]int) [][2]int { +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]] @@ -215,10 +214,10 @@ func (s *state) getNextPossibleMoves(unsettledCoord [2]int) [][2]int { panic("unexpected character to get next moves for " + unsettledChar) } - startedInHallway := isInHallway(unsettledCoord) - // fmt.Println(unsettledChar, unsettledCoord, "in hallway", startedInHallway) var possible [][2]int + startedInHallway := isInHallway(unsettledCoord) + queue := [][2]int{unsettledCoord} seen := map[[2]int]bool{} for len(queue) > 0 { @@ -243,26 +242,26 @@ func (s *state) getNextPossibleMoves(unsettledCoord [2]int) [][2]int { } else if wantChar == unsettledChar { // found the correct room // check if there is a deeper part of the room (aka lower) - maybeLower := [2]int{front[0] + 1, front[1]} - if _, ok := roomCoordToWantChar[maybeLower]; ok { - lowerChar := s.grid[maybeLower[0]][maybeLower[1]] - if lowerChar == "." { - possible = append(possible, maybeLower) - // can only go deeper into the room so just kill the traverse here - continue + + // 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 lower char is the same, then can move into the front of the room - if lowerChar == unsettledChar { - possible = append(possible, front) - // no where else to go, so just continue and end this iteration - continue + if char != "." && char != unsettledChar { + isStuckAmphipod = true + break } - } else { - // otherwise already deep part of the room, append it - // ?probably unreachable code - fmt.Println("unreachable code in else block?") + } + + if !roomHasDeeperOpenSpaces && !isStuckAmphipod { possible = append(possible, front) - continue } } } @@ -306,24 +305,3 @@ func calcEnergy(char string, start, end [2]int) int { } return energyPerType[char] * dist } - -func parseInput(input string) *state { - grid := [][]string{} - for _, line := range strings.Split(input, "\n") { - grid = append(grid, strings.Split(line, "")) - } - - // // uncomment to check if coordToWantChars are correct... - // for c, unsettledChar := range roomCoordToWantChar { - // grid[c[0]][c[1]] = unsettledChar - // } - // st := state{grid: grid} - // fmt.Println(st.String()) - // if !st.allDone() { - // panic("state.allDone() should be true ") - // } - - return &state{ - grid: grid, - } -} diff --git a/2021/day23/main_test.go b/2021/day23/main_test.go index d80cacc..7d00aae 100644 --- a/2021/day23/main_test.go +++ b/2021/day23/main_test.go @@ -2,7 +2,9 @@ package main import ( _ "embed" + "fmt" "reflect" + "strings" "testing" ) @@ -18,96 +20,94 @@ var doneInput = `############# #A#B#C#D# ######### ` -func Test_part1(t *testing.T) { +func Test_amphipodDay23(t *testing.T) { tests := []struct { name string input string + part int want int }{ { - name: "example", + name: "part1 example", input: example, + part: 1, want: 12521, }, { - name: "simple", + name: "part1 simple", input: `############# #.A.........# ###.#B#C#D### - #A#B#C#D# + #A#B#C#D# ######### `, + part: 1, want: 2, }, { - name: "simple: A then B", + name: "part1 simple: A then B", input: `############# #BA.........# ###.#.#C#D### - #A#B#C#D# + #A#B#C#D# ######### `, + part: 1, want: 52, }, { // NOTE found a bug! A moving from a deep room to another room is calculating energy // NOTE as if it is walking through the wall - name: "reversed B room", // B has to get out of A's way first + name: "part1 reversed B room", // B has to get out of A's way first input: `############# #.B.........# ###.#B#C#D### - #A#A#C#D# + #A#A#C#D# ######### `, + part: 1, want: 95, }, { - name: "some shuffling", + name: "part1 some shuffling", input: `############# #...........# ###A#B#C#D### - #A#C#B#D# + #A#C#B#D# ######### `, + part: 1, want: 1120, }, { - name: "doneInput", + name: "part1 doneInput", input: doneInput, + part: 1, want: 0, }, { - name: "actual", + name: "part1 actual", input: input, + part: 1, want: 15299, }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := part1(tt.input); got != tt.want { - t.Errorf("part1() = %v, want %v", got, tt.want) - } - }) - } -} -func Test_part2(t *testing.T) { - tests := []struct { - name string - input string - want int - }{ { - name: "example", + name: "example part 2", input: example, + part: 2, want: 44169, }, - // { - // name: "actual", - // input: input, - // want: 0, - // }, + { + name: "part2 actual", + input: input, + part: 2, + want: 47193, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := part2(tt.input); got != tt.want { - t.Errorf("part2() = %v, want %v", got, tt.want) + if testing.Short() && strings.Contains(tt.name, "actual") { + t.Skip(fmt.Sprintf("skipping %q in -short mode", tt.name)) + } + if got := amphipodDay23(tt.input, tt.part); got != tt.want { + t.Errorf("amphipodDay23() = %v, want %v", got, tt.want) } }) } @@ -115,14 +115,16 @@ func Test_part2(t *testing.T) { func Test_state_getUnsettledCoords(t *testing.T) { tests := []struct { - name string - input string - want [][2]int + name string + input string + roomCoordToWantChar map[[2]int]string + want [][2]int }{ { - name: "already done", - input: doneInput, - want: nil, + name: "already done", + input: doneInput, + roomCoordToWantChar: roomCoordToWantCharPart1, + want: nil, }, { name: "example - 2 \"done\" amphipods", @@ -134,22 +136,41 @@ func Test_state_getUnsettledCoords(t *testing.T) { #A#D#C#A# ######### */ - want: [][2]int{{2, 3}, {2, 5}, {2, 7}, {2, 9}, {3, 5}, {3, 9}}, + roomCoordToWantChar: roomCoordToWantCharPart1, + want: [][2]int{{2, 3}, {3, 5}, {2, 5}, {2, 7}, {3, 9}, {2, 9}}, }, { name: "four unsettled coords", - input: ` ############# + input: `############# #AB.....D..D# ###.#.#C#.### #A#B#C#.# - ######### `, - want: [][2]int{{1, 1}, {1, 2}, {1, 8}, {1, 11}}, + ######### `, + roomCoordToWantChar: roomCoordToWantCharPart1, + want: [][2]int{{1, 1}, {1, 2}, {1, 8}, {1, 11}}, + }, + { + name: "part2 test", + input: `############# +#AB.....D..D# +###.#.#C#.### + #A#A#C#.# + #B#B#C#D# + #A#B#C#D# + ######### `, + roomCoordToWantChar: roomCoordToWantCharPart2, + want: [][2]int{ + // hallway + {1, 1}, {1, 2}, {1, 8}, {1, 11}, + {4, 3}, {3, 3}, // A room + {3, 5}, // B room + }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := parseInput(tt.input) - if got := s.getUnsettledCoords(); !reflect.DeepEqual(got, tt.want) { + if got := s.getUnsettledCoords(tt.roomCoordToWantChar); !reflect.DeepEqual(got, tt.want) { t.Errorf("state.getUnsettledCoords() = %v, want %v", got, tt.want) } }) @@ -158,10 +179,11 @@ func Test_state_getUnsettledCoords(t *testing.T) { func Test_state_getNextPossibleMoves(t *testing.T) { tests := []struct { - name string - input string - unsettledCoord [2]int - want [][2]int + name string + input string + unsettledCoord [2]int + roomCoordToWantChar map[[2]int]string + want [][2]int }{ { name: "example, deep room is stuck", @@ -173,46 +195,116 @@ func Test_state_getNextPossibleMoves(t *testing.T) { #A#D#C#A# ######### */ - unsettledCoord: [2]int{3, 3}, // DEEP room in A slot - want: nil, + unsettledCoord: [2]int{3, 3}, // DEEP room in A slot + roomCoordToWantChar: roomCoordToWantCharPart1, + want: nil, }, { - name: "example-frontA", - input: example, - unsettledCoord: [2]int{2, 3}, // FRONT room in A slot - want: [][2]int{{1, 2}, {1, 4}, {1, 1}, {1, 6}, {1, 8}, {1, 10}, {1, 11}}, + name: "example-frontA", + input: example, + unsettledCoord: [2]int{2, 3}, // FRONT room in A slot + roomCoordToWantChar: roomCoordToWantCharPart1, + want: [][2]int{{1, 2}, {1, 4}, {1, 1}, {1, 6}, {1, 8}, {1, 10}, {1, 11}}, }, { - name: "example-deepA-should be stuck", - input: example, - unsettledCoord: [2]int{3, 3}, // FRONT room in A slot - want: nil, + name: "example-deepA-should be stuck", + input: example, + unsettledCoord: [2]int{3, 3}, // FRONT room in A slot + roomCoordToWantChar: roomCoordToWantCharPart1, + want: nil, }, { name: "hallways to rooms ONLY", - input: ` ############# + input: `############# #AB.....D..D# ###.#.#C#.### #A#B#C#.# ######### `, - unsettledCoord: [2]int{1, 2}, - want: [][2]int{{2, 5}}, + unsettledCoord: [2]int{1, 2}, + roomCoordToWantChar: roomCoordToWantCharPart1, + want: [][2]int{{2, 5}}, }, { name: "hallway to DEEP room", - input: ` ############# + input: `############# #AB.....D..D# ###.#.#C#.### #A#B#C#.# ######### `, - unsettledCoord: [2]int{1, 8}, - want: [][2]int{{3, 9}}, + unsettledCoord: [2]int{1, 8}, + roomCoordToWantChar: roomCoordToWantCharPart1, + want: [][2]int{{3, 9}}, + }, + { + name: "part2 simple", + input: `############# +#B..........# +###A#.#C#D### + #A#B#C#D# + #A#B#C#D# + #A#B#C#D# + ######### `, + unsettledCoord: [2]int{1, 1}, + roomCoordToWantChar: roomCoordToWantCharPart2, + want: [][2]int{{2, 5}}, + }, + { + name: "part2 back of room", + input: `############# +#B......B.BB# +###A#.#C#D### + #A#.#C#D# + #A#.#C#D# + #A#.#C#D# + ######### `, + unsettledCoord: [2]int{1, 1}, + roomCoordToWantChar: roomCoordToWantCharPart2, + want: [][2]int{{5, 5}}, + }, + { + name: "part2 back of room", + input: `############# +#B......B..B# +###A#.#C#D### + #A#.#C#D# + #A#.#C#D# + #A#B#C#D# + ######### `, + unsettledCoord: [2]int{1, 8}, + roomCoordToWantChar: roomCoordToWantCharPart2, + want: [][2]int{{4, 5}}, + }, + { + name: "part2 bug moving C from B room to C room", + input: `############# +#AA.....B.BD# +###B#.#.#.### + #D#C#.#.# + #D#B#C#C# + #A#D#C#A# + ######### `, + unsettledCoord: [2]int{3, 5}, + roomCoordToWantChar: roomCoordToWantCharPart2, + want: [][2]int{{1, 4}, {1, 6}, {3, 7}}, + }, + { + name: "part2 bug moving B out of B room bc of a blocked D", + input: `############# +#AA.....B.BD# +###B#.#.#.### + #D#.#C#.# + #D#B#C#C# + #A#D#C#A# + ######### `, + unsettledCoord: [2]int{4, 5}, + roomCoordToWantChar: roomCoordToWantCharPart2, + want: [][2]int{{1, 4}, {1, 6}}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s := parseInput(tt.input) - if got := s.getNextPossibleMoves(tt.unsettledCoord); !reflect.DeepEqual(got, tt.want) { + if got := s.getNextPossibleMoves(tt.unsettledCoord, tt.roomCoordToWantChar); !reflect.DeepEqual(got, tt.want) { t.Errorf("state.getNextPossibleMoves() = %v, want %v", got, tt.want) } }) @@ -266,6 +358,15 @@ func Test_calcEnergy(t *testing.T) { }, want: 600, }, + { + name: "part2 C", + args: args{ + char: "C", + start: [2]int{5, 11}, + end: [2]int{3, 7}, + }, + want: 1000, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {