diff --git a/2022/16.cpp b/2022/16.cpp index e1a58f2..6558c36 100644 --- a/2022/16.cpp +++ b/2022/16.cpp @@ -1,3 +1,12 @@ +/// @file 16.cpp +/// @brief Solution to Advent of Code 2022 Day 16 +/// +/// This solution follows the following process: +/// 1. Parse the input source into a list of rooms +/// 2. Compute the shortest paths between each room +/// 3. Enumerate all the paths through the graph to find maximum water flow per valve-set. +/// 4. Use the flow/valve summaries to compute the two answers. + #include #include #include @@ -21,11 +30,18 @@ namespace { -using distance_array = boost::multi_array; +/// @brief Array of distances from one node to another. +/// @tparam T distance type +/// +/// `distance[i][j]` is the cost to move from node `i` to node `j` +template +using distance_array = boost::multi_array; /// @brief Update distance matrix with transitive shortest paths +/// @tparam T distance type /// @param dist Single-step distances between nodes (must be square) -auto ShortestDistances(distance_array & dist) -> void +template +auto ShortestDistances(distance_array & dist) -> void { // Floyd–Warshall_algorithm auto const range = boost::irange(dist.size()); @@ -40,6 +56,8 @@ auto ShortestDistances(distance_array & dist) -> void } } +/// @struct Room +/// @brief A single record from the problem input. struct Room { /// @brief Name of the room std::string name; @@ -49,6 +67,15 @@ struct Room { std::vector connections; }; +/// @brief Parse the input file +/// @param in input stream +/// @return Vector of parsed rooms, one per input line +/// +/// The parser will consume input until the end of the stream. +/// +/// Input lines should follow the following pattern: +/// * Valve **name** has flow rate= **number** ; tunnels lead to valves **name**, **name** ... +/// * Valve **name** has flow rate= **number** ; tunnel leads to valve **name** auto Parse(std::istream & in) -> std::vector { std::vector result; @@ -74,7 +101,7 @@ auto Parse(std::istream & in) -> std::vector It b = line.begin(); It e = line.end(); result.emplace_back(); - if (!qi::parse(b, e, room_description, result.back())) { + if (!qi::parse(b, e, room_description, result.back()) || b != e) { throw std::runtime_error{"bad input line"}; } } @@ -85,7 +112,7 @@ auto Parse(std::istream & in) -> std::vector /// @returns starting index and distances auto GenerateDistances( std::vector const& rooms -) -> std::pair +) -> std::pair> { auto const N = rooms.size(); @@ -95,7 +122,7 @@ auto GenerateDistances( names[room.name] = i; } - distance_array distances{boost::extents[N][N]}; + distance_array distances{boost::extents[N][N]}; // N is longer than any optimal distance by at least 1 std::fill_n(distances.data(), distances.num_elements(), N); @@ -117,12 +144,19 @@ auto GenerateDistances( return {names.at("AA"), std::move(distances)}; } +/// @brief Bitset used to track which valves have been turned on using Valves = std::bitset<64>; +/// @struct State +/// @brief Intermediate states for depth-first search in Routes struct State { + /// @brief Time remaining std::uint64_t time; + /// @brief Water flow achieved so far std::uint64_t flow; + /// @brief Current actor location std::size_t location; + /// @brief Set of valves already opened Valves valves; }; @@ -136,38 +170,46 @@ auto Routes( std::size_t const start, std::uint64_t const initial_time, std::vector const& rooms, - distance_array const& distances + distance_array const& distances ) -> std::unordered_map { - std::vector states { State{initial_time, 0, start, {}} }; + // Maximal flow seen at each set of open valves std::unordered_map result; + // Figure out which rooms have flow and are worth visiting at all + std::vector interesting_rooms; + for (auto [i, room] : rooms | boost::adaptors::indexed()) { + if (room.flow > 0) { + interesting_rooms.push_back(i); + } + } + + // Remaining states for depth first search + std::vector states { State{initial_time, 0, start, {}} }; while (!states.empty()) { auto const state = states.back(); states.pop_back(); - auto const i = state.location; - for (auto [j, room] : rooms | boost::adaptors::indexed()) { + if (auto & best = result[state.valves]; best < state.flow) { + best = state.flow; + } + + auto const distances_i = distances[state.location]; + + for (auto const j : interesting_rooms) { // don't revisit a valve if (state.valves.test(j)) { continue; } - // don't visit rooms with useless valves - if (room.flow == 0) { continue; } - // don't visit rooms we can't get to in time - auto const cost = distances[i][j]; - if (cost+1 >= state.time) { continue; } + // +1 accounts for the cost of actually turning the valve + auto const cost = distances_i[j] + 1; + if (cost >= state.time) { continue; } - auto const time = state.time - cost - 1; - auto const flow = state.flow + room.flow * time; + auto const time = state.time - cost; + auto const flow = state.flow + rooms[j].flow * time; auto valves = state.valves; valves.set(j); - // remember the best we've seen for this valve-set - if (result[valves] < flow) { - result[valves] = flow; - } - states.push_back({time, flow, static_cast(j), valves}); } } @@ -183,7 +225,7 @@ auto Routes( auto Part1( std::size_t const start, std::vector const& rooms, - distance_array const& distances + distance_array const& distances ) -> std::uint64_t { auto const routes = Routes(start, 30, rooms, distances); @@ -198,26 +240,27 @@ auto Part1( auto Part2( std::size_t const start, std::vector const& rooms, - distance_array const& distances + distance_array const& distances ) -> std::uint64_t { auto const routes = Routes(start, 26, rooms, distances); auto const end = routes.end(); - std::uint64_t result {0}; + + std::uint64_t best {0}; for (auto it1 = routes.begin(); it1 != end; std::advance(it1, 1)) { for (auto it2 = std::next(it1); it2 != end; std::advance(it2, 1)) { // only consider pairs that have disjoint sets of valves if ((it1->first & it2->first).none()) { - result = std::max(result, it1->second + it2->second); + best = std::max(best, it1->second + it2->second); } } } - return result; + return best; } } // namespace -TEST_SUITE("2022-16 examples") { +TEST_SUITE("2022-16") { TEST_CASE("example") { std::istringstream in { R"(Valve AA has flow rate=0; tunnels lead to valves DD, II, BB @@ -236,8 +279,48 @@ Valve JJ has flow rate=21; tunnel leads to valve II CHECK(1651 == Part1(start, rooms, distances)); CHECK(1707 == Part2(start, rooms, distances)); } + + TEST_CASE("shortest path") { + distance_array distances{boost::extents[4][4]}; + std::fill_n(distances.data(), distances.num_elements(), 100); + + distances[0][2] = -2; + distances[0][0] = 0; + distances[1][0] = 4; + distances[1][1] = 0; + distances[1][2] = 3; + distances[2][2] = 0; + distances[2][3] = 2; + distances[3][1] = -1; + distances[3][3] = 0; + ShortestDistances(distances); + + CHECK(distances[0][0] == 0); + CHECK(distances[0][1] == -1); + CHECK(distances[0][2] == -2); + CHECK(distances[0][3] == 0); + + CHECK(distances[1][0] == 4); + CHECK(distances[1][1] == 0); + CHECK(distances[1][2] == 2); + CHECK(distances[1][3] == 4); + + CHECK(distances[2][0] == 5); + CHECK(distances[2][1] == 1); + CHECK(distances[2][2] == 0); + CHECK(distances[2][3] == 2); + + CHECK(distances[3][0] == 3); + CHECK(distances[3][1] == -1); + CHECK(distances[3][2] == 1); + CHECK(distances[3][3] == 0); + } } +/// @brief Select input source and print solution to part 1 and 2 +/// @param argc Command line argument count +/// @param argv Command line arguments +/// @return 0 on success auto main(int argc, char** argv) -> int { auto const rooms = Parse(*aocpp::Startup(argc, argv)); diff --git a/lib/include/aocpp/Startup.hpp b/lib/include/aocpp/Startup.hpp index b97f14e..fb5208b 100644 --- a/lib/include/aocpp/Startup.hpp +++ b/lib/include/aocpp/Startup.hpp @@ -1,6 +1,8 @@ #ifndef AOCPP_STARTUP_HPP_ #define AOCPP_STARTUP_HPP_ +/// @file Startup.hpp + #include #include #include @@ -8,6 +10,9 @@ namespace aocpp { +/// @brief Return the selected input stream or run the test suite +/// @param argc Number of arguments +/// @param argv Command line arguments auto Startup(int argc, char ** argv) -> std::unique_ptr; }