263 lines
9.5 KiB
C++
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);
|
|
}
|
|
}
|
|
}
|