diff --git a/2022/day16/main.go b/2022/day16/main.go new file mode 100644 index 0000000..0f33bb7 --- /dev/null +++ b/2022/day16/main.go @@ -0,0 +1,393 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "sort" + "strings" + + "github.com/alexchao26/advent-of-code-go/mathy" + "github.com/alexchao26/advent-of-code-go/util" +) + +//go:embed input.txt +var input string + +func init() { + // do this in init (not main) so test file has same input + input = strings.TrimRight(input, "\n") + if len(input) == 0 { + panic("empty input.txt file") + } +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + util.CopyToClipboard(fmt.Sprintf("%v", ans)) + fmt.Println("Output:", ans) + } +} + +/* +PART 1 and PART 2 ARE IMPLEMENTED COMPLETELY SEPARATELY. +If you're looking for part 2 inspiration you might want to not even look at my part1 code... +*/ + +func part1(input string) int { + // 30 min until cave erupts + // start in room AA + // creates graph to other rooms... + // flow rate is zero to start + // could spend 1 minute moving to BB and 1 more minute opening it. will release pressure of during remaining 28 of 30 minutes. 13 flow * 28 minutes = 364 total pressure lol + // then can move to CC in min 3, open in min 4, etc etc + // release max pressure in 30 min? + graph := makeGraph(input) + + open := map[string]bool{"AA": true} + for name, rm := range graph { + if rm.flowRate == 0 { + open[name] = true + } + } + + // "AA" open will not matter really... + return bfs(graph, "AA", 30, 0, open, map[string]int{}) +} + +type room struct { + name string + flowRate int + connectedTo []string +} + +func makeGraph(input string) map[string]room { + graph := map[string]room{} + + for _, line := range strings.Split(input, "\n") { + // Valve BB has flow rate=13; tunnels lead to valves CC, AA + parts := strings.Split(line, "; ") + rm := room{} + _, err := fmt.Sscanf(parts[0], "Valve %s has flow rate=%d", &rm.name, &rm.flowRate) + if err != nil { + panic("parsing valve name and flow rate" + err.Error()) + } + connections := strings.Split(parts[1], ", ") + // update first entry to remove leading string + connections[0] = connections[0][len(connections[0])-2:] + rm.connectedTo = connections + graph[rm.name] = rm + } + + return graph +} + +func bfs(graph map[string]room, currentRoom string, minutesLeft, currentPressure int, open map[string]bool, memo map[string]int) int { + if minutesLeft == 0 { + return 0 + } + + key := hash(currentRoom, minutesLeft, open, currentPressure) + if v, ok := memo[key]; ok { + return v + } + + // recursive calls will update this if it is better, then return it + bestFlow := 0 + + // there are two paths to take at a room + // 1. stay and open the valve + // this is only worth doing if the valve is not already on + // 2. move to a neighboring + + // 1. open current room's valve + if !open[currentRoom] { + open[currentRoom] = true + // totalPressureContribution := (minutesLeft - 1) * graph[currentRoom].flowRate + + newPressure := currentPressure + graph[currentRoom].flowRate + + maybeBest := currentPressure + bfs(graph, currentRoom, minutesLeft-1, newPressure, open, memo) + + bestFlow = mathy.MaxInt(bestFlow, maybeBest) + + // backtrack + open[currentRoom] = false + } + + // 2. move to neighbors + for _, neighbor := range graph[currentRoom].connectedTo { + maybeBest := currentPressure + bfs(graph, neighbor, minutesLeft-1, currentPressure, open, memo) + bestFlow = mathy.MaxInt(bestFlow, maybeBest) + } + + memo[key] = bestFlow + + return bestFlow +} + +func hash(currentRoom string, minutesLeft int, open map[string]bool, currentPressure int) string { + rms := []string{} + for k := range open { + rms = append(rms, k) + } + sort.Strings(rms) + return fmt.Sprint(currentRoom, minutesLeft, rms, currentPressure) +} + +// PART 2, basically restarting because my part1 seems too far in the opposite direction to reuse + +func part2(input string) int { + // index within [16]int arrays which will be used to track which rooms have been visited + // from analysis i know that my input has 15 non-zero pressure rooms + // every time we visit a room we'll open the valve + + graph := makeGraph(input) + + roomToFlowRate := map[string]int{} + highestFlowRatePossible := 0 // might be useful for an optimization later + + // sort room names so the arrays/slices later will be in a repeatable order (easier debugging) + // include starting room "AA" in this list even though it has zero flow rate + roomNames := []string{"AA"} + for name, room := range graph { + if room.flowRate != 0 { + roomNames = append(roomNames, name) + } + } + + // reformat into arrays/slices so we don't have to do room name lookups + flowRates := make([]int, len(roomNames)) + + sort.Strings(roomNames) + + for i, name := range roomNames { + roomToFlowRate[name] = graph[name].flowRate + flowRates[i] = roomToFlowRate[name] + highestFlowRatePossible += graph[name].flowRate + } + + weightedGraph := makeWeightedGraph(graph, roomNames) + + visitedArrayToHighestPressureTotals := map[[16]bool]int{} + dfsGetHighestPressureTotalsForEveryVisitedState(26, [16]bool{}, weightedGraph, flowRates, 0, 0, 0, visitedArrayToHighestPressureTotals) + + return highestDisjointPair(visitedArrayToHighestPressureTotals) +} + +func makeWeightedGraph(graph map[string]room, roomNames []string) [][]int { + ans := make([][]int, len(roomNames)) + for i := range ans { + ans[i] = make([]int, len(roomNames)) + } + + // bfs between every node to make graph + + for startIndex, startName := range roomNames { + for endIndex, endName := range roomNames { + if startName == endName { + continue + } + stepsBetweenRooms := bfsDistanceBetweenRooms(graph, startName, endName) + ans[startIndex][endIndex] = stepsBetweenRooms + ans[endIndex][startIndex] = stepsBetweenRooms + } + } + + return ans +} + +func bfsDistanceBetweenRooms(graph map[string]room, startName, endName string) int { + type node struct { + name string + steps int + } + + queue := []node{ + { + name: startName, + steps: 0, + }, + } + + seen := map[string]bool{} + for len(queue) > 0 { + pop := queue[0] + queue = queue[1:] + if seen[pop.name] { + continue + } + seen[pop.name] = true + + if pop.name == endName { + return pop.steps + } + for _, neighbor := range graph[pop.name].connectedTo { + queue = append(queue, node{ + name: neighbor, + steps: pop.steps + 1, + }) + } + } + // assume all rooms are reachable + panic("should return from loop") +} + +// populates ansArray with the best possible values for visiting a particular set of rooms +func dfsGetHighestPressureTotalsForEveryVisitedState(timeLeft int, visited [16]bool, + graph [][]int, flowRates []int, currentRoom int, flowRate, totalPressure int, + ansArray map[[16]bool]int) { + if timeLeft < 0 { + panic("negative timeLeft") + } + if timeLeft == 0 { + ansArray[visited] = mathy.MaxInt(ansArray[visited], totalPressure) + return + } + + // branch 1: just not moving at all + dfsGetHighestPressureTotalsForEveryVisitedState(0, visited, graph, flowRates, currentRoom, + flowRate, totalPressure+flowRate*timeLeft, ansArray) + + // rest of branches: attempt to visit every possible non-visited node + for roomIndex := range graph { + hasBeenVisited := visited[roomIndex] + if hasBeenVisited { + continue + } + + // get to room, one more to open valve + timeToOpenNextValve := graph[currentRoom][roomIndex] + 1 + // not worth visiting if the valve can't be opened in time + if timeLeft < timeToOpenNextValve { + continue + } + + // in Go this makes a full copy of the array, so &nextVisited != &visited + // this is NOT true for slices ([]bool) + nextVisited := visited + nextVisited[roomIndex] = true + dfsGetHighestPressureTotalsForEveryVisitedState( + timeLeft-timeToOpenNextValve, + nextVisited, + graph, + flowRates, + roomIndex, + flowRate+flowRates[roomIndex], + totalPressure+flowRate*timeToOpenNextValve, + ansArray) + } + +} + +func highestDisjointPair(visitedArrayToHighestPressureTotals map[[16]bool]int) int { + type finishingState struct { + bitmap int + // leaving this here to explain a simpler solution (without bitmap overkill) + // visited [16]bool + totalPressure int + } + + allFinishingStates := []finishingState{} + for visited, totalPressure := range visitedArrayToHighestPressureTotals { + allFinishingStates = append(allFinishingStates, finishingState{ + bitmap: convertToBitmap(visited), + totalPressure: totalPressure, + }) + } + + // sort in decreasing order + sort.Slice(allFinishingStates, func(i, j int) bool { + return allFinishingStates[i].totalPressure > allFinishingStates[j].totalPressure + }) + + bestCombo := -1 + + for _, baseFinishingState := range allFinishingStates { + for _, maybeDisjointFinishingState := range allFinishingStates { + pressureSum := baseFinishingState.totalPressure + maybeDisjointFinishingState.totalPressure + // allFinishingStates is sorted in decreasing order so at this point there is no reason + // to continue checking sums + if pressureSum < bestCombo { + break + } + + // only update baseFinishing state if sets are disjointed (human and elephant can't + // open the same room's valve) + // + // using bit logic is overkill, this could've been replaced with this single for loop: + // isDisjoint := true + // for i, wasVisited := range baseFinishingState.visited { + // if wasVisited && maybeDisjointFinishingState.visited[i] { + // isDisjoint = false + // break + // } + // } + if baseFinishingState.bitmap&maybeDisjointFinishingState.bitmap == 0 { + bestCombo = mathy.MaxInt(bestCombo, pressureSum) + } + + } + } + + return bestCombo +} + +func convertToBitmap(visited [16]bool) int { + var bitmap int + for i, wasVisited := range visited { + if wasVisited { + bitmap |= 1 << i + } + } + return bitmap +} + +// recheck part1 using part2 logic +func part1ViaPart2(input string) int { + graph := makeGraph(input) + + roomToFlowRate := map[string]int{} + highestFlowRatePossible := 0 // might be useful for an optimization later + + // sort room names so the arrays/slices later will be in a repeatable order (easier debugging) + // include starting room "AA" in this list even though it has zero flow rate + roomNames := []string{"AA"} + for name, room := range graph { + if room.flowRate != 0 { + roomNames = append(roomNames, name) + } + } + + // reformat into arrays/slices so we don't have to do room name lookups + flowRates := make([]int, len(roomNames)) + + sort.Strings(roomNames) + + for i, name := range roomNames { + roomToFlowRate[name] = graph[name].flowRate + flowRates[i] = roomToFlowRate[name] + highestFlowRatePossible += graph[name].flowRate + } + + weightedGraph := makeWeightedGraph(graph, roomNames) + + visitedArrayToHighestPressureTotals := map[[16]bool]int{} + dfsGetHighestPressureTotalsForEveryVisitedState(30, [16]bool{}, weightedGraph, flowRates, 0, 0, 0, visitedArrayToHighestPressureTotals) + + highest := 0 + for _, val := range visitedArrayToHighestPressureTotals { + highest = mathy.MaxInt(highest, val) + } + return highest +} diff --git a/2022/day16/main_test.go b/2022/day16/main_test.go new file mode 100644 index 0000000..56cdca8 --- /dev/null +++ b/2022/day16/main_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "testing" +) + +var example = `Valve AA has flow rate=0; tunnels lead to valves DD, II, BB +Valve BB has flow rate=13; tunnels lead to valves CC, AA +Valve CC has flow rate=2; tunnels lead to valves DD, BB +Valve DD has flow rate=20; tunnels lead to valves CC, AA, EE +Valve EE has flow rate=3; tunnels lead to valves FF, DD +Valve FF has flow rate=0; tunnels lead to valves EE, GG +Valve GG has flow rate=0; tunnels lead to valves FF, HH +Valve HH has flow rate=22; tunnel leads to valve GG +Valve II has flow rate=0; tunnels lead to valves AA, JJ +Valve JJ has flow rate=21; tunnel leads to valve II` + +func Test_part1(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 1651, + }, + { + name: "actual", + input: input, + want: 1828, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + t.Run(tt.name+"with part 2 logic", func(t *testing.T) { + if got := part1ViaPart2(tt.input); got != tt.want { + t.Errorf("part1ViaPart2() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_part2(t *testing.T) { + tests := []struct { + name string + input string + want int + }{ + { + name: "example", + input: example, + want: 1707, + }, + { + name: "actual", + input: input, + want: 2292, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } +}