Add solution for "Day 16: Reindeer Maze", part 1

This commit is contained in:
Stefan Müller 2025-04-30 19:40:36 +02:00
parent a819caba8b
commit 08a94ba068
11 changed files with 603 additions and 0 deletions

View File

@ -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 <http://www.gnu.org/licenses/>.
#pragma once
#include <list>
#include <vector>
#include <aoc/LinesSolver.hpp>
#include <aoc/ReindeerMazeCrossing.hpp>
#include <aoc/ReindeerMazePathIncidence.hpp>
#include <aoc/WeightedEdgeGraph.hpp>
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<ReindeerMazeCrossing>& crossings, const int entryVertex);
void addCheckedIncidence(std::vector<ReindeerMazePathIncidence>& incidences, const Point2 start,
const Point2 direction);
Point2 findStart();
void AddPathSegmentEdges(WeightedEdgeGraph& graph, const ReindeerMazePathIncidence& pathIncidence,
const std::vector<ReindeerMazePathIncidence>& otherPathIncidences);
};

View File

@ -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 <http://www.gnu.org/licenses/>.
#pragma once
#include <vector>
#include <aoc/Point2.hpp>
#include <aoc/ReindeerMazePathIncidence.hpp>
class ReindeerMazeCrossing
{
public:
ReindeerMazeCrossing(const Point2 position);
Point2 getPosition() const;
bool isFinished() const;
std::vector<ReindeerMazePathIncidence> incidences;
private:
Point2 position_;
};

View File

@ -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 <http://www.gnu.org/licenses/>.
#pragma once
#include <aoc/Point2.hpp>
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_;
};

View File

@ -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 <http://www.gnu.org/licenses/>.
#pragma once
class VertexEdgeIncidence
{
public:
VertexEdgeIncidence(const int vertex, const int next)
{
this->vertex = vertex;
this->next = next;
}
int vertex;
int next;
};

View File

@ -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 <http://www.gnu.org/licenses/>.
#pragma once
#include <vector>
#include <aoc/VertexEdgeIncidence.hpp>
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<int> firstVertexIncidences_;
std::vector<VertexEdgeIncidence> vertexEdgeIncidences_;
std::vector<int> edgeWeights_;
};

View File

@ -36,6 +36,7 @@
#include <aoc/ClawContraption.hpp>
#include <aoc/RestroomRedoubt.hpp>
#include <aoc/WarehouseWoes.hpp>
#include <aoc/ReindeerMaze.hpp>
#include <aoc/LanParty.hpp>
void Program::run()
@ -69,6 +70,7 @@ void Program::runSolvers()
runSolver<ClawContraption>(solverEngine);
runSolver<RestroomRedoubt>(solverEngine);
runSolver<WarehouseWoes>(solverEngine);
runSolver<ReindeerMaze>(solverEngine);
runSolver<LanParty>(solverEngine);
}

258
src/ReindeerMaze.cpp Normal file
View File

@ -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 <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()
{
// 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<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);
}
}
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<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()
{
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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
#include <aoc/ReindeerMazeCrossing.hpp>
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();
}

View File

@ -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 <http://www.gnu.org/licenses/>.
#include <aoc/ReindeerMazePathIncidence.hpp>
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;
}

70
src/WeightedEdgeGraph.cpp Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
#include <aoc/WeightedEdgeGraph.hpp>
#include <limits>
#include <queue>
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<int> distances(firstVertexIncidences_.size(), std::numeric_limits<int>::max());
auto compare = [&distances](int left, int right) { return distances[left] > distances[right]; };
std::priority_queue<int, std::vector<int>, 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];
}

View File

@ -33,6 +33,7 @@
#include <aoc/ClawContraption.hpp>
#include <aoc/RestroomRedoubt.hpp>
#include <aoc/WarehouseWoes.hpp>
#include <aoc/ReindeerMaze.hpp>
#include <aoc/LanParty.hpp>
#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<ReindeerMaze>(), 72400, 0, test.getInputPaths());
}
SECTION("ExampleData")
{
test.run(std::make_unique<ReindeerMaze>(), 7036, 0, test.getExampleInputPaths());
}
SECTION("ExampleData2")
{
test.run(std::make_unique<ReindeerMaze>(2), 11048, 0, test.getExampleInputPaths());
}
}
TEST_CASE("[LanPartyTests]")
{
TestContext test;