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

This commit is contained in:
Stefan Müller 2025-05-14 21:16:14 +02:00
parent 897bab12bf
commit 26dfb379a2
4 changed files with 139 additions and 22 deletions

View File

@ -114,6 +114,16 @@ The idea for this solver was pretty straight forward. The algorithm moves the ro
It is more complicated for part 2 of course, since here horizontal and vertical directions behave differently. For vertical pushes there are two phases, the first one where individual boxes to be pushed are found, tracked, and checked for walls behind them, and a second phase where all those boxes are moved in reverse order.
### Day 16: Reindeer Maze
:mag_right: Puzzle: <https://adventofcode.com/2024/day/16>, :white_check_mark: Solver: [`ReindeerMaze.cpp`](src/ReindeerMaze.cpp)
For this puzzle, the solver constructs a graph with a vertex for each path segment in the maze, and where any pair of vertices is adjacent if and only if the path segments they represent are adjacent to the same crossing in the maze. Here, a path segment is a contiguous set of adjacent tiles, such that each tile has exactly two neighboring tiles.
Dijkstra's algorithm solves part 1, with the weights of each edge being the sum of half of the costs of the path segments represented by its two vertices, plus the 1000 if a turn is required to transition from one of these path segments to the other.
Since Dijkstra's algorithm cheaply produces shortest distances from the exit to all other vertices in a single run, the solver can use these to quickly traverse the graph to find all minimum paths for part 2. In order to calculate the number of tiles for all these paths, duplicate path segments and crossings have to be detected. The solver attaches the number of tiles per path segment and the connected crossings to each vertex while constructing the graph to facilitate this.
## Thanks
* [Alexander Brouwer](https://github.com/Bromvlieg) for getting the project set up with CMake.

View File

@ -16,6 +16,8 @@
#pragma once
#include <list>
#include <map>
#include <set>
#include <vector>
#include <aoc/common/WeightedEdgeGraph.hpp>
@ -32,15 +34,22 @@ class ReindeerMaze
virtual const int getPuzzleDay() const override;
virtual void finish() override;
private:
typedef std::map<int, std::pair<int, int>> VertexAttachedPositions;
static constexpr char getStartChar();
static constexpr char getEndChar();
static constexpr char getWallChar();
static constexpr int getTurnCost();
void buildPathSegmentGraph(WeightedEdgeGraph& graph, const int entry, const int exit);
void buildPathSegmentGraph(WeightedEdgeGraph& graph, VertexAttachedPositions& vertexAttachedPositions,
const int entry, const int exit);
void initializeWorkList(std::list<ReindeerMazeCrossing>& crossings, const int entryVertex);
void addCheckedIncidence(std::vector<ReindeerMazePathIncidence>& incidences, const Point2 start,
const Point2 direction);
Point2 findStart() const;
void AddPathSegmentEdges(WeightedEdgeGraph& graph, const ReindeerMazePathIncidence& pathIncidence,
void addPathSegmentEdges(WeightedEdgeGraph& graph, const ReindeerMazePathIncidence& pathIncidence,
const std::vector<ReindeerMazePathIncidence>& otherPathIncidences);
std::pair<int, int> makePositionsIdPair(const Point2& position1, const Point2& position2);
int calcShortestPaths(const WeightedEdgeGraph& graph, const VertexAttachedPositions& vertexAttachedPositions,
const int entry, const int exit, const std::vector<int>& shortestDistances);
int getNUniqueAttachedPositions(const VertexAttachedPositions& vertexAttachedPositions,
const std::set<int>& vertices);
};

View File

@ -13,10 +13,12 @@
// 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 <numeric>
#include <stack>
#include <aoc/ReindeerMaze.hpp>
ReindeerMaze::ReindeerMaze(const int inputFileNameSuffix)
: LinesSolver{ inputFileNameSuffix }
ReindeerMaze::ReindeerMaze(const int inputFileNameSuffix) : LinesSolver{ inputFileNameSuffix }
{
}
@ -32,12 +34,19 @@ const int ReindeerMaze::getPuzzleDay() const
void ReindeerMaze::finish()
{
WeightedEdgeGraph graph{};
auto entry = graph.addVertex();
auto exit = graph.addVertex();
buildPathSegmentGraph(graph, entry, exit);
// 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;
part1 = graph.dijkstra(entry, exit) / 2;
WeightedEdgeGraph graph{};
auto entry = graph.addVertex(0);
auto exit = graph.addVertex(0);
buildPathSegmentGraph(graph, vertexAttachedPositions, entry, exit);
auto shortestDistances = graph.dijkstra(exit);
part1 = shortestDistances[entry] / 2;
part2 = calcShortestPaths(graph, vertexAttachedPositions, entry, exit, shortestDistances);
}
constexpr char ReindeerMaze::getStartChar()
@ -62,7 +71,8 @@ constexpr int ReindeerMaze::getTurnCost()
// 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)
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<ReindeerMazeCrossing> crossings{};
@ -79,12 +89,13 @@ void ReindeerMaze::buildPathSegmentGraph(WeightedEdgeGraph& graph, const int ent
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())
std::find_if(crossings.begin(), crossings.end(),
[position](auto& x) { return x.getPosition() == position; }) == crossings.end())
{
nNext = 0;
Point2 nextPosition, nextDirection;
@ -124,6 +135,7 @@ void ReindeerMaze::buildPathSegmentGraph(WeightedEdgeGraph& graph, const int ent
{
position = nextPosition;
pathCost++;
nTiles++;
if (direction != nextDirection)
{
pathCost += getTurnCost();
@ -161,7 +173,8 @@ void ReindeerMaze::buildPathSegmentGraph(WeightedEdgeGraph& graph, const int ent
if (!isSkipped)
{
// Adds the new maze path segment as a vertex.
auto pathVertex = graph.addVertex();
auto pathVertex = graph.addVertex(nTiles);
vertexAttachedPositions.emplace(pathVertex, makePositionsIdPair(startPosition, position));
// Updates start crossing.
incidence->setPathVertex(pathVertex);
@ -170,9 +183,10 @@ void ReindeerMaze::buildPathSegmentGraph(WeightedEdgeGraph& graph, const int ent
// 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();
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.
@ -183,10 +197,10 @@ void ReindeerMaze::buildPathSegmentGraph(WeightedEdgeGraph& graph, const int ent
}
// Connects the new path segment to all adjacent path segments.
AddPathSegmentEdges(graph, *incidence, startCrossing->incidences);
addPathSegmentEdges(graph, *incidence, startCrossing->incidences);
if (endCrossing != crossings.end())
{
AddPathSegmentEdges(graph, *endIncidence, endCrossing->incidences);
addPathSegmentEdges(graph, *endIncidence, endCrossing->incidences);
}
else
{
@ -243,7 +257,7 @@ Point2 ReindeerMaze::findStart() const
return { 0, 0 };
}
void ReindeerMaze::AddPathSegmentEdges(WeightedEdgeGraph& graph, const ReindeerMazePathIncidence& pathIncidence,
void ReindeerMaze::addPathSegmentEdges(WeightedEdgeGraph& graph, const ReindeerMazePathIncidence& pathIncidence,
const std::vector<ReindeerMazePathIncidence>& otherPathIncidences)
{
for (auto& otherIncidence : otherPathIncidences)
@ -260,3 +274,87 @@ void ReindeerMaze::AddPathSegmentEdges(WeightedEdgeGraph& graph, const ReindeerM
}
}
}
std::pair<int, int> ReindeerMaze::makePositionsIdPair(const Point2& position1, const Point2& position2)
{
int offset{ static_cast<int>(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<int>& shortestDistances)
{
std::set<int> shortestPathsVertices{};
std::stack<WeightedEdgeGraph::NeighborIterator> stack{};
auto v = graph.begin(entry);
stack.emplace(v);
int cost{ 0 };
std::set<int> 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<int>& vertices)
{
std::set<int> positions{};
for (const int x : vertices)
{
positions.insert(vertexAttachedPositions.at(x).first);
positions.insert(vertexAttachedPositions.at(x).second);
}
return static_cast<int>(positions.size());
}

View File

@ -258,15 +258,15 @@ TEST_CASE("[ReindeerMazeTests]")
TestContext test;
SECTION("FullData")
{
test.run(std::make_unique<ReindeerMaze>(), 72400, 0, test.getInputPaths());
test.run(std::make_unique<ReindeerMaze>(), 72400, 435, test.getInputPaths());
}
SECTION("ExampleData")
{
test.run(std::make_unique<ReindeerMaze>(), 7036, 0, test.getExampleInputPaths());
test.run(std::make_unique<ReindeerMaze>(), 7036, 45, test.getExampleInputPaths());
}
SECTION("ExampleData2")
{
test.run(std::make_unique<ReindeerMaze>(2), 11048, 0, test.getExampleInputPaths());
test.run(std::make_unique<ReindeerMaze>(2), 11048, 64, test.getExampleInputPaths());
}
}