mirror of
https://github.com/Threnklyn/advent-of-code-go.git
synced 2026-05-18 19:13:27 +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