mirror of
https://github.com/Threnklyn/advent-of-code-go.git
synced 2026-06-07 04:33:31 +02:00
2016-day11: one of the hardest aoc problems yet... essentially a bfs next states
This commit is contained in:
@@ -0,0 +1,284 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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)
|
||||||
|
|
||||||
|
ans := rtgHellDay(util.ReadFile("./input.txt"), part)
|
||||||
|
fmt.Println("Output:", ans)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rtgHellDay(input string, part int) int {
|
||||||
|
currentState := newInitialState(input)
|
||||||
|
|
||||||
|
if part == 2 {
|
||||||
|
currentState.floors[0] = append(currentState.floors[0],
|
||||||
|
halves{isChip: false, material: "elerium"},
|
||||||
|
halves{isChip: true, material: "elerium"},
|
||||||
|
halves{isChip: false, material: "dilithium"},
|
||||||
|
halves{isChip: true, material: "dilithium"},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
queue := []state{currentState}
|
||||||
|
prevStates := map[string]bool{}
|
||||||
|
for len(queue) > 0 {
|
||||||
|
front := queue[0]
|
||||||
|
queue = queue[1:]
|
||||||
|
|
||||||
|
if front.isDone() {
|
||||||
|
return front.steps
|
||||||
|
}
|
||||||
|
|
||||||
|
// do not visit previous states
|
||||||
|
// hashKey method does not differentiate material types because
|
||||||
|
// they are effectively the same
|
||||||
|
hash := front.hashKey()
|
||||||
|
if prevStates[hash] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prevStates[hash] = true
|
||||||
|
|
||||||
|
nextStates := front.getNextStates()
|
||||||
|
queue = append(queue, nextStates...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// halves are either a chip or generator
|
||||||
|
type halves struct {
|
||||||
|
isChip bool // false if is generator
|
||||||
|
material string
|
||||||
|
}
|
||||||
|
|
||||||
|
// for easier debugging
|
||||||
|
func (t halves) String() string {
|
||||||
|
tType := " generator"
|
||||||
|
if t.isChip {
|
||||||
|
tType = " microchip"
|
||||||
|
}
|
||||||
|
return fmt.Sprint(t.material, tType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// state of the puzzle with a bunch of methods for getting next states, checking
|
||||||
|
// validity of a state, if it represents a finish state...
|
||||||
|
type state struct {
|
||||||
|
floors [4][]halves
|
||||||
|
elevatorLevel int
|
||||||
|
steps int
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsing the input file, this probably would've been easier to do manually...
|
||||||
|
func newInitialState(input string) state {
|
||||||
|
s := state{}
|
||||||
|
|
||||||
|
for lineIndex, line := range strings.Split(input, "\n") {
|
||||||
|
// The first floor contains a promethium generator and a promethium-compatible microchip.
|
||||||
|
parts := strings.Split(line, " ")
|
||||||
|
// trim commas and periods, this input is pretty inconsistent
|
||||||
|
for i, v := range parts {
|
||||||
|
parts[i] = strings.Trim(v, ",.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterate through the words and if generator or microchip is found
|
||||||
|
// then parse the previous word for the material type
|
||||||
|
for i, word := range parts {
|
||||||
|
if word == "generator" {
|
||||||
|
material := parts[i-1]
|
||||||
|
s.floors[lineIndex] = append(s.floors[lineIndex], halves{
|
||||||
|
isChip: false,
|
||||||
|
material: material,
|
||||||
|
})
|
||||||
|
} else if word == "microchip" {
|
||||||
|
// also parse off the "-compatible" portion
|
||||||
|
material := parts[i-1][:strings.Index(parts[i-1], "-comp")]
|
||||||
|
s.floors[lineIndex] = append(s.floors[lineIndex], halves{
|
||||||
|
isChip: true,
|
||||||
|
material: material,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// for printability & debugging
|
||||||
|
func (s state) String() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
fmt.Fprintf(&sb, "Level %d x Steps %d\n", s.elevatorLevel, s.steps)
|
||||||
|
for i, f := range s.floors {
|
||||||
|
fmt.Fprintf(&sb, " %d: %v\n", i, f)
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates a hash for this state that is comparable, to not repeat states.
|
||||||
|
// I spent over an hour figuring out that I left out the elevator level, which
|
||||||
|
// is a key component of the state hash
|
||||||
|
func (s state) hashKey() string {
|
||||||
|
// get the indices for each generator and chip
|
||||||
|
mapGenToIndex := map[string]int{}
|
||||||
|
mapChipToIndex := map[string]int{}
|
||||||
|
for flIndex, fl := range s.floors {
|
||||||
|
for _, half := range fl {
|
||||||
|
if half.isChip {
|
||||||
|
mapChipToIndex[half.material] = flIndex
|
||||||
|
} else {
|
||||||
|
mapGenToIndex[half.material] = flIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// then put that into slice form so it ignores the material types
|
||||||
|
// this is b/c the types don't really matter... e.g.
|
||||||
|
// 0: LithGen, LithChip 0: PluGen, PluChip
|
||||||
|
// 1: PluGen == 1: LithGen
|
||||||
|
// 2: PluChip 2: LithChip
|
||||||
|
var genChipPairs [][2]int
|
||||||
|
for material := range mapGenToIndex {
|
||||||
|
genChipPairs = append(genChipPairs, [2]int{
|
||||||
|
mapGenToIndex[material], mapChipToIndex[material],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// sort it
|
||||||
|
sort.Slice(genChipPairs, func(i, j int) bool {
|
||||||
|
if genChipPairs[i][0] != genChipPairs[j][0] {
|
||||||
|
return genChipPairs[i][0] < genChipPairs[j][0]
|
||||||
|
}
|
||||||
|
return genChipPairs[i][1] < genChipPairs[j][1]
|
||||||
|
})
|
||||||
|
|
||||||
|
// fmt.Sprint is my best friend for making hashes
|
||||||
|
return fmt.Sprint(s.elevatorLevel, genChipPairs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s state) isValid() bool {
|
||||||
|
// check every level, I lost another hour here because I was only checking
|
||||||
|
// the active level, but moving some halves off a level could make an old
|
||||||
|
// level invalid
|
||||||
|
for i := range s.floors {
|
||||||
|
// make a hashmap of all the generators on this level
|
||||||
|
gensSeen := map[string]bool{}
|
||||||
|
for _, half := range s.floors[i] {
|
||||||
|
if !half.isChip {
|
||||||
|
gensSeen[half.material] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if there are no gens on this level, it's safe
|
||||||
|
if len(gensSeen) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// there are generators, so if there is any chip that is not protected
|
||||||
|
// then it is an invalid level & thus an invalid state
|
||||||
|
for _, half := range s.floors[i] {
|
||||||
|
if half.isChip && !gensSeen[half.material] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// all chips protected, return true
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// is this the final state?
|
||||||
|
func (s state) isDone() bool {
|
||||||
|
var lenSum int
|
||||||
|
for _, fl := range s.floors[:3] {
|
||||||
|
lenSum += len(fl)
|
||||||
|
}
|
||||||
|
return lenSum == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// get perms of INDICES that can be moved off of this level in the next perm
|
||||||
|
// one or two things can be moved, this generates all perms of one or two
|
||||||
|
// indices on the elevator's floor
|
||||||
|
func (s state) getMovablePermIndices() [][]int {
|
||||||
|
var permsToMove [][]int
|
||||||
|
|
||||||
|
currentLevel := s.floors[s.elevatorLevel]
|
||||||
|
// get pairs first
|
||||||
|
for i := 0; i < len(currentLevel); i++ {
|
||||||
|
for j := i + 1; j < len(currentLevel); j++ {
|
||||||
|
permsToMove = append(permsToMove, []int{i, j})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// then get singles
|
||||||
|
for i := range currentLevel {
|
||||||
|
permsToMove = append(permsToMove, []int{i})
|
||||||
|
}
|
||||||
|
return permsToMove
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a deep clone
|
||||||
|
func (s state) clone() state {
|
||||||
|
cl := state{
|
||||||
|
elevatorLevel: s.elevatorLevel,
|
||||||
|
steps: s.steps,
|
||||||
|
}
|
||||||
|
// slices are effectively reference types in go... they need to be cloned...
|
||||||
|
for i, fl := range s.floors {
|
||||||
|
cl.floors[i] = append([]halves{}, fl...)
|
||||||
|
}
|
||||||
|
return cl
|
||||||
|
}
|
||||||
|
|
||||||
|
// get all valid next states that can be reached from this state
|
||||||
|
func (s state) getNextStates() []state {
|
||||||
|
var futureStates []state
|
||||||
|
|
||||||
|
// all combinations of indices that can be moved from this level
|
||||||
|
movablePermIndices := s.getMovablePermIndices()
|
||||||
|
|
||||||
|
// get diffs that the elevator can move in, i.e. don't let it move up from
|
||||||
|
// the top level, or down from level 0
|
||||||
|
var eleDiffs []int
|
||||||
|
if s.elevatorLevel < len(s.floors)-1 {
|
||||||
|
eleDiffs = append(eleDiffs, 1)
|
||||||
|
}
|
||||||
|
if s.elevatorLevel > 0 {
|
||||||
|
eleDiffs = append(eleDiffs, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, eleDiff := range eleDiffs {
|
||||||
|
// for any elevator direction, iterate over moveable perms and make a
|
||||||
|
// clone that's modified with those halves moved to the target floor
|
||||||
|
for _, permIndices := range movablePermIndices {
|
||||||
|
cl := s.clone()
|
||||||
|
cl.elevatorLevel += eleDiff
|
||||||
|
cl.steps++ // increment steps
|
||||||
|
oldLevel := s.elevatorLevel
|
||||||
|
newLevel := cl.elevatorLevel
|
||||||
|
|
||||||
|
// move halves to the clone's active level
|
||||||
|
for _, index := range permIndices {
|
||||||
|
cl.floors[newLevel] = append(cl.floors[newLevel], cl.floors[oldLevel][index])
|
||||||
|
}
|
||||||
|
// remove halves from the current state's level (in the clone)
|
||||||
|
// this code is gross...
|
||||||
|
for in := len(permIndices) - 1; in >= 0; in-- {
|
||||||
|
cl.floors[oldLevel][permIndices[in]] = cl.floors[oldLevel][len(cl.floors[oldLevel])-1]
|
||||||
|
cl.floors[oldLevel] = cl.floors[oldLevel][:len(cl.floors[oldLevel])-1]
|
||||||
|
}
|
||||||
|
// add to final states if its valid
|
||||||
|
if cl.isValid() {
|
||||||
|
futureStates = append(futureStates, cl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return futureStates
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alexchao26/advent-of-code-go/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
var example = `The first floor contains a hydrogen-compatible microchip and a lithium-compatible microchip.
|
||||||
|
The second floor contains a hydrogen generator.
|
||||||
|
The third floor contains a lithium generator.
|
||||||
|
The fourth floor contains nothing relevant.`
|
||||||
|
|
||||||
|
func Test_rtgHellDay(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
part int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"part1 example", example, 1, 11},
|
||||||
|
{"part1 actual", util.ReadFile("input.txt"), 1, 33},
|
||||||
|
{"part2 actual", util.ReadFile("input.txt"), 2, 57},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := rtgHellDay(tt.input, tt.part); got != tt.want {
|
||||||
|
t.Errorf("rtgHellDay() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user