// 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() { WeightedEdgeGraph graph{}; auto entry = graph.addVertex(); auto exit = graph.addVertex(); buildPathSegmentGraph(graph, entry, exit); 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; } // 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, 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 }; 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); } } } // 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); } } }