2016-day11: one of the hardest aoc problems yet... essentially a bfs next states

This commit is contained in:
alexchao26
2020-12-26 00:38:38 -05:00
parent cb4dadb7fc
commit 94d2e879b0
2 changed files with 316 additions and 0 deletions
+284
View File
@@ -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
}
+32
View File
@@ -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)
}
})
}
}