// 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 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() { // Maps vertex index of the resulting graph to a pair of ids of the connected positions in the original maze. This // could technically be a vector, since the graph will return incremental non-negative integer vertex ids, but we // do not want to hardcode this. VertexAttachedPositions vertexAttachedPositions; WeightedEdgeGraph graph{}; auto entry = graph.addVertex(0); auto exit = graph.addVertex(0); buildPathSegmentGraph(graph, vertexAttachedPositions, entry, exit); auto result = graph.dijkstra(exit); part1 = result.distances[entry] / 2; part2 = calcShortestPaths(graph, vertexAttachedPositions, entry, exit, result.distances); } constexpr char ReindeerMaze::getStartChar() { return 'S'; } constexpr char ReindeerMaze::getEndChar() { return 'E'; } constexpr char ReindeerMaze::getWallChar() { return '#'; } constexpr int ReindeerMaze::getTurnCost() { return 1000; } // Constructs the graph of path segment incidences, starting with a graph that already contains the entry and the exit // vertices. void ReindeerMaze::buildPathSegmentGraph(WeightedEdgeGraph& graph, VertexAttachedPositions& vertexAttachedPositions, const int entry, const int exit) { // 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 }; int nTiles{ 0 }; 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++; nTiles++; 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(nTiles); vertexAttachedPositions.emplace(pathVertex, makePositionsIdPair(startPosition, position)); // 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); } } } // 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() const { 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); } } } std::pair ReindeerMaze::makePositionsIdPair(const Point2& position1, const Point2& position2) { int offset{ static_cast(lines.size()) }; return std::make_pair(position1.x * offset + position1.y, position2.x * offset + position2.y); } int ReindeerMaze::calcShortestPaths(const WeightedEdgeGraph& graph, const VertexAttachedPositions& vertexAttachedPositions, const int entry, const int exit, const std::vector& shortestDistances) { std::set shortestPathsVertices{}; std::stack stack{}; auto v = graph.begin(entry); stack.emplace(v); int cost{ 0 }; std::set stackedVertices{}; stackedVertices.insert(entry); while (!stack.empty()) { auto& current = stack.top(); int newCost{ 0 }; while (current != graph.end() && ((newCost = graph.getEdgeWeight(current->edge) + cost) + shortestDistances[current->vertex] > shortestDistances[entry] || stackedVertices.contains(current->vertex))) { current++; } bool isPathEnd{ current == graph.end() }; if (!isPathEnd) { if (current->vertex == exit) { // Adds this discovered shortest path to the result vertex set, and ends the path, since no other // neighbor of the previous vertex can also lead to a shortest path with the same path stack. The last // edge will be popped below. shortestPathsVertices.insert(stackedVertices.begin(), stackedVertices.end()); isPathEnd = true; } else { // Adds the next valid neighbor vertex to the current path on the stack. stackedVertices.insert(current->vertex); stack.emplace(graph.begin(current->vertex)); cost = newCost; } } if (isPathEnd) { // Backtracks the last edge, since the path cannot continue from it. stack.pop(); if (!stack.empty()) { stackedVertices.erase(stack.top()->vertex); cost -= graph.getEdgeWeight(stack.top()->edge); stack.top()++; } } } // Erases entry vertex because it has no attached vertices, and no weight. shortestPathsVertices.erase(entry); return std::accumulate(shortestPathsVertices.begin(), shortestPathsVertices.end(), getNUniqueAttachedPositions(vertexAttachedPositions, shortestPathsVertices), [&graph](int total, int x) { return total + graph.getVertexWeight(x); }); } int ReindeerMaze::getNUniqueAttachedPositions(const VertexAttachedPositions& vertexAttachedPositions, const std::set& vertices) { std::set positions{}; for (const int x : vertices) { positions.insert(vertexAttachedPositions.at(x).first); positions.insert(vertexAttachedPositions.at(x).second); } return static_cast(positions.size()); }