From 08a94ba068d2384a11bc609a28bd0c7a4cf1af3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20M=C3=BCller?= Date: Wed, 30 Apr 2025 19:40:36 +0200 Subject: [PATCH] Add solution for "Day 16: Reindeer Maze", part 1 --- include/aoc/ReindeerMaze.hpp | 45 ++++ include/aoc/ReindeerMazeCrossing.hpp | 32 +++ include/aoc/ReindeerMazePathIncidence.hpp | 34 +++ include/aoc/VertexEdgeIncidence.hpp | 28 +++ include/aoc/WeightedEdgeGraph.hpp | 33 +++ src/Program.cpp | 2 + src/ReindeerMaze.cpp | 258 ++++++++++++++++++++++ src/ReindeerMazeCrossing.cpp | 32 +++ src/ReindeerMazePathIncidence.cpp | 51 +++++ src/WeightedEdgeGraph.cpp | 70 ++++++ tests/src/TestCases.cpp | 18 ++ 11 files changed, 603 insertions(+) create mode 100644 include/aoc/ReindeerMaze.hpp create mode 100644 include/aoc/ReindeerMazeCrossing.hpp create mode 100644 include/aoc/ReindeerMazePathIncidence.hpp create mode 100644 include/aoc/VertexEdgeIncidence.hpp create mode 100644 include/aoc/WeightedEdgeGraph.hpp create mode 100644 src/ReindeerMaze.cpp create mode 100644 src/ReindeerMazeCrossing.cpp create mode 100644 src/ReindeerMazePathIncidence.cpp create mode 100644 src/WeightedEdgeGraph.cpp diff --git a/include/aoc/ReindeerMaze.hpp b/include/aoc/ReindeerMaze.hpp new file mode 100644 index 0000000..7c8c3d8 --- /dev/null +++ b/include/aoc/ReindeerMaze.hpp @@ -0,0 +1,45 @@ +// Solutions to the Advent Of Code 2024. +// Copyright (C) 2025 Stefan Müller +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +#pragma once + +#include +#include + +#include +#include +#include +#include + +class ReindeerMaze + : public LinesSolver +{ +public: + ReindeerMaze(const int inputFileNameSuffix = 0); + virtual const std::string getPuzzleName() const override; + virtual const int getPuzzleDay() const override; + virtual void finish() override; +private: + static constexpr char getStartChar(); + static constexpr char getEndChar(); + static constexpr char getWallChar(); + static constexpr int getTurnCost(); + void initializeWorkList(std::list& crossings, const int entryVertex); + void addCheckedIncidence(std::vector& incidences, const Point2 start, + const Point2 direction); + Point2 findStart(); + void AddPathSegmentEdges(WeightedEdgeGraph& graph, const ReindeerMazePathIncidence& pathIncidence, + const std::vector& otherPathIncidences); +}; diff --git a/include/aoc/ReindeerMazeCrossing.hpp b/include/aoc/ReindeerMazeCrossing.hpp new file mode 100644 index 0000000..a9b5cdf --- /dev/null +++ b/include/aoc/ReindeerMazeCrossing.hpp @@ -0,0 +1,32 @@ +// Solutions to the Advent Of Code 2024. +// Copyright (C) 2025 Stefan Müller +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +#pragma once + +#include + +#include +#include + +class ReindeerMazeCrossing +{ +public: + ReindeerMazeCrossing(const Point2 position); + Point2 getPosition() const; + bool isFinished() const; + std::vector incidences; +private: + Point2 position_; +}; diff --git a/include/aoc/ReindeerMazePathIncidence.hpp b/include/aoc/ReindeerMazePathIncidence.hpp new file mode 100644 index 0000000..01ca275 --- /dev/null +++ b/include/aoc/ReindeerMazePathIncidence.hpp @@ -0,0 +1,34 @@ +// Solutions to the Advent Of Code 2024. +// Copyright (C) 2025 Stefan Müller +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +#pragma once + +#include + +class ReindeerMazePathIncidence +{ +public: + ReindeerMazePathIncidence(const Point2 direction); + ReindeerMazePathIncidence(const Point2 direction, const int pathVertex); + Point2 getDirection() const; + int getPathVertex() const; + void setPathVertex(const int pathVertex); + int getPathCost() const; + void setPathCost(const int pathCost); +private: + Point2 direction_; + int pathVertex_; + int pathCost_; +}; diff --git a/include/aoc/VertexEdgeIncidence.hpp b/include/aoc/VertexEdgeIncidence.hpp new file mode 100644 index 0000000..452c061 --- /dev/null +++ b/include/aoc/VertexEdgeIncidence.hpp @@ -0,0 +1,28 @@ +// Solutions to the Advent Of Code 2024. +// Copyright (C) 2025 Stefan Müller +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +#pragma once + +class VertexEdgeIncidence +{ +public: + VertexEdgeIncidence(const int vertex, const int next) + { + this->vertex = vertex; + this->next = next; + } + int vertex; + int next; +}; diff --git a/include/aoc/WeightedEdgeGraph.hpp b/include/aoc/WeightedEdgeGraph.hpp new file mode 100644 index 0000000..3a99c5f --- /dev/null +++ b/include/aoc/WeightedEdgeGraph.hpp @@ -0,0 +1,33 @@ +// Solutions to the Advent Of Code 2024. +// Copyright (C) 2025 Stefan Müller +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +#pragma once + +#include + +#include + +class WeightedEdgeGraph +{ +public: + WeightedEdgeGraph(); + int addVertex(); + void addEdge(const int vertex1, const int vertex2, const int weight); + int dijkstra(const int source, const int target) const; +private: + std::vector firstVertexIncidences_; + std::vector vertexEdgeIncidences_; + std::vector edgeWeights_; +}; diff --git a/src/Program.cpp b/src/Program.cpp index f88ac2d..6c970b9 100644 --- a/src/Program.cpp +++ b/src/Program.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include void Program::run() @@ -69,6 +70,7 @@ void Program::runSolvers() runSolver(solverEngine); runSolver(solverEngine); runSolver(solverEngine); + runSolver(solverEngine); runSolver(solverEngine); } diff --git a/src/ReindeerMaze.cpp b/src/ReindeerMaze.cpp new file mode 100644 index 0000000..8347373 --- /dev/null +++ b/src/ReindeerMaze.cpp @@ -0,0 +1,258 @@ +// Solutions to the Advent Of Code 2024. +// Copyright (C) 2025 Stefan Müller +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +#include + +ReindeerMaze::ReindeerMaze(const int inputFileNameSuffix) + : LinesSolver{ inputFileNameSuffix } +{ +} + +const std::string ReindeerMaze::getPuzzleName() const +{ + return "Reindeer Maze"; +} + +const int ReindeerMaze::getPuzzleDay() const +{ + return 16; +} + +void ReindeerMaze::finish() +{ + // Initializes the graph of path segment incidences. + WeightedEdgeGraph graph{}; + auto entry = graph.addVertex(); + auto exit = graph.addVertex(); + + // Uses list for work items to prevent invalidation of iterators on add. + std::list crossings{}; + initializeWorkList(crossings, entry); + + while (!crossings.empty()) + { + auto startCrossing{ --crossings.end() }; + Point2 startPosition = startCrossing->getPosition(); + auto incidence = std::find_if(startCrossing->incidences.begin(), startCrossing->incidences.end(), + [](auto& x) { return x.getPathVertex() == -1; }); + Point2 direction{ incidence->getDirection() }; + Point2 backwards{ -direction }; + Point2 position{ startPosition + direction }; + + int pathCost{ 1 }; + bool isSkipped{ false }; + + int nNext{ 0 }; + while (getCharAt(position) != getEndChar() && + std::find_if(crossings.begin(), crossings.end(), + [position](auto& x) { return x.getPosition() == position; }) == crossings.end()) + { + nNext = 0; + Point2 nextPosition, nextDirection; + for (const auto& checkDirection : Point2::cardinalDirections) + { + if (checkDirection != backwards) + { + Point2 checkPosition{ checkDirection + position }; + if (getCharAt(checkPosition) != getWallChar()) + { + nNext++; + if (nNext == 1) + { + // Found a possible next step. + nextPosition = checkPosition; + nextDirection = checkDirection; + } + else if (nNext == 2) + { + // Found a second possible step, i.e. a new crossing. Will stop processing this path + // segment. + crossings.emplace_back(position); + crossings.back().incidences.emplace_back(backwards); + crossings.back().incidences.emplace_back(nextDirection); + crossings.back().incidences.emplace_back(checkDirection); + } + else + { + // Found another possible step. Adds the incidence to the new crossing. + crossings.back().incidences.emplace_back(checkDirection); + } + } + } + } + + if (nNext == 1) + { + position = nextPosition; + pathCost++; + if (direction != nextDirection) + { + pathCost += getTurnCost(); + direction = nextDirection; + backwards = -direction; + } + + if (position == startPosition) + { + // Stops processing this path segment because it is a loop, and remove both incidences from the + // start crossing. Both must exist here. + startCrossing->incidences.erase(incidence); + auto endIncidence = std::find_if(startCrossing->incidences.begin(), startCrossing->incidences.end(), + [backwards](auto& x) { return x.getDirection() == backwards; }); + startCrossing->incidences.erase(endIncidence); + isSkipped = true; + break; + } + } + else if (nNext == 0) + { + // Stops processing this path segment because it is a dead-end, and remove it from the start crossing. + startCrossing->incidences.erase(incidence); + isSkipped = true; + break; + } + else + { + // Stops processing this path segment because a new crossing has been found. This check avoids the + // find_if() call in the loop condition. + break; + } + } + + if (!isSkipped) + { + // Adds the new maze path segment as a vertex. + auto pathVertex = graph.addVertex(); + + // Updates start crossing. + incidence->setPathVertex(pathVertex); + incidence->setPathCost(pathCost); + + // Determines the end crossing of the new path segment and its incidence. + std::list::iterator endCrossing; + std::vector::iterator endIncidence; + endCrossing = nNext == 1 + ? endCrossing = std::find_if(crossings.begin(), crossings.end(), + [position](auto& x) { return x.getPosition() == position; }) + : --crossings.end(); + if (endCrossing != crossings.end()) + { + // This incidence must exist, no need to check the incidence iterator. + endIncidence = std::find_if(endCrossing->incidences.begin(), endCrossing->incidences.end(), + [backwards](auto& x) { return x.getDirection() == backwards; }); + endIncidence->setPathVertex(pathVertex); + endIncidence->setPathCost(pathCost); + } + + // Connects the new path segment to all adjacent path segments. + AddPathSegmentEdges(graph, *incidence, startCrossing->incidences); + if (endCrossing != crossings.end()) + { + AddPathSegmentEdges(graph, *endIncidence, endCrossing->incidences); + } + else + { + graph.addEdge(pathVertex, exit, pathCost); + } + + // Checks if end crossing is finished. + //checkFinishedCrossing(crossings, endCrossing); + if (endCrossing != crossings.end() && endCrossing->isFinished()) + { + crossings.erase(endCrossing); + } + } + + // Checks if start crossing is finished. + if (startCrossing->isFinished()) + { + crossings.erase(startCrossing); + } + } + + part1 = graph.dijkstra(entry, exit) / 2; +} + +constexpr char ReindeerMaze::getStartChar() +{ + return 'S'; +} + +constexpr char ReindeerMaze::getEndChar() +{ + return 'E'; +} + +constexpr char ReindeerMaze::getWallChar() +{ + return '#'; +} + +constexpr int ReindeerMaze::getTurnCost() +{ + return 1000; +} + +// Initializes the work list of crossing incidences. +void ReindeerMaze::initializeWorkList(std::list& crossings, const int entryVertex) +{ + Point2 start{ findStart() }; + crossings.emplace_back(start); + crossings.back().incidences.emplace_back(Point2::left, entryVertex); + addCheckedIncidence(crossings.back().incidences, start, Point2::right); + addCheckedIncidence(crossings.back().incidences, start, Point2::up); +} + +void ReindeerMaze::addCheckedIncidence(std::vector& incidences, const Point2 start, + const Point2 direction) +{ + if (getCharAt(start + direction) != getWallChar()) + { + incidences.emplace_back(direction); + } +} + +Point2 ReindeerMaze::findStart() +{ + for (int j = 0; j < lines.size(); j++) + { + for (int i = 0; i < lines[j].size(); i++) + { + if (lines[j][i] == getStartChar()) + { + return { i, j }; + } + } + } + return { 0, 0 }; +} + +void ReindeerMaze::AddPathSegmentEdges(WeightedEdgeGraph& graph, const ReindeerMazePathIncidence& pathIncidence, + const std::vector& otherPathIncidences) +{ + for (auto& otherIncidence : otherPathIncidences) + { + if (otherIncidence.getPathVertex() > -1 && otherIncidence.getPathVertex() != pathIncidence.getPathVertex()) + { + int weight{ pathIncidence.getPathCost() + otherIncidence.getPathCost() }; + // Checks for turn, i.e. perpendicular directions. + if (otherIncidence.getDirection().x != -pathIncidence.getDirection().x) + { + weight += 2 * getTurnCost(); + } + graph.addEdge(pathIncidence.getPathVertex(), otherIncidence.getPathVertex(), weight); + } + } +} diff --git a/src/ReindeerMazeCrossing.cpp b/src/ReindeerMazeCrossing.cpp new file mode 100644 index 0000000..26edcce --- /dev/null +++ b/src/ReindeerMazeCrossing.cpp @@ -0,0 +1,32 @@ +// Solutions to the Advent Of Code 2024. +// Copyright (C) 2025 Stefan Müller +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +#include + +ReindeerMazeCrossing::ReindeerMazeCrossing(const Point2 position) + : position_{ position }, incidences{} +{ +} + +Point2 ReindeerMazeCrossing::getPosition() const +{ + return position_; +} + +bool ReindeerMazeCrossing::isFinished() const +{ + return std::find_if(incidences.begin(), incidences.end(), [](auto& x) { return x.getPathVertex() < 0; }) == + incidences.end(); +} diff --git a/src/ReindeerMazePathIncidence.cpp b/src/ReindeerMazePathIncidence.cpp new file mode 100644 index 0000000..fbaa5ae --- /dev/null +++ b/src/ReindeerMazePathIncidence.cpp @@ -0,0 +1,51 @@ +// Solutions to the Advent Of Code 2024. +// Copyright (C) 2025 Stefan Müller +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +#include + +ReindeerMazePathIncidence::ReindeerMazePathIncidence(const Point2 direction) + : ReindeerMazePathIncidence(direction, -1) +{ +} + +ReindeerMazePathIncidence::ReindeerMazePathIncidence(const Point2 direction, const int pathVertex) + : direction_{ direction }, pathVertex_{ pathVertex }, pathCost_{ 0 } +{ +} + +Point2 ReindeerMazePathIncidence::getDirection() const +{ + return direction_; +} + +int ReindeerMazePathIncidence::getPathVertex() const +{ + return pathVertex_; +} + +void ReindeerMazePathIncidence::setPathVertex(const int pathVertex) +{ + pathVertex_ = pathVertex; +} + +int ReindeerMazePathIncidence::getPathCost() const +{ + return pathCost_; +} + +void ReindeerMazePathIncidence::setPathCost(const int pathCost) +{ + pathCost_ = pathCost; +} diff --git a/src/WeightedEdgeGraph.cpp b/src/WeightedEdgeGraph.cpp new file mode 100644 index 0000000..dee4144 --- /dev/null +++ b/src/WeightedEdgeGraph.cpp @@ -0,0 +1,70 @@ +// Solutions to the Advent Of Code 2024. +// Copyright (C) 2025 Stefan Müller +// +// This program is free software: you can redistribute it and/or modify it under +// the terms of the GNU General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later +// version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with +// this program. If not, see . + +#include + +#include +#include + +WeightedEdgeGraph::WeightedEdgeGraph() + : firstVertexIncidences_{}, vertexEdgeIncidences_{}, edgeWeights_{} +{ +} + +int WeightedEdgeGraph::addVertex() +{ + firstVertexIncidences_.push_back(-1); + return (int)firstVertexIncidences_.size() - 1; +} + +void WeightedEdgeGraph::addEdge(const int vertex1, const int vertex2, const int weight) +{ + vertexEdgeIncidences_.emplace_back(vertex2, firstVertexIncidences_[vertex1]); + firstVertexIncidences_[vertex1] = (int)vertexEdgeIncidences_.size() - 1; + vertexEdgeIncidences_.emplace_back(vertex1, firstVertexIncidences_[vertex2]); + firstVertexIncidences_[vertex2] = (int)vertexEdgeIncidences_.size() - 1; + edgeWeights_.push_back(weight); +} + +int WeightedEdgeGraph::dijkstra(const int source, const int target) const +{ + std::vector distances(firstVertexIncidences_.size(), std::numeric_limits::max()); + auto compare = [&distances](int left, int right) { return distances[left] > distances[right]; }; + std::priority_queue, decltype(compare)> queue{ compare }; + + distances[source] = 0; + queue.push(source); + + while (!queue.empty()) + { + int v{ queue.top() }; + queue.pop(); + + int incidence{ firstVertexIncidences_[v] }; + while (incidence > -1) + { + int neighbor{ vertexEdgeIncidences_[incidence].vertex }; + int newDistance{ distances[v] + edgeWeights_[incidence >> 1] }; + if (distances[neighbor] > newDistance) + { + distances[neighbor] = newDistance; + queue.push(neighbor); + } + incidence = vertexEdgeIncidences_[incidence].next; + } + } + + return distances[target]; +} diff --git a/tests/src/TestCases.cpp b/tests/src/TestCases.cpp index 651b5bd..c063546 100644 --- a/tests/src/TestCases.cpp +++ b/tests/src/TestCases.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #define REQUIRE_MESSAGE(cond, msg) if (!(cond)) { INFO(msg); REQUIRE(cond); } @@ -252,6 +253,23 @@ TEST_CASE("[WarehouseWoesTests]") } } +TEST_CASE("[ReindeerMazeTests]") +{ + TestContext test; + SECTION("FullData") + { + test.run(std::make_unique(), 72400, 0, test.getInputPaths()); + } + SECTION("ExampleData") + { + test.run(std::make_unique(), 7036, 0, test.getExampleInputPaths()); + } + SECTION("ExampleData2") + { + test.run(std::make_unique(2), 11048, 0, test.getExampleInputPaths()); + } +} + TEST_CASE("[LanPartyTests]") { TestContext test;