aoc/2021/23/main.go
2021-12-23 21:55:57 +01:00

501 lines
9.1 KiB
Go

package main
import (
aoc "AoC/2021/common"
"bufio"
"fmt"
"regexp"
)
type Amphipod int
const (
Empty Amphipod = 0
Amber = 1
Bronze = 10
Copper = 100
Desert = 1000
)
func GetAmphipod(s string) Amphipod {
switch s {
case "A":
return Amber
case "B":
return Bronze
case "C":
return Copper
case "D":
return Desert
default:
panic("Unknown type of Amphipod")
}
}
func (a Amphipod) Letter() string {
switch a {
case Amber:
return "A"
case Bronze:
return "B"
case Copper:
return "C"
case Desert:
return "D"
default:
return "."
}
}
func (a Amphipod) Destination() (int, error) {
switch a {
case Amber:
return 0, nil
case Bronze:
return 1, nil
case Copper:
return 2, nil
case Desert:
return 3, nil
}
return -1, fmt.Errorf("Amphipod has no destination")
}
type Room struct {
position int
occupants [4]Amphipod
}
func (r Room) ValidDesination(a Amphipod) (int, error) {
depth := -1
for i := range r.occupants {
if r.occupants[i] == Empty {
depth++
} else if r.occupants[i] != a {
return -1, fmt.Errorf("Invalid destination")
}
}
return depth, nil
}
func (r Room) InCorrectRoom(index int, a Amphipod) bool {
b := Empty
switch index {
case 0:
b = Amber
case 1:
b = Bronze
case 2:
b = Copper
case 3:
b = Desert
default:
return false
}
return a == b
}
func (r Room) IsAmphipodDone(index int, depth int, a Amphipod) bool {
if !r.InCorrectRoom(index, a) {
return false
}
// Check if the Amphipods below are correct
for i := depth; i < len(r.occupants); i++ {
if r.occupants[i] != a {
return false
}
}
return true
}
func (r Room) Solved(index int) bool {
for i := range r.occupants {
if !r.InCorrectRoom(index, r.occupants[i]) {
return false
}
}
return true
}
func (r Room) Top() (Amphipod, int) {
for i := range r.occupants {
if r.occupants[i] != Empty {
return r.occupants[i], i
}
}
return r.occupants[0], 0
}
// Create a new room with the top entry removed
func (r Room) Pop() (Room, int) {
for i := range r.occupants {
if r.occupants[i] == Empty {
continue
}
r.occupants[i] = Empty
return r, i
}
return r, 0
}
type Puzzle struct {
corridor [11]Amphipod
rooms [4]Room
}
func (p Puzzle) CheckCorridor(start int, end int) (bool, int) {
cost := 0
for k := start; k <= end; k++ {
cost++
if p.corridor[k] != Empty {
// If there is an non empty tile along the way we move on to the next amphipod
// We do not have to check the other rooms as there can only ever be at most one valid room
return false, 0
}
}
return true, cost
}
func (p Puzzle) Print() {
fmt.Println("#############")
fmt.Print("#")
for _, a := range p.corridor {
fmt.Printf("%s", a.Letter())
}
fmt.Print("#\n")
for i := 0; i < len(p.rooms[0].occupants); i++ {
filler := " "
if i == 0 {
filler = "##"
}
fmt.Printf("%s", filler)
for _, r := range p.rooms {
fmt.Printf("#%s", r.occupants[i].Letter())
}
fmt.Printf("#%s\n", filler)
}
fmt.Println(" ######### ")
}
func (p Puzzle) Solved() bool {
for i := 0; i < len(p.rooms[0].occupants); i++ {
if p.rooms[0].occupants[i] != Amber {
return false
}
if p.rooms[1].occupants[i] != Bronze {
return false
}
if p.rooms[2].occupants[i] != Copper {
return false
}
if p.rooms[3].occupants[i] != Desert {
return false
}
}
return true
}
func (p Puzzle) Solve(iteration int) (bool, int) {
// First check if we have already calculated the state
// if cost, ok := cache[p]; ok {
// return cost
// }
// fmt.Println("Depth:", iteration)
// p.Print()
cost := 0
// If possible move Amphipod to destination room
resolve:
for true {
// fmt.Println("Checking: ROOM => ROOM movement")
for i, r := range p.rooms {
// Check if the room is already solved
if r.Solved(i) {
continue
}
a, adepth := r.Top()
if a == Empty {
continue
}
if r.IsAmphipodDone(i, adepth, a) {
continue
}
d, _ := a.Destination()
if edepth, err := p.rooms[d].ValidDesination(a); err == nil {
// The room is free and valid
start := r.position
end := p.rooms[d].position
if start > end {
temp := start
start = end
end = temp
}
free, potentialCost := p.CheckCorridor(start, end)
if !free {
continue
}
// fmt.Println(potentialCost, r.position, p.rooms[d].position)
// fmt.Printf("\tMoving %v from %d to %d\n", a.Letter(), i, d)
potentialCost += edepth
var sdepth int
p.rooms[i], sdepth = r.Pop()
p.rooms[d].occupants[edepth] = a
potentialCost += sdepth
potentialCost += 1
// fmt.Printf("\t\t%d %d\n", sdepth, edepth)
potentialCost *= int(a)
// fmt.Printf("\t\tCost: %d\n", potentialCost)
cost += potentialCost
continue resolve
}
}
// fmt.Println("Checking: CORRIDOR => ROOM movement")
// For every space in the corridor we check if there is a Amphipod that could move
for i, a := range p.corridor {
if a == Empty {
continue
}
// Get the ID of the destination room
j, _ := a.Destination()
// Check if the destination room is free
if depth, err := p.rooms[j].ValidDesination(a); err == nil {
// Get the start and end position
start := i+1
end := p.rooms[j].position
if start > end {
temp := start-2
start = end
end = temp
}
free, potentialCost := p.CheckCorridor(start, end)
if !free {
continue
}
// We need to add one since we start with a move to the left or the right
potentialCost += depth + 1
potentialCost *= int(a)
// fmt.Printf("\tMoving %s from %d to %d\n", a.Letter(), i, j)
// fmt.Printf("\t\tCost: %d\n", potentialCost)
// Update state
p.corridor[i] = Empty
p.rooms[j].occupants[depth] = a
cost += potentialCost
// Restart looping over all spaces in the corridor as another amphipod might now be able to move
continue resolve
}
}
break
}
if p.Solved() {
return true, cost
}
// Generate future states
minCost := -1
for i, r := range p.rooms {
// Check if the room is already solved
if r.Solved(i) {
continue
}
// Get the top amphipod in the room
a, adepth := r.Top()
// The room is empty, move on to the next room
if a == Empty {
continue
}
if r.IsAmphipodDone(i, adepth, a) {
continue
}
// Move the amphipod to the corridor
outer:
for j, c := range p.corridor {
// The amphipod can not stop in from of the room
// We also can not stop in another amphipod
if c != Empty {
continue
}
for _, rr := range p.rooms {
if j == rr.position {
continue outer
}
}
// Get the start and end point
start := r.position
end := j
if start > end {
temp := start
start = end
end = temp
}
free, potentialCost := p.CheckCorridor(start, end)
if !free {
continue
}
// Create the new state
np := p
nr, depth := r.Pop()
np.rooms[i] = nr
np.corridor[j] = a
solved, nextCost := np.Solve(iteration+1)
if solved {
nextCost += (potentialCost + depth) * int(a)
if nextCost < minCost || minCost == -1 {
minCost = nextCost
}
}
}
}
if minCost == -1 {
return false, 10000000
}
return true, cost + minCost
}
func NewPuzzle1(input *bufio.Scanner) Puzzle {
input.Scan()
input.Scan()
r := regexp.MustCompile("[A-Z]")
var p Puzzle
for i := 0; i < 2; i++ {
input.Scan()
line := r.FindAllString(input.Text(), 4)
for j, a := range line {
p.rooms[j].occupants[i] = GetAmphipod(a)
}
}
// Pad the bottom out so it is compatible with part 2
p.rooms[0].occupants[2] = Amber
p.rooms[0].occupants[3] = Amber
p.rooms[1].occupants[2] = Bronze
p.rooms[1].occupants[3] = Bronze
p.rooms[2].occupants[2] = Copper
p.rooms[2].occupants[3] = Copper
p.rooms[3].occupants[2] = Desert
p.rooms[3].occupants[3] = Desert
for i := range p.rooms {
p.rooms[i].position = 2 + 2*i
}
return p
}
func NewPuzzle2(input *bufio.Scanner) Puzzle {
input.Scan()
input.Scan()
r := regexp.MustCompile("[A-Z]")
var p Puzzle
for i := 0; i < 2; i++ {
input.Scan()
line := r.FindAllString(input.Text(), 4)
for j, a := range line {
p.rooms[j].occupants[i*3] = GetAmphipod(a)
}
}
// Fill in the middle lines
p.rooms[0].occupants[1] = Desert
p.rooms[0].occupants[2] = Desert
p.rooms[1].occupants[1] = Copper
p.rooms[1].occupants[2] = Bronze
p.rooms[2].occupants[1] = Bronze
p.rooms[2].occupants[2] = Amber
p.rooms[3].occupants[1] = Amber
p.rooms[3].occupants[2] = Copper
for i := range p.rooms {
p.rooms[i].position = 2 + 2*i
}
return p
}
// The code to solve this is a complete mess
// BUT it works, and that is all that matters for right now
// Should really write an improved version of this though...
func main() {
challenge := aoc.New(2021, 23)
challenge.Test(`#############
#...........#
###B#C#B#D###
#A#D#C#A#
#########
`, []int{12521, 44169})
challenge.Solution(1, func (input *bufio.Scanner) int {
puzzle := NewPuzzle1(input)
_, cost := puzzle.Solve(0)
return cost
})
challenge.Solution(2, func (input *bufio.Scanner) int {
puzzle := NewPuzzle2(input)
_, cost := puzzle.Solve(0)
return cost
})
}