AdventOfCode2024/src/ReindeerMaze.cpp

263 lines
9.5 KiB
C++

// 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 <http://www.gnu.org/licenses/>.
#include <aoc/ReindeerMaze.hpp>
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<ReindeerMazeCrossing> 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<ReindeerMazeCrossing>::iterator endCrossing;
std::vector<ReindeerMazePathIncidence>::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<ReindeerMazeCrossing>& 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<ReindeerMazePathIncidence>& 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<ReindeerMazePathIncidence>& 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);
}
}
}