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

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

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