diff --git a/2022/graph-utils.h b/2022/graph-utils.h new file mode 100644 index 0000000..d209624 --- /dev/null +++ b/2022/graph-utils.h @@ -0,0 +1,159 @@ +// +// Created by mgretton on 10/01/2022. +// + +#ifndef ADVENT_OF_CODE_GRAPH_UTILS_H +#define ADVENT_OF_CODE_GRAPH_UTILS_H + +#include +#include +#include +#include +#include +#include +#include + +/** \brief Implement Dijkstra's algorithm. + * + * @tparam Cost Cost type + * @tparam Node Node class + * @tparam TransitionManager Class type that manages state transitions + * @tparam FinishedFn Function type to call to test whether a node matches finished state + * @param initial Initial node + * @param initial_cost Cost of initial node + * @param transition_manager Object to manage state transitions + * @return Node, Cost pair indicating success. On failure default node with + * maximum cost is returned. + * + * \a Cost Must be a numeric type. \a Node must be a class which is default constructable, be less + * than comparable. + * + * \a transition_manager must be an object with the following public interfaces: + * \code + * auto is_finished(Node const& node) -> bool; + * + * template + * void generate_children(Node const& node, Inserter inserter); + * \endcode + * + * The \c generate_children() method is called whenever we examine the node \c node, and should + * call \c inserter(next_node, cost_delta) for every node directly reachable from \c node. Where + * \c next_node is the next node to visit and \c cost_delta is the incremental cost to visit the new + * node. \c inserter should be called for all possible nodes that are visitable, it will ensure + * that duplicates are managed correctly/ + * + * The \c is_finished() method should return true if the given node is in a finished state. + */ +template +auto dijkstra(Node const& initial, Cost initial_cost, TransitionManager transition_manager) + -> std::pair +{ + /** Helper struct to order node pointers. */ + struct NodePCmp + { + auto operator()(Node const* lhs, Node const* rhs) const noexcept -> bool + { + if (lhs == nullptr && rhs != nullptr) { + return true; + } + if (rhs == nullptr) { + return false; + } + return *lhs < *rhs; + } + + auto operator()(Node const* lhs, Node const& rhs) const noexcept -> bool + { + if (lhs == nullptr) { + return true; + } + return *lhs < rhs; + } + + auto operator()(Node const& lhs, Node const* rhs) const noexcept -> bool + { + if (rhs == nullptr) { + return false; + } + return lhs < *rhs; + } + }; + + /* We maintain two maps - one from node to cost and the other from cost to nodes. */ + /** \c nodes maintains a map of all the nodes we've visited or want to visit. Nodes that cost + * less than the current cost at the front of costs have been visited. The rest haven't. + */ + std::map nodes; + /** \c costs maintains a map of costs to the nodes that cost that much to visit. */ + std::map> costs; + Cost current_cost{initial_cost}; + Node const* current_node{new Node(initial)}; + nodes.insert({current_node, current_cost}); + costs.insert({current_cost, {current_node}}); + + /* Helper lambda to clean up after ourselves. */ + auto cleanup = [](auto& nodes) { + for (auto const& it : nodes) { + delete it.first; + } + }; + + /* Helper lambda to insert into the maps. */ + auto inserter = [&costs, &nodes, ¤t_cost](Node const& node, Cost cost) { + cost += current_cost; + auto node_it{nodes.find(&node)}; + /* Skip inserting nodes we've already visited, or that would cost more to visit. */ + if (node_it != nodes.end() && node_it->second <= cost) { + return; + } + + Node const* nodep{nullptr}; + if (node_it == nodes.end()) { + nodep = new Node(node); + nodes.insert({nodep, cost}); + } + else { + /* Node has a cheaper cost than we thought: Remove the node from its old cost list */ + nodep = node_it->first; + auto cost_it{costs.find(node_it->second)}; + assert(cost_it != costs.end()); + cost_it->second.erase(nodep); + + /* Now update the cost in the nodes map and use the nodep as the node pointer. */ + node_it->second = cost; + } + + auto [cost_it, success] = costs.insert({cost, {}}); + cost_it->second.insert(nodep); + }; + + std::uint64_t iter{0}; + while (!costs.empty()) { + auto cost_it{costs.begin()}; + current_cost = cost_it->first; + assert(iter < nodes.size()); + assert(std::accumulate(costs.begin(), costs.end(), std::size_t{0}, [](auto a, auto c) { + return a + c.second.size(); + }) == nodes.size() - iter); + for (auto& nodep : cost_it->second) { + if (iter++ % 100'000 == 0) { + std::cout << "Iteration: " << iter << " cost " << current_cost + << " total number of nodes: " << nodes.size() + << ", number of costs left to visit: " << costs.size() + << ", number of nodes left: " << (nodes.size() - iter) << '\n'; + } + if (transition_manager.is_finished(*nodep)) { + auto result{std::make_pair(*nodep, current_cost)}; + cleanup(nodes); + return result; + } + transition_manager.generate_children(*nodep, inserter); + } + costs.erase(costs.begin()); + } + + cleanup(nodes); + return {Node{}, std::numeric_limits::max()}; +} + +#endif // ADVENT_OF_CODE_GRAPH_UTILS_H diff --git a/2022/puzzle-12-01.cc b/2022/puzzle-12-01.cc new file mode 100644 index 0000000..937137f --- /dev/null +++ b/2022/puzzle-12-01.cc @@ -0,0 +1,124 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "graph-utils.h" + +using Int = std::int64_t; + +struct Pos +{ + Pos() noexcept = default; + Pos(Int x, Int y) noexcept : x_(x), y_(y) {} + Pos(Pos const&) noexcept = default; + auto operator=(Pos const&) noexcept -> Pos& = default; + Pos(Pos&&) noexcept = default; + auto operator=(Pos&&) noexcept -> Pos& = default; + ~Pos() noexcept = default; + + auto x() const { return x_; } + auto y() const { return y_; } + +private: + Int x_{0}; + Int y_{0}; +}; + +auto operator+(Pos const& x, Pos const& y) noexcept -> Pos +{ + return Pos{x.x() + y.x(), x.y() + y.y()}; +} + +auto operator<(Pos const& l, Pos const& r) noexcept -> bool +{ + return l.y() < r.y() || (l.y() == r.y() && l.x() < r.x()); +} + +auto operator==(Pos const& l, Pos const& r) noexcept -> bool +{ + return l.x() == r.x() && l.y() == r.y(); +} + +using Height = char; + +struct Map +{ + Map(std::istream& is) + { + std::string line; + while (std::getline(std::cin, line)) { + auto s = line.find('S'); + if (s != std::string::npos) { + start_ = Pos{static_cast(s), static_cast(rows_.size())}; + } + auto e = line.find('E'); + if (e != std::string::npos) { + end_ = Pos{static_cast(e), static_cast(rows_.size())}; + } + rows_.push_back(line); + } + } + auto start() const -> Pos { return start_; } + auto end() const -> Pos { return end_; } + auto width() const -> Int { return rows_[0].length(); } + auto depth() const -> Int { return rows_.size(); } + auto height(Pos const& p) const -> Height + { + if (p == start_) { + return 'a'; + } + if (p == end_) { + return 'z'; + } + return rows_[p.y()][p.x()]; + } + +private: + std::vector rows_; + Pos start_{0, 0}; + Pos end_{0, 0}; +}; + +struct StateTranstitionManager +{ + explicit StateTranstitionManager(Map const& map) : map_(map) {} + + Map const& map_; + + bool is_finished(Pos const& pos) { return pos == map_.end(); } + + template + void generate_children(Pos const& current, Inserter inserter) + { + std::array moves = {Pos{0, 1}, Pos{0, -1}, Pos{1, 0}, Pos{-1, 0}}; + for (auto const& move : moves) { + auto pos{current + move}; + // Check in bounds + if (pos.x() < 0 || pos.x() > map_.width() || pos.y() < 0 || pos.y() > map_.depth()) { + continue; + } + // check height hasn't changed too far + if (map_.height(pos) > map_.height(current) + 1) { + continue; + } + inserter(pos, 1); + } + } +}; + +auto main() -> int +{ + Map map(std::cin); + Pos initial_state{map.start()}; + + auto [finished_state, finished_cost] = + dijkstra(initial_state, Int{0}, StateTranstitionManager{map}); + std::cout << "Done with cost " << finished_cost << '\n'; + return 0; +} diff --git a/2022/puzzle-12-02.cc b/2022/puzzle-12-02.cc new file mode 100644 index 0000000..6bd8632 --- /dev/null +++ b/2022/puzzle-12-02.cc @@ -0,0 +1,116 @@ +#include +#include +#include +#include +#include + +#include "graph-utils.h" + +using Int = std::int64_t; + +struct Pos +{ + Pos() noexcept = default; + Pos(Int x, Int y) noexcept : x_(x), y_(y) {} + Pos(Pos const&) noexcept = default; + auto operator=(Pos const&) noexcept -> Pos& = default; + Pos(Pos&&) noexcept = default; + auto operator=(Pos&&) noexcept -> Pos& = default; + ~Pos() noexcept = default; + + [[nodiscard]] auto x() const { return x_; } + [[nodiscard]] auto y() const { return y_; } + +private: + Int x_{0}; + Int y_{0}; +}; + +auto operator+(Pos const& x, Pos const& y) noexcept -> Pos +{ + return Pos{x.x() + y.x(), x.y() + y.y()}; +} + +auto operator<(Pos const& l, Pos const& r) noexcept -> bool +{ + return l.y() < r.y() || (l.y() == r.y() && l.x() < r.x()); +} + +auto operator==(Pos const& l, Pos const& r) noexcept -> bool +{ + return l.x() == r.x() && l.y() == r.y(); +} + +using Height = char; + +struct Map +{ + explicit Map(std::istream& is) + { + std::string line; + while (std::getline(is, line)) { + auto s = line.find('S'); + if (s != std::string::npos) { + start_ = Pos{static_cast(s), static_cast(rows_.size())}; + line[s] = 'a'; + } + auto e = line.find('E'); + if (e != std::string::npos) { + end_ = Pos{static_cast(e), static_cast(rows_.size())}; + line[e] = 'z'; + } + rows_.push_back(line); + } + } + [[nodiscard]] auto start() const -> Pos { return start_; } + [[nodiscard]] auto end() const -> Pos { return end_; } + [[nodiscard]] auto width() const -> Int { return static_cast(rows_[0].length()); } + [[nodiscard]] auto depth() const -> Int { return static_cast(rows_.size()); } + [[nodiscard]] auto height(Pos const& p) const -> Height + { + return rows_[p.y()][p.x()]; + } + +private: + std::vector rows_; + Pos start_{0, 0}; + Pos end_{0, 0}; +}; + +struct StateTranstitionManager +{ + explicit StateTranstitionManager(Map map) : map_(std::move(map)) {} + + Map map_; + + [[nodiscard]] auto is_finished(Pos const& pos) const -> bool { return map_.height(pos) == 'a'; } + + template + void generate_children(Pos const& current, Inserter inserter) const + { + std::array const moves{Pos{0, 1}, Pos{0, -1}, Pos{1, 0}, Pos{-1, 0}}; + for (auto const& move : moves) { + auto pos{current + move}; + // Check in bounds + if (pos.x() < 0 || pos.x() > map_.width() || pos.y() < 0 || pos.y() > map_.depth()) { + continue; + } + // check height hasn't changed too far + if (map_.height(pos) < map_.height(current) - 1) { + continue; + } + inserter(pos, 1); + } + } +}; + +auto main() -> int +{ + Map const map(std::cin); + Pos const initial_state{map.end()}; + + auto [finished_state, finished_cost] = + dijkstra(initial_state, Int{0}, StateTranstitionManager{map}); + std::cout << "Done with cost " << finished_cost << '\n'; + return 0; +}