diff --git a/2016/day11/main.go b/2016/day11/main.go new file mode 100644 index 0000000..3753400 --- /dev/null +++ b/2016/day11/main.go @@ -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 +} diff --git a/2016/day11/main_test.go b/2016/day11/main_test.go new file mode 100644 index 0000000..52a1072 --- /dev/null +++ b/2016/day11/main_test.go @@ -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) + } + }) + } +}