|
| 1 | +package day16 |
| 2 | + |
| 3 | +import locations.Directory.currentDir |
| 4 | +import inputs.Input.loadFileSync |
| 5 | + |
| 6 | +@main def part1: Unit = |
| 7 | + println(s"The solution is ${part1(loadInput())}") |
| 8 | + |
| 9 | +@main def part2: Unit = |
| 10 | + println(s"The solution is ${part2(loadInput())}") |
| 11 | + |
| 12 | +def loadInput(): String = loadFileSync(s"$currentDir/../input/day16") |
| 13 | + |
| 14 | +/* |
| 15 | +Copyright 2022 Tyler Coles (javadocmd.com) & Quentin Bernet |
| 16 | +
|
| 17 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 18 | +you may not use this file except in compliance with the License. |
| 19 | +You may obtain a copy of the License at |
| 20 | +
|
| 21 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 22 | + |
| 23 | +Unless required by applicable law or agreed to in writing, software |
| 24 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 25 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 26 | +See the License for the specific language governing permissions and |
| 27 | +limitations under the License. |
| 28 | +*/ |
| 29 | + |
| 30 | +type Id = String |
| 31 | +case class Room(id: Id, flow: Int, tunnels: List[Id]) |
| 32 | + |
| 33 | +type Input = List[Room] |
| 34 | +// $_ to avoid tunnel/tunnels distinction and so on |
| 35 | +def parse(xs: String): Input = xs.split("\n").map{ case s"Valve $id has flow rate=$flow; tunnel$_ lead$_ to valve$_ $tunnelsStr" => |
| 36 | + val tunnels = tunnelsStr.split(", ").toList |
| 37 | + Room(id, flow.toInt, tunnels) |
| 38 | +}.toList |
| 39 | + |
| 40 | +case class RoomsInfo( |
| 41 | + /** map of rooms by id */ |
| 42 | + rooms: Map[Id, Room], |
| 43 | + /** map from starting room to a map containing the best distance to all other rooms */ |
| 44 | + routes: Map[Id, Map[Id, Int]], |
| 45 | + /** rooms containing non-zero-flow valves */ |
| 46 | + valves: Set[Id] |
| 47 | +) |
| 48 | + |
| 49 | +// precalculate useful things like pathfinding |
| 50 | +def constructInfo(input: Input): RoomsInfo = |
| 51 | + val rooms: Map[Id, Room] = Map.from(for r <- input yield r.id -> r) |
| 52 | + val valves: Set[Id] = Set.from(for r <- input if r.flow > 0 yield r.id) |
| 53 | + val tunnels: Map[Id, List[Id]] = rooms.mapValues(_.tunnels).toMap |
| 54 | + val routes: Map[Id, Map[Id, Int]] = (valves + "AA").iterator.map{ id => id -> computeRoutes(id, tunnels) }.toMap |
| 55 | + RoomsInfo(rooms, routes, valves) |
| 56 | + |
| 57 | +// a modified A-star to calculate the best distance to all rooms rather then the best path to a single room |
| 58 | +def computeRoutes(start: Id, neighbors: Id => List[Id]): Map[Id, Int] = |
| 59 | + |
| 60 | + case class State(frontier: List[(Id, Int)], scores: Map[Id, Int]): |
| 61 | + |
| 62 | + private def getScore(id: Id): Int = scores.getOrElse(id, Int.MaxValue) |
| 63 | + private def setScore(id: Id, s: Int) = State((id, s + 1) :: frontier, scores + (id -> s)) |
| 64 | + |
| 65 | + def dequeued: (Id, State) = |
| 66 | + val sorted = frontier.sortBy(_._2) |
| 67 | + (sorted.head._1, copy(frontier = sorted.tail)) |
| 68 | + |
| 69 | + def considerEdge(from: Id, to: Id): State = |
| 70 | + val toScore = getScore(from) + 1 |
| 71 | + if toScore >= getScore(to) then this |
| 72 | + else setScore(to, toScore) |
| 73 | + end State |
| 74 | + |
| 75 | + object State: |
| 76 | + def initial(start: Id) = State(List((start, 0)), Map(start -> 0)) |
| 77 | + |
| 78 | + def recurse(state: State): State = |
| 79 | + if state.frontier.isEmpty then |
| 80 | + state |
| 81 | + else |
| 82 | + val (curr, currState) = state.dequeued |
| 83 | + val newState = neighbors(curr) |
| 84 | + .foldLeft(currState) { (s, n) => |
| 85 | + s.considerEdge(curr, n) |
| 86 | + } |
| 87 | + recurse(newState) |
| 88 | + |
| 89 | + recurse(State.initial(start)).scores |
| 90 | + |
| 91 | +end computeRoutes |
| 92 | + |
| 93 | + |
| 94 | +// find the best path (the order of valves to open) and the total pressure released by taking it |
| 95 | +def bestPath(map: RoomsInfo, start: Id, valves: Set[Id], timeAllowed: Int): Int = |
| 96 | + // each step involves moving to a room with a useful valve and opening it |
| 97 | + // we don't need to track each (empty) room in between |
| 98 | + // we limit our options by only considering the still-closed valves |
| 99 | + // and `valves` has already culled any room with a flow value of 0 -- no point in considering these rooms! |
| 100 | + |
| 101 | + def recurse(path: List[Id], valvesLeft: Set[Id], timeLeft: Int, totalValue: Int): Int = |
| 102 | + // recursively consider all plausible options |
| 103 | + // we are finished when we no longer have time to reach another valve or all valves are open |
| 104 | + valvesLeft |
| 105 | + .flatMap{ id => |
| 106 | + val current = path.head |
| 107 | + val distance = map.routes(current)(id) |
| 108 | + // how much time is left after we traverse there and open the valve? |
| 109 | + val t = timeLeft - distance - 1 |
| 110 | + // if `t` is zero or less this option can be skipped |
| 111 | + Option.when(t > 0) { |
| 112 | + // the value of choosing a particular valve (over the life of our simulation) |
| 113 | + // is its flow rate multiplied by the time remaining after opening it |
| 114 | + val value = map.rooms(id).flow * t |
| 115 | + recurse(id :: path, valvesLeft - id, t, totalValue + value) |
| 116 | + } |
| 117 | + } |
| 118 | + .maxOption |
| 119 | + .getOrElse { totalValue } |
| 120 | + end recurse |
| 121 | + recurse(start :: Nil, valves, timeAllowed, 0) |
| 122 | + |
| 123 | +def part1(input: String) = |
| 124 | + val time = 30 |
| 125 | + val map = constructInfo(parse(input)) |
| 126 | + bestPath(map, "AA", map.valves, time) |
| 127 | +end part1 |
| 128 | + |
| 129 | +def part2(input: String) = |
| 130 | + val time = 26 |
| 131 | + val map = constructInfo(parse(input)) |
| 132 | + |
| 133 | + // in the optimal solution, the elephant and I will have divided responsibility for switching the valves |
| 134 | + // 15 (useful valves) choose 7 (half) yields only 6435 possible divisions which is a reasonable search space! |
| 135 | + val valvesA = map.valves.toList |
| 136 | + .combinations(map.valves.size / 2) |
| 137 | + .map(_.toSet) |
| 138 | + |
| 139 | + // NOTE: I assumed an even ditribution of valves would be optimal, and that turned out to be true. |
| 140 | + // However I suppose it's possible an uneven distribution could have been optimal for some graphs. |
| 141 | + // To be safe, you could re-run this using all reasonable values of `n` for `combinations` (1 to 7) and |
| 142 | + // taking the best of those. |
| 143 | + |
| 144 | + // we can now calculate the efforts separately and sum their values to find the best |
| 145 | + val allPaths = |
| 146 | + for va <- valvesA yield |
| 147 | + val vb = map.valves -- va |
| 148 | + val scoreA = bestPath(map, "AA", va, time) |
| 149 | + val scoreB = bestPath(map, "AA", vb, time) |
| 150 | + scoreA + scoreB |
| 151 | + |
| 152 | + allPaths.max |
| 153 | +end part2 |
0 commit comments